[PATCH] Samba VirusFilter (version 12)

Jeremy Allison jra at samba.org
Mon Jan 22 22:34:15 UTC 2018


On Fri, Jan 12, 2018 at 06:45:42PM -0700, Trever L. Adams wrote:
> Opinion: vfs_streams_depot should be one of those that transforms the
> name since it changes the file name the system is accessing. This would
> fix the problem I mentioned earlier. It seems I had a patch for this,
> but cannot find it. By chance do you have a copy of such a thing as part
> of working with me? It seems like it modified the open function and
> added a flag and changed is_ntfs_stream_smb_fname to check for that
> flag. Or should it have something like catia_realpath(...) and
> virusfilter do a realpath call? (I am sorry, still new to the VFS
> interface and trying to figure everything out.
> 
> As for the distributed file system, if it is mounted as an actual fs by
> the kernel, it can be scanned. Do these modules do this? Or are they
> hidden from the kernel view some how?
> 
> Ok, a possible solution:
> 
> Rename action currently just blindly renames. It does no checks. This
> was by design. It may be incorrect, I wrote the code and stand open for
> criticism of it. I figured since the user can see the files and may
> notice the rename, things aren't all that bad.
> 
> Quarantine is not viewable by the user (not normally anyway). There
> should be some way of preserving multiple copies. For about two weeks
> now, I have been wondering if they should have a time stamp (i.e.
> 2018-01-12-18:29:57.1234) appended to the name instead of the mkstemp
> format. The problem is how do we check to see if the underlying name
> exists? Is such a step necessary? What resolution provides a reasonable
> enough granularity (i.e. seconds, milliseconds, etc. I figure at least
> seconds)?

OK, here is a new version that has been modified to fill README.Coding
standards and gets around the stackable problem by quarantining detected
failures to a filename with a 16-byte randomly generated string appended
to it (generated by generate_random_str()). As these are problem files
that the user doesn't have access to anyway, I think this is an acceptable
solution.

Trever can you test this patchset to make sure it's all OK, and once
that's done, Ralph if you could review it, then I think (fingers crossed :-)
we're finally good to merge into master !

Cheers,

	Jeremy.
-------------- next part --------------
From 92b1194feba6217d22efb2c45ffcbf9e9886d84f Mon Sep 17 00:00:00 2001
From: "Trever L. Adams" <trever.adams at gmail.com>
Date: Tue, 18 Oct 2016 13:37:19 -0600
Subject: [PATCH 1/5] Samba-VirusFilter: memcache changes.

Signed-off-by: Trever L. Adams <trever.adams at gmail.com>
Reviewed-by: Jeremy Allison <jra at samba.org>
---
 lib/util/memcache.c | 1 +
 lib/util/memcache.h | 3 ++-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/lib/util/memcache.c b/lib/util/memcache.c
index acd663cd4e7..819ba44b443 100644
--- a/lib/util/memcache.c
+++ b/lib/util/memcache.c
@@ -55,6 +55,7 @@ static bool memcache_is_talloc(enum memcache_number n)
 	case SINGLETON_CACHE_TALLOC:
 	case SHARE_MODE_LOCK_CACHE:
 	case GETWD_CACHE:
+	case VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC:
 		result = true;
 		break;
 	default:
diff --git a/lib/util/memcache.h b/lib/util/memcache.h
index b87746bc928..670cbd6821f 100644
--- a/lib/util/memcache.h
+++ b/lib/util/memcache.h
@@ -44,7 +44,8 @@ enum memcache_number {
 	SINGLETON_CACHE_TALLOC,	/* talloc */
 	SINGLETON_CACHE,
 	SMB1_SEARCH_OFFSET_MAP,
-	SHARE_MODE_LOCK_CACHE	/* talloc */
+	SHARE_MODE_LOCK_CACHE,	/* talloc */
+	VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC /* talloc */
 };
 
 /*
-- 
2.16.0.rc1.238.g530d649a79-goog


From d84f4d159dfb2650a45ae7c288592dcc984546d7 Mon Sep 17 00:00:00 2001
From: "Trever L. Adams" <trever.adams at gmail.com>
Date: Tue, 18 Oct 2016 13:34:53 -0600
Subject: [PATCH 2/5] Samba-VirusFilter: common headers and sources.

Samba-VirusFilter Contributors:

SATOH Fumiyasu @ OSS Technology Corp., Japan
Module creator/maintainer

Luke Dixon luke.dixon at zynstra.com
Samba 4 support

Trever L. Adams
Documentation
Code contributions
Samba-master merge work

With many thanks to the Samba Team.

Signed-off-by: Trever L. Adams <trever.adams at gmail.com>
Signed-off-by: SATOH Fumiyasu <fumiyas at osstech.co.jp>
Reviewed-by: Jeremy Allison <jra at samba.org>
---
 docs-xml/manpages/vfs_virusfilter.8.xml            |  336 +++++
 docs-xml/wscript_build                             |    1 +
 .../scripts/vfs/virusfilter/virusfilter-notify.ksh |  284 ++++
 source3/modules/vfs_virusfilter.c                  | 1454 ++++++++++++++++++++
 source3/modules/vfs_virusfilter_common.h           |  149 ++
 source3/modules/vfs_virusfilter_utils.c            | 1031 ++++++++++++++
 source3/modules/vfs_virusfilter_utils.h            |  177 +++
 source3/modules/wscript_build                      |   13 +
 source3/wscript                                    |    2 +-
 9 files changed, 3446 insertions(+), 1 deletion(-)
 create mode 100644 docs-xml/manpages/vfs_virusfilter.8.xml
 create mode 100644 examples/scripts/vfs/virusfilter/virusfilter-notify.ksh
 create mode 100644 source3/modules/vfs_virusfilter.c
 create mode 100644 source3/modules/vfs_virusfilter_common.h
 create mode 100644 source3/modules/vfs_virusfilter_utils.c
 create mode 100644 source3/modules/vfs_virusfilter_utils.h

diff --git a/docs-xml/manpages/vfs_virusfilter.8.xml b/docs-xml/manpages/vfs_virusfilter.8.xml
new file mode 100644
index 00000000000..eb6112e3827
--- /dev/null
+++ b/docs-xml/manpages/vfs_virusfilter.8.xml
@@ -0,0 +1,336 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE refentry PUBLIC "-//Samba-Team//DTD DocBook V4.2-Based Variant V1.0//EN" "http://www.samba.org/samba/DTD/samba-doc">
+<refentry id="vfs_virusfilter.8">
+
+<refmeta>
+	<refentrytitle>vfs_virusfilter</refentrytitle>
+	<manvolnum>8</manvolnum>
+	<refmiscinfo class="source">Samba</refmiscinfo>
+	<refmiscinfo class="manual">System Administration tools</refmiscinfo>
+	<refmiscinfo class="version">4.8</refmiscinfo>
+</refmeta>
+
+
+<refnamediv>
+	<refname>vfs_virusfilter</refname>
+	<refpurpose>On access virus scanner</refpurpose>
+</refnamediv>
+
+<refsynopsisdiv>
+	<cmdsynopsis>
+		<command>vfs objects = virusfilter</command>
+	</cmdsynopsis>
+</refsynopsisdiv>
+
+<refsect1>
+	<title>DESCRIPTION</title>
+
+	<para>This is a set of various Samba VFS modules to scan and filter
+	virus files on Samba file services with an anti-virus scanner.</para>
+
+	<para>This module is stackable.</para>
+
+</refsect1>
+
+<refsect1>
+	<title>OPTIONS</title>
+
+	<variablelist>
+
+		<varlistentry>
+		<term>virusfilter:scanner</term>
+		<listitem>
+		<para>The antivirus scan-engine.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:socket path = PATH</term>
+		<listitem>
+		<para>Path of local socket for the virus scanner.
+		</para>
+		<para>If this option is not set, the default path depends on the
+		configured AV scanning engine.
+		</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:connect timeout = 30000</term>
+		<listitem>
+		<para>Controls how long to wait on connecting to the virus
+		scanning process before timing out. Value is in milliseconds.
+		</para>
+		<para>If this option is not set, the default is 30000.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:io timeout = 60000</term>
+		<listitem>
+		<para>Controls how long to wait on communications with the virus
+		scanning process before timing out. Value is in milliseconds.
+		</para>
+		<para>If this option is not set, the default is 60000.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan on open = yes</term>
+		<listitem>
+		<para>This option controls whether files are scanned on open.
+		</para>
+		<para>If this option is not set, the default is yes.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan on close = no</term>
+		<listitem>
+		<para>This option controls whether files are scanned on close.
+		</para>
+		<para>If this option is not set, the default is no.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:max file size = 100000000</term>
+		<listitem>
+		<para>This is the largest sized file, in bytes, which will be scanned.
+		</para>
+		<para>If this option is not set, the default is 100MB.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:min file size = 10</term>
+		<listitem>
+		<para>This is the smallest sized file, in bytes, which will be scanned.
+		</para>
+		<para>If this option is not set, the default is 10.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:infected file action = nothing</term>
+		<listitem>
+		<para>What to do with an infected file. The options are
+		nothing, quarantine, rename, delete.</para>
+		<para>If this option is not set, the default is nothing.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:infected file errno on open = EACCES</term>
+		<listitem>
+		<para>What errno to return on open if the file is infected.
+		</para>
+		<para>If this option is not set, the default is EACCES.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:infected file errno on close = 0</term>
+		<listitem>
+		<para>What errno to return on close if the file is infected.
+		</para>
+		<para>If this option is not set, the default is 0.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:quarantine directory = PATH</term>
+		<listitem>
+		<para>Where to move infected files. This path must be an
+		absolute path.</para>
+		<para>If this option is not set, the default is ".quarantine"
+		relative to the share path. </para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:quarantine prefix = virusfilter.</term>
+		<listitem>
+		<para>Prefix for quarantined files.</para>
+		<para>If this option is not set, the default is "virusfilter.".</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:quarantine suffix = .infected</term>
+		<listitem>
+		<para>Suffix for quarantined files.
+		This option is only used if keep name is true. Otherwise it is ignored.</para>
+		<para>If this option is not set, the default is ".infected".</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:rename prefix = virusfilter.</term>
+		<listitem>
+		<para>Prefix for infected files.</para>
+		<para>If this option is not set, the default is "virusfilter.".</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:rename suffix = .infected</term>
+		<listitem>
+		<para>Suffix for infected files.</para>
+		<para>If this option is not set, the default is ".infected".</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:quarantine keep tree = yes</term>
+		<listitem>
+		<para>If keep tree is set, the directory structure relative
+		to the share is maintained in the quarantine directory.
+		</para>
+		<para>If this option is not set, the default is yes.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:quarantine keep name = yes</term>
+		<listitem>
+		<para>Should the file name be left unmodified other than adding a suffix
+		and/or prefix and a random suffix name as defined in virusfilter:rename prefix
+		and virusfilter:rename suffix.</para>
+		<para>If this option is not set, the default is yes.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:infected file command = @SAMBA_DATADIR@/bin/virusfilter-notify --mail-to virusmaster at example.com --cc "%U at example.com" --from samba at example.com --subject-prefix "Samba: Infected File: "</term>
+		<listitem>
+		<para>External command to run on an infected file is found.</para>
+		<para>If this option is not set, the default is none.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan archive = true</term>
+		<listitem>
+		<para>This defines whether or not to scan archives.</para>
+		<para>Sophos supports this and defaults to false.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:max nested scan archive = 1</term>
+		<listitem>
+		<para>This defines the maximum depth to search nested archives.</para>
+		<para>The Sophos module supports this and defaults to 1.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan error command = @SAMBA_DATADIR@/bin/virusfilter-notify --mail-to virusmaster at example.com --from samba at example.com --subject-prefix "Samba: Scan Error: "</term>
+		<listitem>
+		<para>External command to run on scan error.</para>
+		<para>If this option is not set, the default is none.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:exclude files = empty</term>
+		<listitem>
+		<para>Files to exclude from scanning.</para>
+		<para>If this option is not set, the default is empty.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:block access on error = false</term>
+		<listitem>
+		<para>Controls whether or not access should be blocked on
+		a scanning error.</para>
+		<para>If this option is not set, the default is false.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan error errno on open = EACCES</term>
+		<listitem>
+		<para>What errno to return on open if there is an error in
+		scanning the file and block access on error is true.
+		</para>
+		<para>If this option is not set, the default is EACCES.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan error errno on close = 0</term>
+		<listitem>
+		<para>What errno to return on close if there is an error in
+		scanning the file and block access on error is true.
+		</para>
+		<para>If this option is not set, the default is 0.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:cache entry limit = 100</term>
+		<listitem>
+		<para>The maximum number of entries in the scanning results
+		cache. Due to how Samba's memcache works, this is approximate.</para>
+		<para>If this option is not set, the default is 100.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:cache time limit = 10</term>
+		<listitem>
+		<para>The maximum number of seconds that a scanning result
+		will stay in the results cache. -1 disables the limit.
+		0 disables caching.</para>
+		<para>If this option is not set, the default is 10.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:quarantine directory mode = 0755</term>
+		<listitem>
+		<para>This is the octet mode for the quarantine directory and
+		its sub-directories as they are created.</para>
+		<para>If this option is not set, the default is 0755 or
+		S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH |
+		S_IXOTH.</para>
+		<para>Permissions must be such that all users can read and
+		search. I.E. don't mess with this unless you really know what
+		you are doing.</para>
+		</listitem>
+		</varlistentry>
+
+	</variablelist>
+</refsect1>
+
+<refsect1>
+	<title>NOTES</title>
+
+	<para>This module can scan other than default streams, if the
+	alternative datastreams are each backed as separate files, such as with
+	the vfs module streams_depot.</para>
+
+	<para>For proper operation the streams support module must be before
+	the virusfilter module in your vfs objects list (i.e. streams_depot
+	must be called before virusfilter module).</para>
+
+	<para>This module is intended for security in depth by providing
+	virus scanning capability on the server. It is not intended to be used
+	in lieu of proper client based security. Other modules for security may
+	exist and may be desirable for security in depth on the server.</para>
+</refsect1>
+
+<refsect1>
+	<title>AUTHOR</title>
+
+	<para>The original Samba software and related utilities
+	were created by Andrew Tridgell. Samba is now developed
+	by the Samba Team as an Open Source project similar
+	to the way the Linux kernel is developed.</para>
+
+</refsect1>
+
+</refentry>
diff --git a/docs-xml/wscript_build b/docs-xml/wscript_build
index f586208b471..954c62a29bc 100644
--- a/docs-xml/wscript_build
+++ b/docs-xml/wscript_build
@@ -90,6 +90,7 @@ manpages='''
          manpages/vfs_time_audit.8
          manpages/vfs_tsmsm.8
          manpages/vfs_unityed_media.8
+         manpages/vfs_virusfilter.8
          manpages/vfs_worm.8
          manpages/vfs_xattr_tdb.8
          manpages/vfstest.1
diff --git a/examples/scripts/vfs/virusfilter/virusfilter-notify.ksh b/examples/scripts/vfs/virusfilter/virusfilter-notify.ksh
new file mode 100644
index 00000000000..a07b9148e83
--- /dev/null
+++ b/examples/scripts/vfs/virusfilter/virusfilter-notify.ksh
@@ -0,0 +1,284 @@
+#!/bin/ksh
+##
+## Samba-VirusFilter VFS modules
+## Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+##
+## This 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 3 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/>.
+##
+
+set -u
+
+pdie() { echo "$0: ERROR: ${1-}" 1>&2; exit "${2-1}"; }
+
+## ======================================================================
+
+sendmail="${VIRUSFILTER_NOTIFY_SENDMAIL_COMMAND:-/usr/sbin/sendmail}"
+sendmail_opts="${VIRUSFILTER_NOTIFY_SENDMAIL_OPTIONS:-}"
+
+smbclient="${VIRUSFILTER_NOTIFY_SMBCLIENT_COMMAND:- at SAMBA_BINDIR@/smbclient}"
+smbclient_opts="${VIRUSFILTER_NOTIFY_SMBCLIENT_OPTIONS:-}"
+
+## ======================================================================
+
+if [ -n "${VIRUSFILTER_RESULT_IS_CACHE-}" ]; then
+  ## Result is cache. Ignore!
+  exit 0
+fi
+
+if [ ! -t 1 ] && [ -z "${VIRUSFILTER_NOTIFY_BG-}" ]; then
+  export VIRUSFILTER_NOTIFY_BG=1
+  "$0" ${1+"$@"} </dev/null >/dev/null &
+  exit 0
+fi
+
+## ----------------------------------------------------------------------
+
+if [ -n "${VIRUSFILTER_INFECTED_FILE_ACTION-}" ]; then
+  report="$VIRUSFILTER_INFECTED_FILE_REPORT"
+else
+  report="$VIRUSFILTER_SCAN_ERROR_REPORT"
+fi
+
+if [ X"$VIRUSFILTER_SERVER_NAME" != X"$VIRUSFILTER_SERVER_IP" ]; then
+  server_name="$VIRUSFILTER_SERVER_NAME"
+else
+  server_name="$VIRUSFILTER_SERVER_NETBIOS_NAME"
+fi
+
+if [ X"$VIRUSFILTER_CLIENT_NAME" != X"$VIRUSFILTER_CLIENT_IP" ]; then
+  client_name="$VIRUSFILTER_CLIENT_NAME"
+else
+  client_name="$VIRUSFILTER_CLIENT_NETBIOS_NAME"
+fi
+
+mail_to=""
+winpopup_to=""
+subject_prefix=""
+sender=""
+from=""
+cc=""
+bcc=""
+content_type="text/plain"
+content_encoding="UTF-8"
+
+cmd_usage="Usage: $0 [OPTIONS]
+
+Options:
+  --mail-to ADDRESS
+    Send a notice message to this e-mail address(es)
+  --winpopup-to NAME
+    Send a \"WinPopup\" message to this NetBIOS name
+  --sender ADDRESS
+    Envelope sender address for mail
+  --from ADDRESS
+    From: e-mail address for mail
+  --cc ADDRESS
+    Cc: e-mail address(es) for mail
+  --bcc ADDRESS
+    Bcc: e-mail address(es) for mail
+  --subject-prefix PREFIX
+    Subject: prefix string for mail
+  --content-type TYPE
+  --content-encoding ENCODING
+    Content-Type: TYPE; charset=\"ENCODING\" for mail [$content_type; charset=\"$content_encoding\"]
+  --header-file FILE
+    Prepend the content of FILE to the message
+  --footer-file FILE
+    Append the content of FILE to the message
+"
+
+## ----------------------------------------------------------------------
+
+getopts_want_arg()
+{
+  if [ "$#" -lt 2 ]; then
+    pdie "Option requires an argument: $1"
+  fi
+  if [ "$#" -ge 3 ]; then
+    if expr x"$2" : x"$3\$" >/dev/null; then
+      : OK
+    else
+      pdie "Invalid value for option: $1 $2"
+    fi
+  fi
+}
+
+while [ "$#" -gt 0 ]; do
+  OPT="$1"; shift
+  case "$OPT" in
+  --help)
+    echo "$cmd_usage"
+    exit 0
+    ;;
+  --mail-to)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    mail_to="${mail_to:+$mail_to, }$1"; shift
+    ;;
+  --winpopup-to)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    winpopup_to="$1"; shift
+    ;;
+  --sender)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    sender="$1"; shift
+    ;;
+  --from)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    from="$1"; shift
+    ;;
+  --cc)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    cc="${cc:+$cc, }$1"; shift
+    ;;
+  --bcc)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    bcc="${bcc:+$bcc, }$1"; shift
+    ;;
+  --subject-prefix)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    subject_prefix="$1"; shift
+    ;;
+  --content-type)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    content_type="$1"; shift
+    ;;
+  --content-encoding)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    content_encoding="$1"; shift
+    ;;
+  --header-file)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    header_file="$1"; shift
+    ;;
+  --footer-file)
+    getopts_want_arg "$OPT" ${1+"$1"}
+    footer_file="$1"; shift
+    ;;
+  --)
+    break
+    ;;
+  -*)
+    pdie "Invalid option: $OPT"
+    ;;
+  *)
+    set -- "$OPT" ${1+"$@"}
+    break
+    ;;
+  esac
+done
+
+[ -z "$sender" ] && sender="$from"
+subject="$subject_prefix$report"
+
+## ======================================================================
+
+msg_header="\
+Subject: $subject
+Content-Type: $content_type; charset=$content_encoding
+X-VIRUSFILTER-Version: $VIRUSFILTER_VERSION
+X-VIRUSFILTER-Module-Name: $VIRUSFILTER_MODULE_NAME
+"
+
+if [ -n "${VIRUSFILTER_MODULE_VERSION-}" ]; then
+  msg_header="${msg_header}\
+X-VIRUSFILTER-Module-Version: $VIRUSFILTER_MODULE_VERSION
+"
+fi
+
+if [ -n "${from-}" ]; then
+  msg_header="${msg_header}\
+From: $from
+"
+fi
+
+if [ -n "${mail_to-}" ]; then
+  msg_header="${msg_header}\
+To: $mail_to
+"
+fi
+
+if [ -n "${cc-}" ]; then
+  msg_header="${msg_header}\
+Cc: $cc
+"
+fi
+
+if [ -n "${bcc-}" ]; then
+  msg_header="${msg_header}\
+Bcc: $bcc
+"
+fi
+
+## ----------------------------------------------------------------------
+
+msg_body=""
+
+if [ -n "${header_file-}" ] && [ -f "$header_file" ]; then
+  msg_body="${msg_body}\
+`cat "$header_file"`
+"
+fi
+
+msg_body="${msg_body}\
+Server: $server_name ($VIRUSFILTER_SERVER_IP)
+Server PID: $VIRUSFILTER_SERVER_PID
+Service name: $VIRUSFILTER_SERVICE_NAME
+Service path: $VIRUSFILTER_SERVICE_PATH
+Client: $client_name ($VIRUSFILTER_CLIENT_IP)
+User: $VIRUSFILTER_USER_DOMAIN\\$VIRUSFILTER_USER_NAME
+"
+
+if [ -n "${VIRUSFILTER_INFECTED_FILE_ACTION-}" ]; then
+  msg_body="${msg_body}\
+Infected file report: $VIRUSFILTER_INFECTED_FILE_REPORT
+"
+  msg_body="${msg_body}\
+Infected file path: $VIRUSFILTER_SERVICE_PATH/$VIRUSFILTER_INFECTED_SERVICE_FILE_PATH
+"
+  msg_body="${msg_body}\
+Infected file action: $VIRUSFILTER_INFECTED_FILE_ACTION
+"
+else
+  msg_body="${msg_body}\
+Scan error report: $VIRUSFILTER_SCAN_ERROR_REPORT
+Scan error file path: $VIRUSFILTER_SERVICE_PATH/$VIRUSFILTER_SCAN_ERROR_SERVICE_FILE_PATH
+"
+fi
+
+if [ -n "${VIRUSFILTER_QUARANTINED_FILE_PATH-}" ]; then
+  msg_body="${msg_body}\
+Quarantined/Renamed file path: ${VIRUSFILTER_QUARANTINED_FILE_PATH-}
+"
+fi
+
+if [ -n "${footer_file-}" ] && [ -f "$footer_file" ]; then
+  msg_body="${msg_body}\
+`cat "$footer_file"`
+"
+fi
+
+## ======================================================================
+
+if [ -n "$mail_to" ]; then
+  (echo "$msg_header"; echo "$msg_body") \
+    |"$sendmail" -t -i ${sender:+-f "$sender"} $sendmail_opts
+fi
+
+if [ -n "$winpopup_to" ]; then
+  echo "$msg_body" \
+    |"$smbclient" -M "$winpopup_to" -U% $smbclient_opts \
+    >/dev/null
+fi
+
+exit 0
diff --git a/source3/modules/vfs_virusfilter.c b/source3/modules/vfs_virusfilter.c
new file mode 100644
index 00000000000..eb8ac26665f
--- /dev/null
+++ b/source3/modules/vfs_virusfilter.c
@@ -0,0 +1,1454 @@
+/*
+ * Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+ * Copyright (C) 2016-2017 Trever L. Adams
+ * Copyright (C) 2017 Ralph Boehme <slow at samba.org>
+ * Copyright (C) 2017 Jeremy Allison <jra at samba.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 3 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/>.
+ */
+
+#include "vfs_virusfilter_common.h"
+#include "vfs_virusfilter_utils.h"
+
+/*
+ * Default configuration values
+ * ======================================================================
+ */
+
+#define VIRUSFILTER_DEFAULT_QUARANTINE_PREFIX		"virusfilter."
+#define VIRUSFILTER_DEFAULT_QUARANTINE_SUFFIX		".infected"
+#define VIRUSFILTER_DEFAULT_RENAME_PREFIX		"virusfilter."
+#define VIRUSFILTER_DEFAULT_RENAME_SUFFIX		".infected"
+
+/* ====================================================================== */
+
+enum virusfilter_scanner_enum {
+	VIRUSFILTER_SCANNER_CLAMAV,
+	VIRUSFILTER_SCANNER_FSAV,
+	VIRUSFILTER_SCANNER_SOPHOS
+};
+
+static const struct enum_list scanner_list[] = {
+	{ VIRUSFILTER_SCANNER_CLAMAV,	"clamav" },
+	{ VIRUSFILTER_SCANNER_FSAV,	"fsav" },
+	{ VIRUSFILTER_SCANNER_SOPHOS,	"sophos" },
+	{ -1,				NULL }
+};
+
+static const struct enum_list virusfilter_actions[] = {
+	{ VIRUSFILTER_ACTION_QUARANTINE,	"quarantine" },
+	{ VIRUSFILTER_ACTION_RENAME,		"rename" },
+	{ VIRUSFILTER_ACTION_DELETE,		"delete" },
+
+	/* alias for "delete" */
+	{ VIRUSFILTER_ACTION_DELETE,		"remove" },
+
+	/* alias for "delete" */
+	{ VIRUSFILTER_ACTION_DELETE,		"unlink" },
+	{ VIRUSFILTER_ACTION_DO_NOTHING,	"nothing" },
+	{ -1,					NULL}
+};
+
+static int virusfilter_config_destructor(struct virusfilter_config *config)
+{
+	TALLOC_FREE(config->backend);
+	return 0;
+}
+
+/*
+ * This is adapted from vfs_recycle module.
+ * Caller must have become_root();
+ */
+static bool quarantine_directory_exist(
+	struct vfs_handle_struct *handle,
+	const char *dname)
+{
+	int ret = -1;
+	struct smb_filename smb_fname = {
+		.base_name = discard_const_p(char, dname)
+	};
+
+	ret = SMB_VFS_STAT(handle->conn, &smb_fname);
+	if (ret == 0) {
+		return S_ISDIR(smb_fname.st.st_ex_mode);
+	}
+
+	return false;
+}
+
+/**
+ * Create directory tree
+ * @param conn connection
+ * @param dname Directory tree to be created
+ * @return Returns true for success
+ * This is adapted from vfs_recycle module.
+ * Caller must have become_root();
+ */
+static bool quarantine_create_dir(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const char *dname)
+{
+	size_t len = 0;
+	size_t cat_len = 0;
+	char *new_dir = NULL;
+	char *tmp_str = NULL;
+	char *token = NULL;
+	char *tok_str = NULL;
+	bool status = false;
+	bool ok = false;
+	int ret = -1;
+	char *saveptr = NULL;
+
+	tmp_str = talloc_strdup(talloc_tos(), dname);
+	if (tmp_str == NULL) {
+		DBG_ERR("virusfilter-vfs: out of memory!\n");
+		errno = ENOMEM;
+		goto done;
+	}
+	tok_str = tmp_str;
+
+	len = strlen(dname)+1;
+	new_dir = (char *)talloc_size(talloc_tos(), len + 1);
+	if (new_dir == NULL) {
+		DBG_ERR("virusfilter-vfs: out of memory!\n");
+		errno = ENOMEM;
+		goto done;
+	}
+	*new_dir = '\0';
+	if (dname[0] == '/') {
+		/* Absolute path. */
+		cat_len = strlcat(new_dir, "/", len + 1);
+		if (cat_len >= len+1) {
+			goto done;
+		}
+	}
+
+	/* Create directory tree if neccessary */
+	for (token = strtok_r(tok_str, "/", &saveptr);
+	     token != NULL;
+	     token = strtok_r(NULL, "/", &saveptr))
+	{
+		cat_len = strlcat(new_dir, token, len + 1);
+		if (cat_len >= len+1) {
+			goto done;
+		}
+		ok = quarantine_directory_exist(handle, new_dir);
+		if (ok == true) {
+			DBG_DEBUG("quarantine: dir %s already exists\n",
+				  new_dir);
+		} else {
+			struct smb_filename *smb_fname = NULL;
+
+			DBG_INFO("quarantine: creating new dir %s\n", new_dir);
+
+			smb_fname = synthetic_smb_fname(talloc_tos(), new_dir,
+							NULL, NULL, 0);
+			if (smb_fname == NULL) {
+				goto done;
+			}
+
+			ret = SMB_VFS_NEXT_MKDIR(handle,
+					smb_fname,
+					config->quarantine_dir_mode);
+			if (ret != 0) {
+				TALLOC_FREE(smb_fname);
+
+				DBG_WARNING("quarantine: mkdir failed for %s "
+					    "with error: %s\n", new_dir,
+					    strerror(errno));
+				status = false;
+				goto done;
+			}
+			TALLOC_FREE(smb_fname);
+		}
+		cat_len = strlcat(new_dir, "/", len + 1);
+		if (cat_len >= len + 1) {
+			goto done;
+		}
+	}
+
+	status = true;
+done:
+	TALLOC_FREE(tmp_str);
+	TALLOC_FREE(new_dir);
+	return status;
+}
+
+static int virusfilter_vfs_connect(
+	struct vfs_handle_struct *handle,
+	const char *svc,
+	const char *user)
+{
+	int snum = SNUM(handle->conn);
+	struct virusfilter_config *config = NULL;
+	const char *exclude_files = NULL;
+	const char *temp_quarantine_dir_mode = NULL;
+	char *sret = NULL;
+	char *tmp = NULL;
+	enum virusfilter_scanner_enum backend;
+	int connect_timeout = 0;
+	int io_timeout = 0;
+	int ret = -1;
+
+	config = talloc_zero(handle, struct virusfilter_config);
+	if (config == NULL) {
+		DBG_ERR("talloc_zero failed\n");
+		return -1;
+	}
+	talloc_set_destructor(config, virusfilter_config_destructor);
+
+	SMB_VFS_HANDLE_SET_DATA(handle, config, NULL,
+				struct virusfilter_config, return -1);
+
+	config->scan_request_limit = lp_parm_int(
+		snum, "virusfilter", "scan request limit", 0);
+
+	config->scan_on_open = lp_parm_bool(
+		snum, "virusfilter", "scan on open", true);
+
+	config->scan_on_close = lp_parm_bool(
+		snum, "virusfilter", "scan on close", false);
+
+	config->max_nested_scan_archive = lp_parm_int(
+		snum, "virusfilter", "max nested scan archive", 1);
+
+	config->scan_archive = lp_parm_bool(
+		snum, "virusfilter", "scan archive", false);
+
+	config->scan_mime = lp_parm_bool(
+		snum, "virusfilter", "scan mime", false);
+
+	config->max_file_size = (ssize_t)lp_parm_ulong(
+		snum, "virusfilter", "max file size", 100000000L);
+
+	config->min_file_size = (ssize_t)lp_parm_ulong(
+		snum, "virusfilter", "min file size", 10);
+
+	exclude_files = lp_parm_const_string(
+		snum, "virusfilter", "exclude files", NULL);
+	if (exclude_files != NULL) {
+		set_namearray(&config->exclude_files, exclude_files);
+	}
+
+	config->cache_entry_limit = lp_parm_int(
+		snum, "virusfilter", "cache entry limit", 100);
+
+	config->cache_time_limit = lp_parm_int(
+		snum, "virusfilter", "cache time limit", 10);
+
+	config->infected_file_action = lp_parm_enum(
+		snum, "virusfilter", "infected file action",
+		virusfilter_actions, VIRUSFILTER_ACTION_DO_NOTHING);
+
+	config->infected_file_command = lp_parm_const_string(
+		snum, "virusfilter", "infected file command", NULL);
+
+	config->scan_error_command = lp_parm_const_string(
+		snum, "virusfilter", "scan error command", NULL);
+
+	config->block_access_on_error = lp_parm_bool(
+		snum, "virusfilter", "block access on error", false);
+
+	tmp = talloc_asprintf(config, "%s/.quarantine",
+		handle->conn->connectpath);
+
+	config->quarantine_dir = lp_parm_const_string(
+		snum, "virusfilter", "quarantine directory",
+		tmp ? tmp : "/tmp/.quarantine");
+
+	if (tmp != config->quarantine_dir) {
+		TALLOC_FREE(tmp);
+	}
+
+	temp_quarantine_dir_mode = lp_parm_const_string(
+		snum, "virusfilter", "quarantine directory mode", "0755");
+	if (temp_quarantine_dir_mode != NULL) {
+		sscanf(temp_quarantine_dir_mode, "%o",
+		       &config->quarantine_dir_mode);
+	}
+
+	config->quarantine_prefix = lp_parm_const_string(
+		snum, "virusfilter", "quarantine prefix",
+		VIRUSFILTER_DEFAULT_QUARANTINE_PREFIX);
+
+	config->quarantine_suffix = lp_parm_const_string(
+		snum, "virusfilter", "quarantine suffix",
+		VIRUSFILTER_DEFAULT_QUARANTINE_SUFFIX);
+
+	/*
+	 * Make sure prefixes and suffixes do not contain directory
+	 * delimiters
+	 */
+	sret = strstr(config->quarantine_prefix, "/");
+	if (sret != NULL) {
+		DBG_ERR("quarantine prefix must not contain directory "
+			"delimiter(s) such as '/' (%s replaced with %s)\n",
+			config->quarantine_prefix,
+			VIRUSFILTER_DEFAULT_QUARANTINE_PREFIX);
+		config->quarantine_prefix =
+			VIRUSFILTER_DEFAULT_QUARANTINE_PREFIX;
+	}
+	sret = strstr(config->quarantine_suffix, "/");
+	if (sret != NULL) {
+		DBG_ERR("quarantine suffix must not contain directory "
+			"delimiter(s) such as '/' (%s replaced with %s)\n",
+			config->quarantine_suffix,
+			VIRUSFILTER_DEFAULT_QUARANTINE_SUFFIX);
+		config->quarantine_suffix =
+			VIRUSFILTER_DEFAULT_QUARANTINE_SUFFIX;
+	}
+
+	config->quarantine_keep_tree = lp_parm_bool(
+		snum, "virusfilter", "quarantine keep tree", true);
+
+	config->quarantine_keep_name = lp_parm_bool(
+		snum, "virusfilter", "quarantine keep name", true);
+
+	config->rename_prefix = lp_parm_const_string(
+		snum, "virusfilter", "rename prefix",
+		VIRUSFILTER_DEFAULT_RENAME_PREFIX);
+
+	config->rename_suffix = lp_parm_const_string(
+		snum, "virusfilter", "rename suffix",
+		VIRUSFILTER_DEFAULT_RENAME_SUFFIX);
+
+	/*
+	 * Make sure prefixes and suffixes do not contain directory
+	 * delimiters
+	 */
+	sret = strstr(config->rename_prefix, "/");
+	if (sret != NULL) {
+		DBG_ERR("rename prefix must not contain directory "
+			"delimiter(s) such as '/' (%s replaced with %s)\n",
+			config->rename_prefix,
+			VIRUSFILTER_DEFAULT_RENAME_PREFIX);
+		config->rename_prefix =
+			VIRUSFILTER_DEFAULT_RENAME_PREFIX;
+	}
+	sret = strstr(config->rename_suffix, "/");
+	if (sret != NULL) {
+		DBG_ERR("rename suffix must not contain directory "
+			"delimiter(s) such as '/' (%s replaced with %s)\n",
+			config->rename_suffix,
+			VIRUSFILTER_DEFAULT_RENAME_SUFFIX);
+		config->rename_suffix =
+			VIRUSFILTER_DEFAULT_RENAME_SUFFIX;
+	}
+
+	config->infected_open_errno = lp_parm_int(
+		snum, "virusfilter", "infected file errno on open", EACCES);
+
+	config->infected_close_errno = lp_parm_int(
+		snum, "virusfilter", "infected file errno on close", 0);
+
+	config->scan_error_open_errno = lp_parm_int(
+		snum, "virusfilter", "scan error errno on open", EACCES);
+
+	config->scan_error_close_errno = lp_parm_int(
+		snum, "virusfilter", "scan error errno on close", 0);
+
+	config->socket_path = lp_parm_const_string(
+		snum, "virusfilter", "socket path", NULL);
+
+	/* canonicalize socket_path */
+	if (config->socket_path != NULL && config->socket_path[0] != '/') {
+		DBG_ERR("socket path must be an absolute path. "
+			"Using backend default\n");
+		config->socket_path = NULL;
+        }
+	if (config->socket_path != NULL) {
+		canonicalize_absolute_path(handle,
+					   config->socket_path);
+	}
+
+	connect_timeout = lp_parm_int(snum, "virusfilter",
+				      "connect timeout", 30000);
+
+	io_timeout = lp_parm_int(snum, "virusfilter", "io timeout", 60000);
+
+	config->io_h = virusfilter_io_new(config, connect_timeout, io_timeout);
+	if (config->io_h == NULL) {
+		DBG_ERR("virusfilter_io_new failed");
+		return -1;
+	}
+
+	if (config->cache_entry_limit > 0) {
+		config->cache = virusfilter_cache_new(handle,
+					config->cache_entry_limit,
+					config->cache_time_limit);
+		if (config->cache == NULL) {
+			DBG_ERR("Initializing cache failed: Cache disabled\n");
+			return -1;
+		}
+	}
+
+	/*
+	 * Check quarantine directory now to save processing
+	 * and becoming root over and over.
+	 */
+	if (config->infected_file_action == VIRUSFILTER_ACTION_QUARANTINE) {
+		bool ok = false;
+		bool dir_exists;
+
+		/*
+		 * Do SMB_VFS_NEXT_MKDIR(config->quarantine_dir)
+		 * hierarchy
+		 */
+		become_root();
+		dir_exists = quarantine_directory_exist(handle,
+						config->quarantine_dir);
+		if (!dir_exists) {
+			DBG_DEBUG("Creating quarantine directory: %s\n",
+				  config->quarantine_dir);
+			ok = quarantine_create_dir(handle, config,
+					      config->quarantine_dir);
+		}
+		unbecome_root();
+		if (ok == false) {
+			DBG_ERR("Creating quarantine directory %s "
+				"failed with %s\n",
+				config->quarantine_dir,
+				strerror(errno));
+			return -1;
+		}
+	}
+
+	/*
+	 * Now that the frontend options are initialized, load the configured
+	 * backend.
+	 */
+
+	backend = (enum virusfilter_scanner_enum)lp_parm_enum(snum,
+				"virusfilter",
+				"scanner",
+				scanner_list,
+			       -1);
+	if (backend == (enum virusfilter_scanner_enum)-1) {
+		DBG_ERR("No AV-Scanner configured, "
+			"please set \"virusfilter:scanner\"\n");
+		return -1;
+	}
+
+	/* This goes away as soon as the next commit adds an actual backend... */
+	if (config->backend == NULL) {
+		DBG_INFO("Not implemented\n");
+		return SMB_VFS_NEXT_CONNECT(handle, svc, user);
+	}
+
+	if (config->backend->fns->connect != NULL) {
+		ret = config->backend->fns->connect(handle, config, svc, user);
+		if (ret == -1) {
+			return -1;
+		}
+	}
+
+	return SMB_VFS_NEXT_CONNECT(handle, svc, user);
+}
+
+static void virusfilter_vfs_disconnect(struct vfs_handle_struct *handle)
+{
+	struct virusfilter_config *config = NULL;
+
+	SMB_VFS_HANDLE_GET_DATA(handle, config,
+				struct virusfilter_config, return);
+
+	if (config->backend->fns->disconnect != NULL) {
+		config->backend->fns->disconnect(handle);
+	}
+
+	free_namearray(config->exclude_files);
+	virusfilter_io_disconnect(config->io_h);
+
+	SMB_VFS_NEXT_DISCONNECT(handle);
+}
+
+static int virusfilter_set_module_env(TALLOC_CTX *mem_ctx,
+				      struct virusfilter_config *config,
+				      char **env_list)
+{
+	int ret;
+
+	ret = virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_VERSION",
+				  VIRUSFILTER_VERSION);
+	if (ret == -1) {
+		return -1;
+	}
+	ret = virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_MODULE_NAME",
+				  config->backend->name);
+	if (ret == -1) {
+		return -1;
+	}
+
+	if (config->backend->version != 0) {
+		char *version = NULL;
+
+		version = talloc_asprintf(talloc_tos(), "%u",
+					  config->backend->version);
+		if (version == NULL) {
+			return -1;
+		}
+		ret = virusfilter_env_set(mem_ctx, env_list,
+					  "VIRUSFILTER_MODULE_VERSION",
+					  version);
+		TALLOC_FREE(version);
+		if (ret == -1) {
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+static virusfilter_action virusfilter_do_infected_file_action(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp,
+	const char **filepath_newp)
+{
+	TALLOC_CTX *mem_ctx = talloc_tos();
+	connection_struct *conn = handle->conn;
+	char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	char *fname = fsp->fsp_name->base_name;
+	const struct smb_filename *smb_fname = fsp->fsp_name;
+	struct smb_filename *q_smb_fname = NULL;
+	char *q_dir = NULL;
+	char *q_prefix = NULL;
+	char *q_suffix = NULL;
+	char *q_filepath = NULL;
+	char *dir_name = NULL;
+	char *temp_path = NULL;
+	const char *base_name = NULL;
+	char *rand_filename_component = NULL;
+	bool ok = false;
+	int ret = -1;
+	int saved_errno = 0;
+
+	*filepath_newp = NULL;
+
+	switch (config->infected_file_action) {
+	case VIRUSFILTER_ACTION_RENAME:
+		q_prefix = virusfilter_string_sub(mem_ctx, conn,
+						config->rename_prefix);
+		q_suffix = virusfilter_string_sub(mem_ctx, conn,
+						config->rename_suffix);
+		if (q_prefix == NULL || q_suffix == NULL) {
+			DBG_ERR("Rename failed: %s/%s: Cannot allocate "
+				"memory\n", cwd_fname, fname);
+			TALLOC_FREE(q_prefix);
+			TALLOC_FREE(q_suffix);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		ok = parent_dirname(mem_ctx, fname, &q_dir, &base_name);
+		if (!ok) {
+			DBG_ERR("Rename failed: %s/%s: Cannot allocate "
+				"memory\n", cwd_fname, fname);
+			TALLOC_FREE(q_prefix);
+			TALLOC_FREE(q_suffix);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		if (q_dir == NULL) {
+			DBG_ERR("Rename failed: %s/%s: Cannot allocate "
+				"memory\n", cwd_fname, fname);
+			TALLOC_FREE(q_prefix);
+			TALLOC_FREE(q_suffix);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		q_filepath = talloc_asprintf(talloc_tos(), "%s/%s%s%s", q_dir,
+					     q_prefix, base_name, q_suffix);
+
+		TALLOC_FREE(q_dir);
+		TALLOC_FREE(q_prefix);
+		TALLOC_FREE(q_suffix);
+
+		q_smb_fname = synthetic_smb_fname(mem_ctx, q_filepath,
+						  smb_fname->stream_name, NULL,
+						  smb_fname->flags);
+		if (q_smb_fname == NULL) {
+			TALLOC_FREE(q_filepath);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		become_root();
+		ret = virusfilter_vfs_next_move(handle, smb_fname,
+						   q_smb_fname);
+		if (ret == -1) {
+			saved_errno = errno;
+		}
+		unbecome_root();
+
+		if (ret == -1) {
+			DBG_ERR("Rename failed: %s/%s: Rename failed: %s\n",
+				cwd_fname, fname,
+				strerror(saved_errno));
+			errno = saved_errno;
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		*filepath_newp = q_filepath;
+
+		return VIRUSFILTER_ACTION_RENAME;
+
+	case VIRUSFILTER_ACTION_QUARANTINE:
+		q_dir = virusfilter_string_sub(mem_ctx, conn,
+					config->quarantine_dir);
+		q_prefix = virusfilter_string_sub(mem_ctx, conn,
+					config->quarantine_prefix);
+		q_suffix = virusfilter_string_sub(mem_ctx, conn,
+					config->quarantine_suffix);
+		if (q_dir == NULL || q_prefix == NULL || q_suffix == NULL) {
+			DBG_ERR("Quarantine failed: %s/%s: Cannot allocate "
+				"memory\n", cwd_fname, fname);
+			TALLOC_FREE(q_dir);
+			TALLOC_FREE(q_prefix);
+			TALLOC_FREE(q_suffix);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		if (config->quarantine_keep_name ||
+		    config->quarantine_keep_tree)
+		{
+			ok = parent_dirname(mem_ctx, smb_fname->base_name,
+					    &dir_name, &base_name);
+			if (!ok) {
+				DBG_ERR("Quarantine failed: %s/%s: Cannot "
+					"allocate memory\n", cwd_fname, fname);
+				TALLOC_FREE(q_dir);
+				TALLOC_FREE(q_prefix);
+				TALLOC_FREE(q_suffix);
+				TALLOC_FREE(rand_filename_component);
+				return VIRUSFILTER_ACTION_DO_NOTHING;
+			}
+
+			if (config->quarantine_keep_tree) {
+				temp_path = talloc_asprintf(mem_ctx,
+						"%s/%s", q_dir, cwd_fname);
+				if (temp_path == NULL) {
+					DBG_ERR("Quarantine failed: %s/%s: "
+						"Cannot allocate memory\n",
+						cwd_fname, fname);
+					TALLOC_FREE(q_dir);
+					TALLOC_FREE(q_prefix);
+					TALLOC_FREE(q_suffix);
+					TALLOC_FREE(rand_filename_component);
+					return VIRUSFILTER_ACTION_DO_NOTHING;
+				}
+
+				become_root();
+				ok = quarantine_directory_exist(handle,
+								temp_path);
+				if (ok) {
+					DBG_DEBUG("quarantine: Directory "
+						  "already exists\n");
+					TALLOC_FREE(q_dir);
+					q_dir = temp_path;
+				} else {
+					DBG_DEBUG("quarantine: Creating "
+					      "directory %s\n", temp_path);
+					ok = quarantine_create_dir(handle,
+							config, temp_path);
+					if (!ok) {
+						DBG_NOTICE("quarantine: Could "
+							"not create directory "
+							"ignoring for %s...\n",
+							smb_fname_str_dbg(
+								smb_fname));
+						TALLOC_FREE(temp_path);
+					} else {
+						TALLOC_FREE(q_dir);
+						q_dir = temp_path;
+					}
+				}
+				unbecome_root();
+			}
+		}
+
+		/* Get a 16 byte + \0 random filename component. */
+		rand_filename_component = generate_random_str(talloc_tos(), 16);
+		if (rand_filename_component == NULL) {
+			DBG_ERR("Quarantine failed: %s/%s: Cannot "
+				"allocate memory\n", cwd_fname, fname);
+			TALLOC_FREE(dir_name);
+			TALLOC_FREE(q_dir);
+			TALLOC_FREE(q_prefix);
+			TALLOC_FREE(q_suffix);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+		if (config->quarantine_keep_name) {
+			q_filepath = talloc_asprintf(talloc_tos(),
+					"%s/%s%s%s-%s",
+					q_dir, q_prefix, base_name, q_suffix,
+					rand_filename_component);
+		} else {
+			q_filepath = talloc_asprintf(talloc_tos(),
+				"%s/%s%s", q_dir, q_prefix,
+				rand_filename_component);
+		}
+
+		TALLOC_FREE(dir_name);
+		TALLOC_FREE(q_dir);
+		TALLOC_FREE(q_prefix);
+		TALLOC_FREE(q_suffix);
+		TALLOC_FREE(rand_filename_component);
+
+		if (q_filepath == NULL) {
+			DBG_ERR("Quarantine failed: %s/%s: Cannot allocate "
+				"memory\n", cwd_fname, fname);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		q_smb_fname = synthetic_smb_fname(mem_ctx, q_filepath,
+			smb_fname->stream_name, NULL, smb_fname->flags);
+		if (q_smb_fname == NULL) {
+			TALLOC_FREE(q_filepath);
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		become_root();
+		ret = virusfilter_vfs_next_move(handle, smb_fname,
+						   q_smb_fname);
+		if (ret == -1) {
+			saved_errno = errno;
+		}
+		unbecome_root();
+		if (ret == -1) {
+			DBG_ERR("Quarantine failed: %s/%s: Rename to %s "
+				"failed: %s\n",
+				cwd_fname, fname,
+				q_filepath,
+				strerror(saved_errno));
+			errno = saved_errno;
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+
+		*filepath_newp = q_filepath;
+
+		return VIRUSFILTER_ACTION_QUARANTINE;
+
+	case VIRUSFILTER_ACTION_DELETE:
+		become_root();
+		ret = SMB_VFS_NEXT_UNLINK(handle, smb_fname);
+		if (ret == -1) {
+			saved_errno = errno;
+		}
+		unbecome_root();
+		if (ret == -1) {
+			DBG_ERR("Delete failed: %s/%s: Unlink failed: %s\n",
+				cwd_fname, fname,
+				strerror(saved_errno));
+			errno = saved_errno;
+			return VIRUSFILTER_ACTION_DO_NOTHING;
+		}
+		return VIRUSFILTER_ACTION_DELETE;
+
+	case VIRUSFILTER_ACTION_DO_NOTHING:
+	default:
+		return VIRUSFILTER_ACTION_DO_NOTHING;
+	}
+}
+
+static virusfilter_action virusfilter_treat_infected_file(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp,
+	const char *report,
+	bool is_cache)
+{
+	connection_struct *conn = handle->conn;
+	char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	char *fname = fsp->fsp_name->base_name;
+	TALLOC_CTX *mem_ctx = talloc_tos();
+	int i;
+	virusfilter_action action;
+	const char *action_name = "UNKNOWN";
+	const char *filepath_q = NULL;
+	char *env_list = NULL;
+	char *command = NULL;
+	int command_result;
+	int ret;
+
+	action = virusfilter_do_infected_file_action(handle, config, fsp,
+						     &filepath_q);
+	for (i=0; virusfilter_actions[i].name; i++) {
+		if (virusfilter_actions[i].value == action) {
+			action_name = virusfilter_actions[i].name;
+			break;
+		}
+	}
+	DBG_WARNING("Infected file action: %s/%s: %s\n", cwd_fname,
+		    fname, action_name);
+
+	if (!config->infected_file_command) {
+		return action;
+	}
+
+	ret = virusfilter_set_module_env(mem_ctx, config, &env_list);
+	if (ret == -1) {
+		goto done;
+	}
+	ret = virusfilter_env_set(mem_ctx, &env_list,
+				  "VIRUSFILTER_INFECTED_SERVICE_FILE_PATH",
+				  fname);
+	if (ret == -1) {
+		goto done;
+	}
+	if (report != NULL) {
+		ret = virusfilter_env_set(mem_ctx, &env_list,
+					  "VIRUSFILTER_INFECTED_FILE_REPORT",
+					  report);
+		if (ret == -1) {
+			goto done;
+		}
+	}
+	ret = virusfilter_env_set(mem_ctx, &env_list,
+				  "VIRUSFILTER_INFECTED_FILE_ACTION",
+				  action_name);
+	if (ret == -1) {
+		goto done;
+	}
+	if (filepath_q != NULL) {
+		ret = virusfilter_env_set(mem_ctx, &env_list,
+					  "VIRUSFILTER_QUARANTINED_FILE_PATH",
+					  filepath_q);
+		if (ret == -1) {
+			goto done;
+		}
+	}
+	if (is_cache) {
+		ret = virusfilter_env_set(mem_ctx, &env_list,
+					  "VIRUSFILTER_RESULT_IS_CACHE", "yes");
+		if (ret == -1) {
+			goto done;
+		}
+	}
+
+	command = virusfilter_string_sub(mem_ctx, conn,
+					 config->infected_file_command);
+	if (command == NULL) {
+		DBG_ERR("virusfilter_string_sub failed\n");
+		goto done;
+	}
+
+	DBG_NOTICE("Infected file command line: %s/%s: %s\n", cwd_fname,
+		   fname, command);
+
+	command_result = virusfilter_shell_run(mem_ctx, command, &env_list,
+					       conn, true);
+	if (command_result != 0) {
+		DBG_ERR("Infected file command failed: %d\n", command_result);
+	}
+
+	DBG_DEBUG("Infected file command finished: %d\n", command_result);
+
+done:
+	TALLOC_FREE(env_list);
+	TALLOC_FREE(command);
+
+	return action;
+}
+
+static void virusfilter_treat_scan_error(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp,
+	const char *report,
+	bool is_cache)
+{
+	connection_struct *conn = handle->conn;
+	const char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	const char *fname = fsp->fsp_name->base_name;
+	TALLOC_CTX *mem_ctx = talloc_tos();
+	char *env_list = NULL;
+	char *command = NULL;
+	int command_result;
+	int ret;
+
+	if (!config->scan_error_command) {
+		return;
+	}
+	ret = virusfilter_set_module_env(mem_ctx, config, &env_list);
+	if (ret == -1) {
+		goto done;
+	}
+	ret = virusfilter_env_set(mem_ctx, &env_list,
+				  "VIRUSFILTER_SCAN_ERROR_SERVICE_FILE_PATH",
+				  fname);
+	if (ret == -1) {
+		goto done;
+	}
+	if (report != NULL) {
+		ret = virusfilter_env_set(mem_ctx, &env_list,
+					  "VIRUSFILTER_SCAN_ERROR_REPORT",
+					  report);
+		if (ret == -1) {
+			goto done;
+		}
+	}
+	if (is_cache) {
+		ret = virusfilter_env_set(mem_ctx, &env_list,
+					  "VIRUSFILTER_RESULT_IS_CACHE", "1");
+		if (ret == -1) {
+			goto done;
+		}
+	}
+
+	command = virusfilter_string_sub(mem_ctx, conn,
+					 config->scan_error_command);
+	if (command == NULL) {
+		DBG_ERR("virusfilter_string_sub failed\n");
+		goto done;
+	}
+
+	DBG_NOTICE("Scan error command line: %s/%s: %s\n", cwd_fname,
+		   fname, command);
+
+	command_result = virusfilter_shell_run(mem_ctx, command, &env_list,
+					       conn, true);
+	if (command_result != 0) {
+		DBG_ERR("Scan error command failed: %d\n", command_result);
+	}
+
+done:
+	TALLOC_FREE(env_list);
+	TALLOC_FREE(command);
+}
+
+static virusfilter_result virusfilter_scan(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp)
+{
+	virusfilter_result scan_result;
+	char *scan_report = NULL;
+	const char *fname = fsp->fsp_name->base_name;
+	const char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	struct virusfilter_cache_entry *scan_cache_e = NULL;
+	bool is_cache = false;
+	virusfilter_action file_action = VIRUSFILTER_ACTION_DO_NOTHING;
+	bool add_scan_cache = true;
+	bool ok = false;
+
+	if (config->cache) {
+		DBG_DEBUG("Searching cache entry: fname: %s\n", fname);
+		scan_cache_e = virusfilter_cache_get(config->cache,
+						     cwd_fname, fname);
+		if (scan_cache_e != NULL) {
+			DBG_DEBUG("Cache entry found: cached result: %d\n",
+			      scan_cache_e->result);
+			is_cache = true;
+			scan_result = scan_cache_e->result;
+			scan_report = scan_cache_e->report;
+			goto virusfilter_scan_result_eval;
+		}
+		DBG_DEBUG("Cache entry not found\n");
+	}
+
+	if (config->backend->fns->scan_init != NULL) {
+		scan_result = config->backend->fns->scan_init(config);
+		if (scan_result != VIRUSFILTER_RESULT_OK) {
+			scan_result = VIRUSFILTER_RESULT_ERROR;
+			scan_report = talloc_asprintf(
+				talloc_tos(),
+				"Initializing scanner failed");
+			goto virusfilter_scan_result_eval;
+		}
+	}
+
+	scan_result = config->backend->fns->scan(handle, config, fsp,
+						 &scan_report);
+
+	if (config->backend->fns->scan_end != NULL) {
+		bool scan_end = true;
+
+		if (config->scan_request_limit > 0) {
+			scan_end = false;
+			config->scan_request_count++;
+			if (config->scan_request_count >=
+			    config->scan_request_limit)
+			{
+				scan_end = true;
+				config->scan_request_count = 0;
+			}
+		}
+		if (scan_end) {
+			config->backend->fns->scan_end(config);
+		}
+	}
+
+virusfilter_scan_result_eval:
+
+	switch (scan_result) {
+	case VIRUSFILTER_RESULT_CLEAN:
+		DBG_INFO("Scan result: Clean: %s/%s\n", cwd_fname, fname);
+		break;
+
+	case VIRUSFILTER_RESULT_INFECTED:
+		DBG_ERR("Scan result: Infected: %s/%s: %s\n",
+			cwd_fname, fname, scan_report ? scan_report :
+			"infected (memory error on report)");
+		file_action = virusfilter_treat_infected_file(handle,
+					config, fsp, scan_report, is_cache);
+		if (file_action != VIRUSFILTER_ACTION_DO_NOTHING) {
+			add_scan_cache = false;
+		}
+		break;
+
+	case VIRUSFILTER_RESULT_SUSPECTED:
+		if (!config->block_suspected_file) {
+			break;
+		}
+		DBG_ERR("Scan result: Suspected: %s/%s: %s\n",
+			cwd_fname, fname, scan_report ? scan_report :
+			"suspected infection (memory error on report)");
+		file_action = virusfilter_treat_infected_file(handle,
+					config, fsp, scan_report, is_cache);
+		if (file_action != VIRUSFILTER_ACTION_DO_NOTHING) {
+			add_scan_cache = false;
+		}
+		break;
+
+	case VIRUSFILTER_RESULT_ERROR:
+		DBG_ERR("Scan result: Error: %s/%s: %s\n",
+			cwd_fname, fname, scan_report ? scan_report :
+			"error (memory error on report)");
+		virusfilter_treat_scan_error(handle, config, fsp,
+					     scan_report, is_cache);
+		add_scan_cache = false;
+		break;
+
+	default:
+		DBG_ERR("Scan result: Unknown result code %d: %s/%s: %s\n",
+			scan_result, cwd_fname, fname, scan_report ?
+			scan_report : "Unknown (memory error on report)");
+		virusfilter_treat_scan_error(handle, config, fsp,
+					     scan_report, is_cache);
+		add_scan_cache = false;
+		break;
+	}
+
+	if (config->cache) {
+		if (!is_cache && add_scan_cache) {
+			DBG_DEBUG("Adding new cache entry: %s, %d\n", fname,
+				  scan_result);
+			ok = virusfilter_cache_entry_add(
+					config->cache, cwd_fname, fname,
+					scan_result, scan_report);
+			if (!ok) {
+				DBG_ERR("Cannot create cache entry: "
+					"virusfilter_cache_entry_new failed");
+				goto virusfilter_scan_return;
+			}
+		} else if (is_cache) {
+			virusfilter_cache_entry_free(scan_cache_e);
+		}
+	}
+
+virusfilter_scan_return:
+	return scan_result;
+}
+
+static int virusfilter_vfs_open(
+	struct vfs_handle_struct *handle,
+	struct smb_filename *smb_fname,
+	files_struct *fsp,
+	int flags,
+	mode_t mode)
+{
+	TALLOC_CTX *mem_ctx = talloc_tos();
+	struct virusfilter_config *config;
+	const char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	virusfilter_result scan_result;
+	const char *fname = fsp->fsp_name->base_name;
+	char *dir_name = NULL;
+	const char *base_name = NULL;
+	int scan_errno = 0;
+	size_t test_prefix;
+	size_t test_suffix;
+	int rename_trap_count = 0;
+	int ret;
+	bool ok1, ok2;
+	char *sret = NULL;
+
+	SMB_VFS_HANDLE_GET_DATA(handle, config,
+				struct virusfilter_config, return -1);
+
+	test_prefix = strlen(config->rename_prefix);
+	test_suffix = strlen(config->rename_suffix);
+	if (test_prefix > 0) {
+		rename_trap_count++;
+	}
+	if (test_suffix > 0) {
+		rename_trap_count++;
+	}
+
+	ok1 = is_ntfs_stream_smb_fname(smb_fname);
+	ok2 = is_ntfs_default_stream_smb_fname(smb_fname);
+	if (ok1 && !ok2) {
+		DBG_INFO("Not scanned: only file backed streams can be scanned:"
+			 " %s/%s\n", cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+
+	if (!config->scan_on_open) {
+		DBG_INFO("Not scanned: scan on open is disabled: %s/%s\n",
+			 cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+
+	if (flags & O_TRUNC) {
+		DBG_INFO("Not scanned: Open flags have O_TRUNC: %s/%s\n",
+			 cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+
+	ret = SMB_VFS_NEXT_STAT(handle, smb_fname);
+	if (ret != 0) {
+
+		/*
+		 * Do not return immediately if !(flags & O_CREAT) &&
+		 * errno != ENOENT.
+		 * Do not do this here or anywhere else. The module is
+		 * stackable and there may be modules below, such as audit
+		 * modules, which should be handled.
+		 */
+		goto virusfilter_vfs_open_next;
+	}
+	ret = S_ISREG(smb_fname->st.st_ex_mode);
+	if (ret == 0) {
+		DBG_INFO("Not scanned: Directory or special file: %s/%s\n",
+			 cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+	if (config->max_file_size > 0 &&
+	    smb_fname->st.st_ex_size > config->max_file_size)
+	{
+		DBG_INFO("Not scanned: file size > max file size: %s/%s\n",
+			 cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+	if (config->min_file_size > 0 &&
+	    smb_fname->st.st_ex_size < config->min_file_size)
+	{
+		DBG_INFO("Not scanned: file size < min file size: %s/%s\n",
+		      cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+
+	ok1 = is_in_path(fname, config->exclude_files, false);
+	if (config->exclude_files && ok1)
+	{
+		DBG_INFO("Not scanned: exclude files: %s/%s\n",
+			 cwd_fname, fname);
+		goto virusfilter_vfs_open_next;
+	}
+
+	if (config->infected_file_action == VIRUSFILTER_ACTION_QUARANTINE) {
+		sret = strstr_m(fname, config->quarantine_dir);
+		if (sret != NULL) {
+			scan_errno = config->infected_open_errno;
+			goto virusfilter_vfs_open_fail;
+		}
+	}
+
+	if (test_prefix > 0 || test_suffix > 0) {
+		ok1 = parent_dirname(mem_ctx, fname, &dir_name, &base_name);
+		if (ok1)
+		{
+			if (test_prefix > 0) {
+				ret = strncmp(base_name,
+				    config->rename_prefix, test_prefix);
+				if (ret != 0) {
+					test_prefix = 0;
+				}
+			}
+			if (test_suffix > 0) {
+				ret = strcmp(base_name + (strlen(base_name)
+						 - test_suffix),
+						 config->rename_suffix);
+				if (ret != 0) {
+					test_suffix = 0;
+				}
+			}
+
+			TALLOC_FREE(dir_name);
+
+			if ((rename_trap_count == 2 && test_prefix &&
+			    test_suffix) || (rename_trap_count == 1 &&
+			    (test_prefix || test_suffix)))
+			{
+				scan_errno =
+					config->infected_open_errno;
+				goto virusfilter_vfs_open_fail;
+			}
+		}
+	}
+
+	scan_result = virusfilter_scan(handle, config, fsp);
+
+	switch (scan_result) {
+	case VIRUSFILTER_RESULT_CLEAN:
+		break;
+	case VIRUSFILTER_RESULT_INFECTED:
+		scan_errno = config->infected_open_errno;
+		goto virusfilter_vfs_open_fail;
+	case VIRUSFILTER_RESULT_ERROR:
+		if (config->block_access_on_error) {
+			DBG_INFO("Block access\n");
+			scan_errno = config->scan_error_open_errno;
+			goto virusfilter_vfs_open_fail;
+		}
+		break;
+	default:
+		scan_errno = config->scan_error_open_errno;
+		goto virusfilter_vfs_open_fail;
+	}
+
+virusfilter_vfs_open_next:
+	return SMB_VFS_NEXT_OPEN(handle, smb_fname, fsp, flags, mode);
+
+virusfilter_vfs_open_fail:
+	errno = (scan_errno != 0) ? scan_errno : EACCES;
+	return -1;
+}
+
+static int virusfilter_vfs_close(
+	struct vfs_handle_struct *handle,
+	files_struct *fsp)
+{
+	/*
+         * The name of this variable is for consistency. If API changes to
+         * match _open change to cwd_fname as in virusfilter_vfs_open.
+         */
+	const char *cwd_fname = handle->conn->connectpath;
+
+	struct virusfilter_config *config = NULL;
+	char *fname = fsp->fsp_name->base_name = NULL;
+	int close_result = -1;
+	int close_errno = 0;
+	virusfilter_result scan_result;
+	int scan_errno = 0;
+	bool ok1, ok2;
+
+	SMB_VFS_HANDLE_GET_DATA(handle, config,
+				struct virusfilter_config, return -1);
+
+	/*
+	 * Must close after scan? It appears not as the scanners are not
+	 * internal and other modules such as greyhole seem to do
+	 * SMB_VFS_NEXT_* functions before processing.
+	 */
+	close_result = SMB_VFS_NEXT_CLOSE(handle, fsp);
+	if (close_result == -1) {
+		close_errno = errno;
+	}
+
+	/*
+	 * Return immediately if close_result == -1, and close_errno == EBADF.
+	 * If close failed, file likely doesn't exist, do not try to scan.
+	 */
+	if (close_result == -1 && close_errno == EBADF) {
+		if (fsp->modified) {
+			DBG_DEBUG("Removing cache entry (if existent): "
+				  "fname: %s\n", fname);
+			virusfilter_cache_remove(config->cache,
+						 cwd_fname, fname);
+		}
+		goto virusfilter_vfs_close_fail;
+	}
+
+	if (fsp->is_directory) {
+		DBG_INFO("Not scanned: Directory: %s/%s\n", cwd_fname,
+			 fname);
+		return close_result;
+	}
+
+	ok1 = is_ntfs_stream_smb_fname(fsp->fsp_name);
+	ok2 = is_ntfs_default_stream_smb_fname(fsp->fsp_name);
+	if (ok1 && !ok2) {
+		if (config->scan_on_open && fsp->modified) {
+			if (config->cache) {
+				DBG_DEBUG("Removing cache entry (if existent)"
+					  ": fname: %s\n", fname);
+				virusfilter_cache_remove(
+						config->cache,
+						cwd_fname, fname);
+			}
+		}
+		DBG_INFO("Not scanned: only file backed streams can be scanned:"
+			 " %s/%s\n", cwd_fname, fname);
+		return close_result;
+	}
+
+	if (!config->scan_on_close) {
+		if (config->scan_on_open && fsp->modified) {
+			if (config->cache) {
+				DBG_DEBUG("Removing cache entry (if existent)"
+					  ": fname: %s\n", fname);
+				virusfilter_cache_remove(
+						config->cache,
+						cwd_fname, fname);
+			}
+		}
+		DBG_INFO("Not scanned: scan on close is disabled: %s/%s\n",
+			 cwd_fname, fname);
+		return close_result;
+	}
+
+	if (!fsp->modified) {
+		DBG_NOTICE("Not scanned: File not modified: %s/%s\n",
+			   cwd_fname, fname);
+
+		return close_result;
+	}
+
+	if (config->exclude_files && is_in_path(fname,
+	    config->exclude_files, false))
+	{
+		DBG_INFO("Not scanned: exclude files: %s/%s\n",
+			 cwd_fname, fname);
+		return close_result;
+	}
+
+	scan_result = virusfilter_scan(handle, config, fsp);
+
+	switch (scan_result) {
+	case VIRUSFILTER_RESULT_CLEAN:
+		break;
+	case VIRUSFILTER_RESULT_INFECTED:
+		scan_errno = config->infected_close_errno;
+		goto virusfilter_vfs_close_fail;
+	case VIRUSFILTER_RESULT_ERROR:
+		if (config->block_access_on_error) {
+			DBG_INFO("Block access\n");
+			scan_errno = config->scan_error_close_errno;
+			goto virusfilter_vfs_close_fail;
+		}
+		break;
+	default:
+		scan_errno = config->scan_error_close_errno;
+		goto virusfilter_vfs_close_fail;
+	}
+
+	if (close_errno != 0) {
+		errno = close_errno;
+	}
+
+	return close_result;
+
+virusfilter_vfs_close_fail:
+
+	errno = (scan_errno != 0) ? scan_errno : close_errno;
+
+	return close_result;
+}
+
+static int virusfilter_vfs_unlink(
+	struct vfs_handle_struct *handle,
+	const struct smb_filename *smb_fname)
+{
+	int ret = SMB_VFS_NEXT_UNLINK(handle, smb_fname);
+	struct virusfilter_config *config = NULL;
+	char *fname = NULL;
+	char *cwd_fname = handle->conn->cwd_fname->base_name;
+
+	if (ret != 0 && errno != ENOENT) {
+		return ret;
+	}
+
+	SMB_VFS_HANDLE_GET_DATA(handle, config,
+				struct virusfilter_config, return -1);
+
+	if (config->cache == NULL) {
+		return 0;
+	}
+
+	fname = smb_fname->base_name;
+
+	DBG_DEBUG("Removing cache entry (if existent): fname: %s\n", fname);
+	virusfilter_cache_remove(config->cache, cwd_fname, fname);
+
+	return 0;
+}
+
+static int virusfilter_vfs_rename(
+	struct vfs_handle_struct *handle,
+	const struct smb_filename *smb_fname_src,
+	const struct smb_filename *smb_fname_dst)
+{
+	int ret = SMB_VFS_NEXT_RENAME(handle, smb_fname_src, smb_fname_dst);
+	struct virusfilter_config *config = NULL;
+	char *fname = NULL;
+	char *dst_fname = NULL;
+	char *cwd_fname = handle->conn->cwd_fname->base_name;
+
+	if (ret != 0) {
+		return ret;
+	}
+
+	SMB_VFS_HANDLE_GET_DATA(handle, config,
+				struct virusfilter_config, return -1);
+
+	if (config->cache == NULL) {
+		return 0;
+	}
+
+	fname = smb_fname_src->base_name;
+	dst_fname = smb_fname_dst->base_name;
+
+	DBG_DEBUG("Renaming cache entry: fname: %s to: %s\n",
+		  fname, dst_fname);
+	virusfilter_cache_entry_rename(config->cache,
+				       cwd_fname, fname,
+				       dst_fname);
+
+	return 0;
+}
+
+/* VFS operations */
+static struct vfs_fn_pointers vfs_virusfilter_fns = {
+	.connect_fn	= virusfilter_vfs_connect,
+	.disconnect_fn	= virusfilter_vfs_disconnect,
+	.open_fn	= virusfilter_vfs_open,
+	.close_fn	= virusfilter_vfs_close,
+	.unlink_fn	= virusfilter_vfs_unlink,
+	.rename_fn	= virusfilter_vfs_rename,
+};
+
+NTSTATUS vfs_virusfilter_init(TALLOC_CTX *);
+NTSTATUS vfs_virusfilter_init(TALLOC_CTX *ctx)
+{
+	NTSTATUS status;
+
+	status = smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
+				  "virusfilter",
+				  &vfs_virusfilter_fns);
+	if (!NT_STATUS_IS_OK(status)) {
+		return status;
+	}
+
+	virusfilter_debug_class = debug_add_class("virusfilter");
+	if (virusfilter_debug_class == -1) {
+		virusfilter_debug_class = DBGC_VFS;
+		DBG_ERR("Couldn't register custom debugging class!\n");
+	} else {
+		DBG_DEBUG("Debug class number: %d\n", virusfilter_debug_class);
+	}
+
+	DBG_INFO("registered\n");
+
+	return status;
+}
diff --git a/source3/modules/vfs_virusfilter_common.h b/source3/modules/vfs_virusfilter_common.h
new file mode 100644
index 00000000000..468883fdaf8
--- /dev/null
+++ b/source3/modules/vfs_virusfilter_common.h
@@ -0,0 +1,149 @@
+/*
+   Samba-VirusFilter VFS modules
+   Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+
+   This 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 3 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/>.
+*/
+
+#ifndef _VIRUSFILTER_COMMON_H
+#define _VIRUSFILTER_COMMON_H
+
+#include <stdint.h>
+#include <time.h>
+
+/* Samba common include file */
+#include "includes.h"
+
+#include "smbd/smbd.h"
+#include "smbd/globals.h"
+#include "system/filesys.h"
+#include "transfer_file.h"
+#include "auth.h"
+#include "passdb.h"
+#include "../librpc/gen_ndr/ndr_netlogon.h"
+#include "../lib/tsocket/tsocket.h"
+
+/* Samba debug class for VIRUSFILTER */
+#undef DBGC_CLASS
+#define DBGC_CLASS virusfilter_debug_class
+extern int virusfilter_debug_class;
+
+/* Samba's global variable */
+extern userdom_struct current_user_info;
+
+#define VIRUSFILTER_VERSION "0.1.5"
+
+/* ====================================================================== */
+
+typedef enum {
+	VIRUSFILTER_ACTION_DO_NOTHING,
+	VIRUSFILTER_ACTION_QUARANTINE,
+	VIRUSFILTER_ACTION_RENAME,
+	VIRUSFILTER_ACTION_DELETE,
+} virusfilter_action;
+
+typedef enum {
+	VIRUSFILTER_RESULT_OK,
+	VIRUSFILTER_RESULT_CLEAN,
+	VIRUSFILTER_RESULT_ERROR,
+	VIRUSFILTER_RESULT_INFECTED,
+	VIRUSFILTER_RESULT_SUSPECTED,
+	/* FIXME: VIRUSFILTER_RESULT_RISKWARE, */
+} virusfilter_result;
+
+struct virusfilter_config {
+	int				scan_request_count;
+	int				scan_request_limit;
+
+	/* Scan on file operations */
+	bool				scan_on_open;
+	bool				scan_on_close;
+
+	/* Special scan options */
+	bool				scan_archive;
+	int				max_nested_scan_archive;
+	bool				scan_mime;
+	bool				block_suspected_file;
+
+	/* Size limit */
+	size_t				max_file_size;
+	size_t				min_file_size;
+
+	/* Exclude files */
+	name_compare_entry		*exclude_files;
+
+	/* Scan result cache */
+	struct virusfilter_cache	*cache;
+	int				cache_entry_limit;
+	int				cache_time_limit;
+
+	/* Infected file options */
+	virusfilter_action		infected_file_action;
+	const char *			infected_file_command;
+	int				infected_open_errno;
+	int				infected_close_errno;
+
+	/* Scan error options */
+	const char *			scan_error_command;
+	int				scan_error_open_errno;
+	int				scan_error_close_errno;
+	bool				block_access_on_error;
+
+	/* Quarantine infected files */
+	const char *			quarantine_dir;
+	const char *			quarantine_prefix;
+	const char *			quarantine_suffix;
+	bool				quarantine_keep_tree;
+	bool				quarantine_keep_name;
+	mode_t				quarantine_dir_mode;
+
+	/* Rename infected files */
+	const char *			rename_prefix;
+	const char *			rename_suffix;
+
+	/* Network options */
+	const char *			socket_path;
+	struct virusfilter_io_handle	*io_h;
+
+	/* The backend AV engine */
+	struct virusfilter_backend	*backend;
+};
+
+struct virusfilter_backend_fns {
+	int (*connect)(
+		struct vfs_handle_struct *handle,
+		struct virusfilter_config *config,
+		const char *svc,
+		const char *user);
+	void (*disconnect)(
+		struct vfs_handle_struct *handle);
+	virusfilter_result (*scan_init)(
+		struct virusfilter_config *config);
+	virusfilter_result (*scan)(
+		struct vfs_handle_struct *handle,
+		struct virusfilter_config *config,
+		const struct files_struct *fsp,
+		char **reportp);
+	void (*scan_end)(
+		struct virusfilter_config *config);
+};
+
+struct virusfilter_backend {
+	unsigned version;
+	const char *name;
+	const struct virusfilter_backend_fns *fns;
+	void *backend_private;
+};
+
+#endif /* _VIRUSFILTER_COMMON_H */
diff --git a/source3/modules/vfs_virusfilter_utils.c b/source3/modules/vfs_virusfilter_utils.c
new file mode 100644
index 00000000000..2b98431f676
--- /dev/null
+++ b/source3/modules/vfs_virusfilter_utils.c
@@ -0,0 +1,1031 @@
+/*
+   Samba-VirusFilter VFS modules
+   Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+   Copyright (C) 2016-2017 Trever L. Adams
+
+   This 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 3 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/>.
+*/
+
+#include "modules/vfs_virusfilter_common.h"
+#include "modules/vfs_virusfilter_utils.h"
+
+struct iovec;
+
+#include "lib/util/iov_buf.h"
+#include <tevent.h>
+#include "lib/tsocket/tsocket.h"
+
+int virusfilter_debug_class = DBGC_VFS;
+
+/* ====================================================================== */
+
+char *virusfilter_string_sub(
+	TALLOC_CTX *mem_ctx,
+	connection_struct *conn,
+	const char *str)
+{
+	return talloc_sub_advanced(mem_ctx,
+		lp_servicename(mem_ctx, SNUM(conn)),
+		conn->session_info->unix_info->unix_name,
+		conn->connectpath,
+		conn->session_info->unix_token->gid,
+		conn->session_info->unix_info->sanitized_username,
+		conn->session_info->info->domain_name,
+		str);
+}
+
+int virusfilter_vfs_next_move(
+	struct vfs_handle_struct *vfs_h,
+	const struct smb_filename *smb_fname_src,
+	const struct smb_filename *smb_fname_dst)
+{
+	int result;
+
+	result = SMB_VFS_NEXT_RENAME(vfs_h, smb_fname_src, smb_fname_dst);
+	if (result == 0 || errno != EXDEV) {
+		return result;
+	}
+
+	/*
+	 * For now, do not handle EXDEV as poking around violates
+	 * stackability. Return -1, simply refuse access.
+	 */
+	return -1;
+}
+
+/* Line-based socket I/O
+ * ======================================================================
+ */
+
+struct virusfilter_io_handle *virusfilter_io_new(
+	TALLOC_CTX *mem_ctx,
+	int connect_timeout,
+	int io_timeout)
+{
+	struct virusfilter_io_handle *io_h = talloc_zero(mem_ctx,
+						struct virusfilter_io_handle);
+
+	if (io_h == NULL) {
+		return NULL;
+	}
+
+	io_h->stream = NULL;
+	io_h->r_len = 0;
+
+	virusfilter_io_set_connect_timeout(io_h, connect_timeout);
+	virusfilter_io_set_io_timeout(io_h, io_timeout);
+	virusfilter_io_set_writel_eol(io_h, "\x0A", 1);
+	virusfilter_io_set_readl_eol(io_h, "\x0A", 1);
+
+	return io_h;
+}
+
+int virusfilter_io_set_connect_timeout(
+	struct virusfilter_io_handle *io_h,
+	int timeout)
+{
+	int timeout_old = io_h->connect_timeout;
+
+	/* timeout <= 0 means infinite */
+	io_h->connect_timeout = (timeout > 0) ? timeout : -1;
+
+	return timeout_old;
+}
+
+int virusfilter_io_set_io_timeout(
+	struct virusfilter_io_handle *io_h,
+	int timeout)
+{
+	int timeout_old = io_h->io_timeout;
+
+	/* timeout <= 0 means infinite */
+	io_h->io_timeout = (timeout > 0) ? timeout : -1;
+
+	return timeout_old;
+}
+
+void virusfilter_io_set_writel_eol(
+	struct virusfilter_io_handle *io_h,
+	const char *eol,
+	int eol_size)
+{
+	if (eol_size < 1 || eol_size > VIRUSFILTER_IO_EOL_SIZE) {
+		return;
+	}
+
+	memcpy(io_h->w_eol, eol, eol_size);
+	io_h->w_eol_size = eol_size;
+}
+
+void virusfilter_io_set_readl_eol(
+	struct virusfilter_io_handle *io_h,
+	const char *eol,
+	int eol_size)
+{
+	if (eol_size < 1 || eol_size > VIRUSFILTER_IO_EOL_SIZE) {
+		return;
+	}
+
+	memcpy(io_h->r_eol, eol, eol_size);
+	io_h->r_eol_size = eol_size;
+}
+
+bool virusfilter_io_connect_path(
+	struct virusfilter_io_handle *io_h,
+	const char *path)
+{
+	struct sockaddr_un addr;
+	NTSTATUS status;
+	int socket, bes_result, flags, ret;
+
+	ZERO_STRUCT(addr);
+	addr.sun_family = AF_UNIX;
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path));
+
+	status = open_socket_out((struct sockaddr_storage *)&addr, 0,
+				 io_h->connect_timeout,
+				 &socket);
+	if (!NT_STATUS_IS_OK(status)) {
+		io_h->stream = NULL;
+		return false;
+	}
+
+	/* We must not block */
+	flags = fcntl(socket, F_GETFL);
+	if (flags <= 0) {
+		/* Handle error by ignoring */;
+		flags = 0;
+		DBG_WARNING("Could not get flags on socket (%s).\n",
+			    strerror(errno));
+	}
+	flags |= SOCK_NONBLOCK;
+	ret = fcntl(socket, F_SETFL, flags);
+	if (ret == -1) {
+		/* Handle error by ignoring for now */
+		DBG_WARNING("Could not set flags on socket: %s.\n",
+			    strerror(errno));
+	}
+
+	bes_result = tstream_bsd_existing_socket(io_h, socket, &io_h->stream);
+	if (bes_result < 0) {
+		DBG_ERR("Could not convert socket to tstream: %s.\n",
+			strerror(errno));
+		io_h->stream = NULL;
+		return false;
+	}
+
+	return true;
+}
+
+static void disconnect_done(struct tevent_req *req)
+{
+	uint64_t *perr = tevent_req_callback_data(req, uint64_t);
+	int ret;
+	int err_ret;
+
+	ret = tstream_disconnect_recv(req, &err_ret);
+	TALLOC_FREE(req);
+	if (ret == -1) {
+		*perr = err_ret;
+	}
+}
+
+bool virusfilter_io_disconnect(
+	struct virusfilter_io_handle *io_h)
+{
+	struct tevent_req *req;
+	struct tevent_context *ev;
+	uint64_t *perror = NULL;
+	bool ok = true;
+	TALLOC_CTX *frame = talloc_stackframe();
+
+	if (io_h->stream == NULL) {
+		io_h->r_len = 0;
+		TALLOC_FREE(frame);
+		return VIRUSFILTER_RESULT_OK;
+	}
+
+	ev = tevent_context_init(frame);
+	if (ev == NULL) {
+		DBG_ERR("Failed to setup event context.\n");
+		ok = false;
+		goto fail;
+	}
+
+	/* Error return - must be talloc'ed. */
+	perror = talloc_zero(frame, uint64_t);
+	if (perror == NULL) {
+		goto fail;
+	}
+
+	req = tstream_disconnect_send(io_h, ev, io_h->stream);
+
+	/* Callback when disconnect is done. */
+	tevent_req_set_callback(req, disconnect_done, perror);
+
+	/* Set timeout. */
+	ok = tevent_req_set_endtime(req, ev, timeval_current_ofs_msec(
+				    io_h->connect_timeout));
+	if (!ok) {
+		DBG_ERR("Can't set endtime\n");
+		goto fail;
+	}
+
+	/* Loop waiting for req to finish. */
+	ok = tevent_req_poll(req, ev);
+	if (!ok) {
+		DBG_ERR("tevent_req_poll failed\n");
+		goto fail;
+	}
+
+	/* Emit debug error if failed. */
+	if (*perror != 0) {
+		DBG_DEBUG("Error %s\n", strerror((int)*perror));
+		goto fail;
+	}
+
+	/* Here we know we disconnected. */
+
+	io_h->stream = NULL;
+	io_h->r_len = 0;
+
+	fail:
+		TALLOC_FREE(frame);
+		return ok;
+}
+
+static void writev_done(struct tevent_req *req)
+{
+	uint64_t *perr = tevent_req_callback_data(req, uint64_t);
+	int ret;
+	int err_ret;
+
+	ret = tstream_writev_recv(req, &err_ret);
+	TALLOC_FREE(req);
+	if (ret == -1) {
+		*perr = err_ret;
+	}
+}
+
+/****************************************************************************
+ Write all data from an iov array, with msec timeout (per write)
+ NB. This can be called with a non-socket fd, don't add dependencies
+ on socket calls.
+****************************************************************************/
+
+bool write_data_iov_timeout(
+	struct tstream_context *stream,
+	const struct iovec *iov,
+	size_t iovcnt,
+	int ms_timeout)
+{
+	struct tevent_context *ev = NULL;
+	struct tevent_req *req = NULL;
+	uint64_t *perror = NULL;
+	bool ok = false;
+	TALLOC_CTX *frame = talloc_stackframe();
+
+	ev = tevent_context_init(frame);
+	if (ev == NULL) {
+		DBG_ERR("Failed to setup event context.\n");
+		goto fail;
+	}
+
+	/* Error return - must be talloc'ed. */
+	perror = talloc_zero(frame, uint64_t);
+	if (perror == NULL) {
+		goto fail;
+	}
+
+	/* Send the data. */
+	req = tstream_writev_send(frame, ev, stream, iov, iovcnt);
+	if (req == NULL) {
+		DBG_ERR("Out of memory.\n");
+		goto fail;
+	}
+
+	/* Callback when *all* data sent. */
+	tevent_req_set_callback(req, writev_done, perror);
+
+	/* Set timeout. */
+	ok = tevent_req_set_endtime(req, ev,
+				    timeval_current_ofs_msec(ms_timeout));
+	if (!ok) {
+		DBG_ERR("Can't set endtime\n");
+		goto fail;
+	}
+
+	/* Loop waiting for req to finish. */
+	ok = tevent_req_poll(req, ev);
+	if (!ok) {
+		DBG_ERR("tevent_req_poll failed\n");
+		goto fail;
+	}
+
+	/* Done with req - freed by the callback. */
+	req = NULL;
+
+	/* Emit debug error if failed. */
+	if (*perror != 0) {
+		DBG_DEBUG("Error %s\n", strerror((int)*perror));
+		goto fail;
+	}
+
+	/* Here we know we correctly wrote all data. */
+	TALLOC_FREE(frame);
+	return true;
+
+  fail:
+	TALLOC_FREE(frame);
+	return false;
+}
+
+bool virusfilter_io_write(
+	struct virusfilter_io_handle *io_h,
+	const char *data,
+	size_t data_size)
+{
+	struct iovec iov;
+
+	if (data_size == 0) {
+		return VIRUSFILTER_RESULT_OK;
+	}
+
+	iov.iov_base = discard_const_p(void, data);
+	iov.iov_len = data_size;
+
+	switch (write_data_iov_timeout(io_h->stream, &iov, 1,
+		io_h->io_timeout)) {
+	case false:
+		return false;
+	default:
+		return true;
+	}
+}
+
+bool virusfilter_io_writel(
+	struct virusfilter_io_handle *io_h,
+	const char *data,
+	size_t data_size)
+{
+	bool ok;
+
+	ok = virusfilter_io_write(io_h, data, data_size);
+	if (!ok) {
+		return ok;
+	}
+
+	return virusfilter_io_write(io_h, io_h->w_eol, io_h->w_eol_size);
+}
+
+bool virusfilter_io_writefl(
+	struct virusfilter_io_handle *io_h,
+	const char *data_fmt, ...)
+{
+	va_list ap;
+	char data[VIRUSFILTER_IO_BUFFER_SIZE + VIRUSFILTER_IO_EOL_SIZE];
+	size_t data_size;
+
+	va_start(ap, data_fmt);
+	data_size = vsnprintf(data, VIRUSFILTER_IO_BUFFER_SIZE, data_fmt, ap);
+	va_end(ap);
+
+	if (unlikely (data_size < 0)) {
+		DBG_ERR("vsnprintf failed: %s\n", strerror(errno));
+		return false;
+	}
+
+	memcpy(data + data_size, io_h->w_eol, io_h->w_eol_size);
+	data_size += io_h->w_eol_size;
+
+	return virusfilter_io_write(io_h, data, data_size);
+}
+
+bool virusfilter_io_vwritefl(
+	struct virusfilter_io_handle *io_h,
+	const char *data_fmt, va_list ap)
+{
+	char data[VIRUSFILTER_IO_BUFFER_SIZE + VIRUSFILTER_IO_EOL_SIZE];
+	size_t data_size;
+
+	data_size = vsnprintf(data, VIRUSFILTER_IO_BUFFER_SIZE, data_fmt, ap);
+
+	if (unlikely (data_size < 0)) {
+		DBG_ERR("vsnprintf failed: %s\n", strerror(errno));
+		return false;
+	}
+
+	memcpy(data + data_size, io_h->w_eol, io_h->w_eol_size);
+	data_size += io_h->w_eol_size;
+
+	return virusfilter_io_write(io_h, data, data_size);
+}
+
+bool virusfilter_io_writev(
+	struct virusfilter_io_handle *io_h, ...)
+{
+	va_list ap;
+	struct iovec iov[VIRUSFILTER_IO_IOV_MAX], *iov_p;
+	int iov_n;
+
+	va_start(ap, io_h);
+	for (iov_p = iov, iov_n = 0;
+	     iov_n < VIRUSFILTER_IO_IOV_MAX;
+	     iov_p++, iov_n++)
+	{
+		iov_p->iov_base = va_arg(ap, void *);
+		if (iov_p->iov_base == NULL) {
+			break;
+		}
+		iov_p->iov_len = va_arg(ap, int);
+	}
+	va_end(ap);
+
+	return write_data_iov_timeout(io_h->stream, iov, iov_n,
+		io_h->io_timeout);
+}
+
+bool virusfilter_io_writevl(
+	struct virusfilter_io_handle *io_h, ...)
+{
+	va_list ap;
+	struct iovec iov[VIRUSFILTER_IO_IOV_MAX + 1], *iov_p;
+	int iov_n;
+
+	va_start(ap, io_h);
+	for (iov_p = iov, iov_n = 0; iov_n < VIRUSFILTER_IO_IOV_MAX;
+	     iov_p++, iov_n++)
+	{
+		iov_p->iov_base = va_arg(ap, void *);
+		if (iov_p->iov_base == NULL) {
+			break;
+		}
+		iov_p->iov_len = va_arg(ap, int);
+	}
+	va_end(ap);
+
+	iov_p->iov_base = io_h->r_eol;
+	iov_p->iov_len = io_h->r_eol_size;
+	iov_n++;
+
+	return write_data_iov_timeout(io_h->stream, iov, iov_n,
+		io_h->io_timeout);
+}
+
+static bool return_existing_line(TALLOC_CTX *ctx,
+				struct virusfilter_io_handle *io_h,
+				char **read_line)
+{
+	size_t read_line_len = 0;
+	char *end_p = NULL;
+	char *eol = NULL;
+
+	eol = memmem(io_h->r_buffer, io_h->r_len,
+			io_h->r_eol, io_h->r_eol_size);
+	if (eol == NULL) {
+		return false;
+	}
+	end_p = eol + io_h->r_eol_size;
+
+	*eol = '\0';
+	read_line_len = strlen(io_h->r_buffer) + 1;
+	*read_line = talloc_memdup(ctx,
+				io_h->r_buffer,
+				read_line_len);
+	if (*read_line == NULL) {
+		return false;
+	}
+
+	/*
+	 * Copy the remaining buffer over the line
+	 * we returned.
+	 */
+	memmove(io_h->r_buffer,
+		end_p,
+		io_h->r_len - (end_p - io_h->r_buffer));
+
+	/* And reduce the size left in the buffer. */
+	io_h->r_len -= (end_p - io_h->r_buffer);
+	return true;
+}
+
+static void readv_done(struct tevent_req *req)
+{
+	uint64_t *perr = tevent_req_callback_data(req, uint64_t);
+	int ret;
+	int err_ret;
+
+	ret = tstream_readv_recv(req, &err_ret);
+	TALLOC_FREE(req);
+	if (ret == -1) {
+		*perr = err_ret;
+	}
+}
+
+bool virusfilter_io_readl(TALLOC_CTX *ctx,
+			struct virusfilter_io_handle *io_h,
+			char **read_line)
+{
+	struct tevent_context *ev = NULL;
+	bool ok = false;
+	uint64_t *perror = NULL;
+	TALLOC_CTX *frame = talloc_stackframe();
+
+	/* Search for an existing complete line. */
+	ok = return_existing_line(ctx, io_h, read_line);
+	if (ok) {
+		goto finish;
+	}
+
+	/*
+	 * No complete line in the buffer. We must read more
+	 * from the server.
+	 */
+	ev = tevent_context_init(frame);
+	if (ev == NULL) {
+		DBG_ERR("Failed to setup event context.\n");
+		goto finish;
+	}
+
+	/* Error return - must be talloc'ed. */
+	perror = talloc_zero(frame, uint64_t);
+	if (perror == NULL) {
+		goto finish;
+	}
+
+	for (;;) {
+		ssize_t pending = 0;
+		size_t read_size = 0;
+		struct iovec iov;
+		struct tevent_req *req = NULL;
+
+		/*
+		 * How much can we read ?
+		 */
+		pending = tstream_pending_bytes(io_h->stream);
+		if (pending < 0) {
+			DBG_ERR("tstream_pending_bytes failed (%s).\n",
+				strerror(errno));
+			goto finish;
+		}
+
+		read_size = pending;
+		/* Must read at least one byte. */
+		read_size = MIN(read_size, 1);
+
+		/* And max remaining buffer space. */
+		read_size = MAX(read_size,
+				(sizeof(io_h->r_buffer) - io_h->r_len));
+
+		if (read_size == 0) {
+			/* Buffer is full with no EOL. Error out. */
+			DBG_ERR("Line buffer full.\n");
+			goto finish;
+		}
+
+		iov.iov_base = io_h->r_buffer + io_h->r_len;
+		iov.iov_len = read_size;
+
+		/* Read the data. */
+		req = tstream_readv_send(frame,
+					ev,
+					io_h->stream,
+					&iov,
+					1);
+		if (req == NULL) {
+			DBG_ERR("out of memory.\n");
+			goto finish;
+		}
+
+		/* Callback when *all* data read. */
+		tevent_req_set_callback(req, readv_done, perror);
+
+		/* Set timeout. */
+		ok = tevent_req_set_endtime(req, ev,
+				timeval_current_ofs_msec(io_h->io_timeout));
+		if (!ok) {
+			DBG_ERR("can't set endtime\n");
+			goto finish;
+		}
+
+		/* Loop waiting for req to finish. */
+		ok = tevent_req_poll(req, ev);
+		if (!ok) {
+			DBG_ERR("tevent_req_poll failed\n");
+			goto finish;
+		}
+
+		/* Done with req - freed by the callback. */
+		req = NULL;
+
+		/*
+		 * Emit debug error if failed.
+		 * EPIPE may be success so, don't exit.
+		 */
+		if (*perror != 0 && *perror != EPIPE) {
+			DBG_DEBUG("Error %s\n", strerror((int)*perror));
+			errno = (int)*perror;
+			goto finish;
+		}
+
+		/*
+		 * We read read_size bytes. Extend the useable
+		 * buffer length.
+		 */
+		io_h->r_len += read_size;
+
+		/* Paranoia... */
+		SMB_ASSERT(io_h->r_len <= sizeof(io_h->r_buffer));
+
+		/* Exit if we have a line to return. */
+		ok = return_existing_line(ctx, io_h, read_line);
+		if (ok) {
+			goto finish;
+		}
+		/* No eol - keep reading. */
+	}
+
+  finish:
+
+	TALLOC_FREE(frame);
+	return ok;
+}
+
+bool virusfilter_io_writefl_readl(
+	struct virusfilter_io_handle *io_h,
+	char **read_line,
+	const char *fmt, ...)
+{
+	bool ok;
+
+	if (fmt) {
+		va_list ap;
+
+		va_start(ap, fmt);
+		ok = virusfilter_io_vwritefl(io_h, fmt, ap);
+		va_end(ap);
+
+		if (!ok) {
+			return ok;
+		}
+	}
+
+	ok = virusfilter_io_readl(talloc_tos(), io_h, read_line);
+	if (!ok) {
+		DBG_ERR("virusfilter_io_readl not OK: %d\n", ok);
+		return false;
+	}
+	if (io_h->r_len == 0) { /* EOF */
+		DBG_ERR("virusfilter_io_readl EOF\n");
+		return false;
+	}
+
+	return true;
+}
+
+struct virusfilter_cache *virusfilter_cache_new(
+	TALLOC_CTX *ctx,
+	int entry_limit,
+	time_t time_limit)
+{
+	struct virusfilter_cache *cache;
+
+	if (time_limit == 0) {
+		return NULL;
+	}
+
+	cache = talloc_zero(ctx, struct virusfilter_cache);
+	if (cache == NULL) {
+		DBG_ERR("talloc_zero failed.\n");
+		return NULL;
+	}
+
+	cache->cache = memcache_init(cache->ctx, entry_limit *
+				       (sizeof(struct virusfilter_cache_entry)
+				       + VIRUSFILTER_CACHE_BUFFER_SIZE));
+	if (cache->cache == NULL) {
+		DBG_ERR("memcache_init failed.\n");
+		return NULL;
+	}
+	cache->ctx = ctx;
+	cache->time_limit = time_limit;
+
+	return cache;
+}
+
+bool virusfilter_cache_entry_add(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	const char *fname,
+	virusfilter_result result,
+	char *report)
+{
+	int blob_size = sizeof(struct virusfilter_cache_entry);
+	struct virusfilter_cache_entry *cache_e =
+					talloc_zero_size(NULL, blob_size);
+	int fname_len = 0;
+
+	if (fname == NULL || directory == NULL) {
+		TALLOC_FREE(report);
+		return false;
+	}
+
+	fname = talloc_asprintf(talloc_tos(), "%s/%s", directory, fname);
+
+	if (fname == NULL) {
+		TALLOC_FREE(report);
+		return false;
+	}
+
+	fname_len = strlen(fname);
+
+	if (cache_e == NULL|| cache->time_limit == 0) {
+		TALLOC_FREE(report);
+		return false;
+	}
+
+	cache_e->result = result;
+	if (report != NULL) {
+		cache_e->report = talloc_steal(cache_e, report);
+	}
+	if (cache->time_limit > 0) {
+		cache_e->time = time(NULL);
+	}
+
+	memcache_add_talloc(cache->cache,
+			    VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC,
+			    data_blob_const(fname, fname_len), &cache_e);
+
+	return true;
+}
+
+bool virusfilter_cache_entry_rename(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	char *old_fname,
+	char *new_fname)
+{
+	int old_fname_len = 0;
+	int new_fname_len = 0;
+	struct virusfilter_cache_entry *new_data = NULL;
+	struct virusfilter_cache_entry *old_data = NULL;
+
+	if (old_fname == NULL || new_fname == NULL || directory == NULL) {
+		return false;
+	}
+
+	old_fname = talloc_asprintf(talloc_tos(), "%s/%s", directory, old_fname);
+	new_fname = talloc_asprintf(talloc_tos(), "%s/%s", directory, new_fname);
+
+	if (old_fname == NULL || new_fname == NULL) {
+		TALLOC_FREE(old_fname);
+		TALLOC_FREE(new_fname);
+		return false;
+	}
+
+	old_fname_len = strlen(old_fname);
+	new_fname_len = strlen(new_fname);
+
+	old_data = memcache_lookup_talloc(
+				cache->cache,
+				VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC,
+				data_blob_const(old_fname, old_fname_len));
+
+	if (old_data == NULL) {
+		return false;
+	}
+
+	new_data = talloc_memdup(cache->ctx, old_data,
+				 sizeof(struct virusfilter_cache_entry));
+	if (new_data == NULL) {
+		return false;
+	}
+	new_data->report = talloc_strdup(new_data, old_data->report);
+
+	memcache_add_talloc(cache->cache,
+			VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC,
+			data_blob_const(new_fname, new_fname_len), &new_data);
+
+	memcache_delete(cache->cache, VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC,
+			data_blob_const(old_fname, old_fname_len));
+
+	return true;
+}
+
+void virusfilter_cache_purge(struct virusfilter_cache *cache)
+{
+	memcache_flush(cache->cache, VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC);
+}
+
+struct virusfilter_cache_entry *virusfilter_cache_get(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	const char *fname)
+{
+	int fname_len = 0;
+	struct virusfilter_cache_entry *cache_e = NULL;
+	struct virusfilter_cache_entry *data = NULL;
+
+	if (fname == NULL || directory == NULL) {
+		return 0;
+	}
+
+	fname = talloc_asprintf(talloc_tos(), "%s/%s", directory, fname);
+
+	if (fname == NULL) {
+		return 0;
+	}
+
+	fname_len = strlen(fname);
+
+	data = memcache_lookup_talloc(cache->cache,
+				      VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC,
+				      data_blob_const(fname, fname_len));
+
+	if (data == NULL) {
+		return cache_e;
+	}
+
+	if (cache->time_limit > 0) {
+		if (time(NULL) - data->time  > cache->time_limit) {
+			DBG_DEBUG("Cache entry is too old: %s\n",
+				  fname);
+			virusfilter_cache_remove(cache, directory, fname);
+			return cache_e;
+		}
+	}
+	cache_e = talloc_memdup(cache->ctx, data,
+			       sizeof(struct virusfilter_cache_entry));
+	if (cache_e == NULL) {
+		return NULL;
+	}
+	if (data->report != NULL) {
+		cache_e->report = talloc_strdup(cache_e, data->report);
+	} else {
+		cache_e->report = NULL;
+	}
+
+	return cache_e;
+}
+
+void virusfilter_cache_remove(struct virusfilter_cache *cache,
+	const char *directory,
+	const char *fname)
+{
+	DBG_DEBUG("Purging cache entry: %s/%s\n", directory, fname);
+
+	if (fname == NULL || directory == NULL) {
+		return;
+	}
+
+	fname = talloc_asprintf(talloc_tos(), "%s/%s", directory, fname);
+
+	if (fname == NULL) {
+		return;
+	}
+
+	memcache_delete(cache->cache, VIRUSFILTER_SCAN_RESULTS_CACHE_TALLOC,
+			data_blob_const(fname, strlen(fname)));
+}
+
+void virusfilter_cache_entry_free(struct virusfilter_cache_entry *cache_e)
+{
+	if (cache_e != NULL) {
+		TALLOC_FREE(cache_e->report);
+		cache_e->report = NULL;
+	}
+	TALLOC_FREE(cache_e);
+}
+
+/* Shell scripting
+ * ======================================================================
+ */
+
+int virusfilter_env_set(
+	TALLOC_CTX *mem_ctx,
+	char **env_list,
+	const char *name,
+	const char *value)
+{
+	char *env_new;
+	int ret;
+
+	env_new = talloc_asprintf(mem_ctx, "%s=%s", name, value);
+	if (env_new == NULL) {
+		DBG_ERR("talloc_asprintf failed\n");
+		return -1;
+	}
+
+	ret = strv_add(mem_ctx, env_list, env_new);
+
+	TALLOC_FREE(env_new);
+
+	return ret;
+}
+
+/* virusfilter_env version Samba's *_sub_advanced() in substitute.c */
+int virusfilter_shell_set_conn_env(
+	TALLOC_CTX *mem_ctx,
+	char **env_list,
+	connection_struct *conn)
+{
+	int snum = SNUM(conn);
+	char *server_addr_p;
+	char *client_addr_p;
+	const char *local_machine_name = get_local_machine_name();
+	fstring pidstr;
+	int ret;
+
+	if (local_machine_name == NULL || *local_machine_name == '\0') {
+		local_machine_name = lp_netbios_name();
+	}
+
+	server_addr_p = tsocket_address_inet_addr_string(
+				conn->sconn->local_address, talloc_tos());
+
+	if (server_addr_p != NULL) {
+		ret = strncmp("::ffff:", server_addr_p, 7);
+		if (ret == 0) {
+			server_addr_p += 7;
+		}
+		virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_SERVER_IP",
+				    server_addr_p);
+	}
+	TALLOC_FREE(server_addr_p);
+
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_SERVER_NAME",
+			    myhostname());
+	virusfilter_env_set(mem_ctx, env_list,
+			    "VIRUSFILTER_SERVER_NETBIOS_NAME",
+			    local_machine_name);
+	slprintf(pidstr,sizeof(pidstr)-1, "%ld", (long)getpid());
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_SERVER_PID",
+			    pidstr);
+
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_SERVICE_NAME",
+			    lp_const_servicename(snum));
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_SERVICE_PATH",
+			    conn->cwd_fname->base_name);
+
+	client_addr_p = tsocket_address_inet_addr_string(
+				conn->sconn->remote_address, talloc_tos());
+
+	if (client_addr_p != NULL) {
+		ret = strncmp("::ffff:", client_addr_p, 7);
+		if (ret == 0) {
+			client_addr_p += 7;
+		}
+		virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_CLIENT_IP",
+				    client_addr_p);
+	}
+	TALLOC_FREE(client_addr_p);
+
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_CLIENT_NAME",
+			    conn->sconn->remote_hostname);
+	virusfilter_env_set(mem_ctx, env_list,
+			    "VIRUSFILTER_CLIENT_NETBIOS_NAME",
+			    get_remote_machine_name());
+
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_USER_NAME",
+			    get_current_username());
+	virusfilter_env_set(mem_ctx, env_list, "VIRUSFILTER_USER_DOMAIN",
+			    current_user_info.domain);
+
+	return 0;
+}
+
+/* Wrapper to Samba's smbrun() in smbrun.c */
+int virusfilter_shell_run(
+	TALLOC_CTX *mem_ctx,
+	const char *cmd,
+	char **env_list,
+	connection_struct *conn,
+	bool sanitize)
+{
+	int ret;
+
+	if (conn != NULL) {
+		ret = virusfilter_shell_set_conn_env(mem_ctx, env_list, conn);
+		if (ret == -1) {
+			return -1;
+		}
+	}
+
+	if (sanitize) {
+		return smbrun(cmd, NULL, strv_to_env(talloc_tos(), *env_list));
+	} else {
+		return smbrun_no_sanitize(cmd, NULL, strv_to_env(talloc_tos(),
+					  *env_list));
+	}
+}
diff --git a/source3/modules/vfs_virusfilter_utils.h b/source3/modules/vfs_virusfilter_utils.h
new file mode 100644
index 00000000000..69754aa6546
--- /dev/null
+++ b/source3/modules/vfs_virusfilter_utils.h
@@ -0,0 +1,177 @@
+/*
+   Samba-VirusFilter VFS modules
+   Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+
+   This 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 3 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/>.
+*/
+
+#ifndef _VIRUSFILTER_UTILS_H
+#define _VIRUSFILTER_UTILS_H
+
+#include "modules/vfs_virusfilter_common.h"
+#include "../lib/util/memcache.h"
+#include "../lib/util/strv.h"
+
+/*#define str_eq(s1, s2)		\
+	((strcmp((s1), (s2)) == 0) ? true : false)
+#define strn_eq(s1, s2, n)	\
+	((strncmp((s1), (s2), (n)) == 0) ? true : false) */
+
+/* "* 3" is for %-encoding */
+#define VIRUSFILTER_IO_URL_MAX		(PATH_MAX * 3)
+#define VIRUSFILTER_IO_BUFFER_SIZE	(VIRUSFILTER_IO_URL_MAX + 128)
+#define VIRUSFILTER_IO_EOL_SIZE		1
+#define VIRUSFILTER_IO_IOV_MAX		16
+#define VIRUSFILTER_CACHE_BUFFER_SIZE	(PATH_MAX + 128)
+
+struct virusfilter_io_handle {
+	struct tstream_context *stream;
+	int		connect_timeout;	/* msec */
+	int		io_timeout;		/* msec */
+
+	/* end-of-line character(s) */
+	char		w_eol[VIRUSFILTER_IO_EOL_SIZE];
+	int		w_eol_size;
+
+	/* end-of-line character(s) */
+	char		r_eol[VIRUSFILTER_IO_EOL_SIZE];
+	int		r_eol_size;
+
+	/* buffer */
+	char		r_buffer[VIRUSFILTER_IO_BUFFER_SIZE];
+	size_t		r_len;
+};
+
+struct virusfilter_cache_entry {
+	time_t time;
+	virusfilter_result result;
+	char *report;
+};
+
+struct virusfilter_cache {
+	struct memcache *cache;
+	TALLOC_CTX *ctx;
+	time_t time_limit;
+};
+
+/* ====================================================================== */
+
+char *virusfilter_string_sub(
+	TALLOC_CTX *mem_ctx,
+	connection_struct *conn,
+	const char *str);
+int virusfilter_vfs_next_move(
+	vfs_handle_struct *handle,
+	const struct smb_filename *smb_fname_src,
+	const struct smb_filename *smb_fname_dst);
+
+/* Line-based socket I/O */
+struct virusfilter_io_handle *virusfilter_io_new(
+	TALLOC_CTX *mem_ctx,
+	int connect_timeout,
+	int timeout);
+int virusfilter_io_set_connect_timeout(
+	struct virusfilter_io_handle *io_h,
+	int timeout);
+int virusfilter_io_set_io_timeout(
+	struct virusfilter_io_handle *io_h, int timeout);
+void virusfilter_io_set_writel_eol(
+	struct virusfilter_io_handle *io_h,
+	const char *eol,
+	int eol_size);
+void virusfilter_io_set_readl_eol(
+	struct virusfilter_io_handle *io_h,
+	const char *eol,
+	int eol_size);
+bool virusfilter_io_connect_path(
+	struct virusfilter_io_handle *io_h,
+	const char *path);
+bool virusfilter_io_disconnect(
+	struct virusfilter_io_handle *io_h);
+bool write_data_iov_timeout(
+	struct tstream_context *stream,
+	const struct iovec *iov,
+	size_t iovcnt,
+	int ms_timeout);
+bool virusfilter_io_write(
+	struct virusfilter_io_handle *io_h,
+	const char *data,
+	size_t data_size);
+bool virusfilter_io_writel(
+	struct virusfilter_io_handle *io_h,
+	const char *data,
+	size_t data_size);
+bool virusfilter_io_writefl(
+	struct virusfilter_io_handle *io_h,
+	const char *data_fmt, ...);
+bool virusfilter_io_vwritefl(
+	struct virusfilter_io_handle *io_h,
+	const char *data_fmt, va_list ap);
+bool virusfilter_io_writev(
+	struct virusfilter_io_handle *io_h, ...);
+bool virusfilter_io_writevl(
+	struct virusfilter_io_handle *io_h, ...);
+bool virusfilter_io_readl(TALLOC_CTX *ctx,
+			struct virusfilter_io_handle *io_h,
+			char **read_line);
+bool virusfilter_io_writefl_readl(
+	struct virusfilter_io_handle *io_h,
+	char **read_line,
+	const char *fmt, ...);
+
+/* Scan result cache */
+struct virusfilter_cache *virusfilter_cache_new(
+	TALLOC_CTX *ctx,
+	int entry_limit,
+	time_t time_limit);
+bool virusfilter_cache_entry_add(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	const char *fname,
+	virusfilter_result result,
+	char *report);
+bool virusfilter_cache_entry_rename(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	char *old_fname,
+	char *new_fname);
+void virusfilter_cache_entry_free(struct virusfilter_cache_entry *cache_e);
+struct virusfilter_cache_entry *virusfilter_cache_get(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	const char *fname);
+void virusfilter_cache_remove(
+	struct virusfilter_cache *cache,
+	const char *directory,
+	const char *fname);
+void virusfilter_cache_purge(struct virusfilter_cache *cache);
+
+/* Shell scripting */
+int virusfilter_env_set(
+	TALLOC_CTX *mem_ctx,
+	char **env_list,
+	const char *name,
+	const char *value);
+int virusfilter_shell_set_conn_env(
+	TALLOC_CTX *mem_ctx,
+	char **env_list,
+	connection_struct *conn);
+int virusfilter_shell_run(
+	TALLOC_CTX *mem_ctx,
+	const char *cmd,
+	char **env_list,
+	connection_struct *conn,
+	bool sanitize);
+
+#endif /* _VIRUSFILTER_UTILS_H */
diff --git a/source3/modules/wscript_build b/source3/modules/wscript_build
index 079302cd584..f4179477376 100644
--- a/source3/modules/wscript_build
+++ b/source3/modules/wscript_build
@@ -17,6 +17,11 @@ bld.SAMBA3_LIBRARY('non_posix_acls',
                    deps='samba-util vfs',
                    private_library=True)
 
+bld.SAMBA3_SUBSYSTEM('VFS_VIRUSFILTER_UTILS',
+                   source='vfs_virusfilter_utils.c',
+                   deps='strv',
+                   enabled=(bld.SAMBA3_IS_ENABLED_MODULE('vfs_virusfilter')))
+
 bld.SAMBA3_SUBSYSTEM('VFS_AIXACL_UTIL',
                     source='vfs_aixacl_util.c',
                     enabled=(bld.SAMBA3_IS_ENABLED_MODULE('vfs_aixacl') or bld.SAMBA3_IS_ENABLED_MODULE('vfs_aixacl2')))
@@ -505,6 +510,14 @@ bld.SAMBA3_MODULE('vfs_snapper',
                  internal_module=bld.SAMBA3_IS_STATIC_MODULE('vfs_snapper'),
                  enabled=bld.SAMBA3_IS_ENABLED_MODULE('vfs_snapper'))
 
+bld.SAMBA3_MODULE('vfs_virusfilter',
+                 subsystem='vfs',
+                 source='vfs_virusfilter.c',
+                 deps='samba-util VFS_VIRUSFILTER_UTILS',
+                 init_function='',
+                 internal_module=bld.SAMBA3_IS_STATIC_MODULE('vfs_virusfilter'),
+                 enabled=bld.SAMBA3_IS_ENABLED_MODULE('vfs_virusfilter'))
+
 bld.SAMBA3_MODULE('vfs_vxfs',
                  subsystem='vfs',
                  source='lib_vxfs.c vfs_vxfs.c',
diff --git a/source3/wscript b/source3/wscript
index 8751833b221..6d2d94bae87 100644
--- a/source3/wscript
+++ b/source3/wscript
@@ -1669,7 +1669,7 @@ main() {
                                       vfs_preopen vfs_catia
                                       vfs_media_harmony vfs_unityed_media vfs_fruit vfs_shell_snap
                                       vfs_commit vfs_worm vfs_crossrename vfs_linux_xfs_sgid
-                                      vfs_time_audit vfs_offline
+                                      vfs_time_audit vfs_offline vfs_virusfilter
                                   '''))
     default_shared_modules.extend(TO_LIST('auth_script idmap_tdb2 idmap_script'))
     # these have broken dependencies
-- 
2.16.0.rc1.238.g530d649a79-goog


From 3f64f4444bd89dc207906efc2ab6c213ef95c2e3 Mon Sep 17 00:00:00 2001
From: "Trever L. Adams" <trever.adams at gmail.com>
Date: Tue, 18 Oct 2016 13:38:14 -0600
Subject: [PATCH 3/5] Samba-VirusFilter: Sophos VFS backend.

Signed-off-by: Trever L. Adams <trever.adams at gmail.com>
Signed-off-by: SATOH Fumiyasu <fumiyas at osstech.co.jp>
Reviewed-by: Jeremy Allison <jra at samba.org>
---
 docs-xml/manpages/vfs_virusfilter.8.xml  |   6 +
 source3/modules/vfs_virusfilter.c        |  15 +-
 source3/modules/vfs_virusfilter_common.h |   2 +
 source3/modules/vfs_virusfilter_sophos.c | 391 +++++++++++++++++++++++++++++++
 source3/modules/wscript_build            |   5 +-
 5 files changed, 414 insertions(+), 5 deletions(-)
 create mode 100644 source3/modules/vfs_virusfilter_sophos.c

diff --git a/docs-xml/manpages/vfs_virusfilter.8.xml b/docs-xml/manpages/vfs_virusfilter.8.xml
index eb6112e3827..c4bc8920043 100644
--- a/docs-xml/manpages/vfs_virusfilter.8.xml
+++ b/docs-xml/manpages/vfs_virusfilter.8.xml
@@ -41,6 +41,10 @@
 		<term>virusfilter:scanner</term>
 		<listitem>
 		<para>The antivirus scan-engine.</para>
+		<itemizedlist>
+		  <listitem><para><emphasis>sophos</emphasis>, the Sophos AV
+		  scanner</para></listitem>
+		</itemizedlist>
 		</listitem>
 		</varlistentry>
 
@@ -52,6 +56,8 @@
 		<para>If this option is not set, the default path depends on the
 		configured AV scanning engine.
 		</para>
+		<para>For the <emphasis>sophos</emphasis>backend the default is
+		<emphasis>/var/run/savdi/sssp.sock</emphasis>.</para>
 		</listitem>
 		</varlistentry>
 
diff --git a/source3/modules/vfs_virusfilter.c b/source3/modules/vfs_virusfilter.c
index eb8ac26665f..0353adaa37d 100644
--- a/source3/modules/vfs_virusfilter.c
+++ b/source3/modules/vfs_virusfilter.c
@@ -441,10 +441,17 @@ static int virusfilter_vfs_connect(
 		return -1;
 	}
 
-	/* This goes away as soon as the next commit adds an actual backend... */
-	if (config->backend == NULL) {
-		DBG_INFO("Not implemented\n");
-		return SMB_VFS_NEXT_CONNECT(handle, svc, user);
+	switch (backend) {
+	case VIRUSFILTER_SCANNER_SOPHOS:
+		ret = virusfilter_sophos_init(config);
+		break;
+	default:
+		DBG_ERR("Unhandled scanner %d\n", backend);
+		return -1;
+	}
+	if (ret != 0) {
+		DBG_ERR("Scanner backend init failed\n");
+		return -1;
 	}
 
 	if (config->backend->fns->connect != NULL) {
diff --git a/source3/modules/vfs_virusfilter_common.h b/source3/modules/vfs_virusfilter_common.h
index 468883fdaf8..69519c9daa4 100644
--- a/source3/modules/vfs_virusfilter_common.h
+++ b/source3/modules/vfs_virusfilter_common.h
@@ -146,4 +146,6 @@ struct virusfilter_backend {
 	void *backend_private;
 };
 
+int virusfilter_sophos_init(struct virusfilter_config *config);
+
 #endif /* _VIRUSFILTER_COMMON_H */
diff --git a/source3/modules/vfs_virusfilter_sophos.c b/source3/modules/vfs_virusfilter_sophos.c
new file mode 100644
index 00000000000..72051cd64a2
--- /dev/null
+++ b/source3/modules/vfs_virusfilter_sophos.c
@@ -0,0 +1,391 @@
+/*
+   Samba-VirusFilter VFS modules
+   Sophos Anti-Virus savdid (SSSP/1.0) support
+   Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+
+   This 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 3 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/>.
+*/
+
+#include "vfs_virusfilter_common.h"
+#include "vfs_virusfilter_utils.h"
+
+/* Default values for standard "extra" configuration variables */
+#ifdef SOPHOS_DEFAULT_SOCKET_PATH
+#  define VIRUSFILTER_DEFAULT_SOCKET_PATH	SOPHOS_DEFAULT_SOCKET_PATH
+#else
+#  define VIRUSFILTER_DEFAULT_SOCKET_PATH	"/var/run/savdi/sssp.sock"
+#endif
+
+static void virusfilter_sophos_scan_end(struct virusfilter_config *config);
+
+/* Python's urllib.quote(string[, safe]) clone */
+static int virusfilter_url_quote(const char *src, char *dst, int dst_size)
+{
+	char *dst_c = dst;
+	static char hex[] = "0123456789ABCDEF";
+
+	for (; *src != '\0'; src++) {
+		if ((*src < '0' && *src != '-' && *src != '.' && *src != '/') ||
+		    (*src > '9' && *src < 'A') ||
+		    (*src > 'Z' && *src < 'a' && *src != '_') ||
+		    (*src > 'z'))
+		{
+			if (dst_size < 4) {
+				return -1;
+			}
+			*dst_c++ = '%';
+			*dst_c++ = hex[(*src >> 4) & 0x0F];
+			*dst_c++ = hex[*src & 0x0F];
+			dst_size -= 3;
+		} else {
+			if (dst_size < 2) {
+				return -1;
+			}
+			*dst_c++ = *src;
+			dst_size--;
+		}
+	}
+
+	*dst_c = '\0';
+
+	return (dst_c - dst);
+}
+
+static int virusfilter_sophos_connect(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const char *svc,
+	const char *user)
+{
+	virusfilter_io_set_readl_eol(config->io_h, "\x0D\x0A", 2);
+
+	return 0;
+}
+
+static virusfilter_result virusfilter_sophos_scan_ping(
+	struct virusfilter_config *config)
+{
+	struct virusfilter_io_handle *io_h = config->io_h;
+	char *reply = NULL;
+	bool ok;
+	int ret;
+
+	/* SSSP/1.0 has no "PING" command */
+	ok = virusfilter_io_writel(io_h, "SSSP/1.0 OPTIONS\n", 17);
+	if (!ok) {
+		return VIRUSFILTER_RESULT_ERROR;
+	}
+
+	for (;;) {
+		ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+		if (!ok) {
+			return VIRUSFILTER_RESULT_ERROR;
+		}
+		ret = strcmp(reply, "");
+		if (ret == 0) {
+			break;
+		}
+		TALLOC_FREE(reply);
+	}
+
+	TALLOC_FREE(reply);
+	return VIRUSFILTER_RESULT_OK;
+}
+
+static virusfilter_result virusfilter_sophos_scan_init(
+	struct virusfilter_config *config)
+{
+	struct virusfilter_io_handle *io_h = config->io_h;
+	char *reply = NULL;
+	int ret;
+	bool ok;
+
+	if (io_h->stream != NULL) {
+		DBG_DEBUG("SSSP: Checking if connection is alive\n");
+
+		ret = virusfilter_sophos_scan_ping(config);
+		if (ret == VIRUSFILTER_RESULT_OK)
+		{
+			DBG_DEBUG("SSSP: Re-using existent connection\n");
+			return VIRUSFILTER_RESULT_OK;
+		}
+
+		DBG_INFO("SSSP: Closing dead connection\n");
+		virusfilter_sophos_scan_end(config);
+	}
+
+
+	DBG_INFO("SSSP: Connecting to socket: %s\n",
+		config->socket_path);
+
+	become_root();
+	ok = virusfilter_io_connect_path(io_h, config->socket_path);
+	unbecome_root();
+
+	if (!ok) {
+		DBG_ERR("SSSP: Connecting to socket failed: %s: %s\n",
+			config->socket_path, strerror(errno));
+		return VIRUSFILTER_RESULT_ERROR;
+	}
+
+	ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+	if (!ok) {
+		DBG_ERR("SSSP: Reading greeting message failed: %s\n",
+			strerror(errno));
+		goto virusfilter_sophos_scan_init_failed;
+	}
+	ret = strncmp(reply, "OK SSSP/1.0", 11);
+	if (ret != 0) {
+		DBG_ERR("SSSP: Invalid greeting message: %s\n",
+			reply);
+		goto virusfilter_sophos_scan_init_failed;
+	}
+
+	DBG_DEBUG("SSSP: Connected\n");
+
+	DBG_INFO("SSSP: Configuring\n");
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply,
+	    "SSSP/1.0 OPTIONS\noutput:brief\nsavigrp:GrpArchiveUnpack %d\n",
+	    config->scan_archive ? 1 : 0);
+	if (!ok) {
+		DBG_ERR("SSSP: OPTIONS: I/O error: %s\n", strerror(errno));
+		goto virusfilter_sophos_scan_init_failed;
+	}
+	ret = strncmp(reply, "ACC ", 4);
+	if (ret != 0) {
+		DBG_ERR("SSSP: OPTIONS: Not accepted: %s\n", reply);
+		goto virusfilter_sophos_scan_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+	if (!ok) {
+		DBG_ERR("SSSP: OPTIONS: Read error: %s\n", strerror(errno));
+		goto virusfilter_sophos_scan_init_failed;
+	}
+	ret = strncmp(reply, "DONE OK ", 8);
+	if (ret != 0) {
+		DBG_ERR("SSSP: OPTIONS failed: %s\n", reply);
+		goto virusfilter_sophos_scan_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+	if (!ok) {
+		DBG_ERR("SSSP: OPTIONS: Read error: %s\n", strerror(errno));
+		goto virusfilter_sophos_scan_init_failed;
+	}
+	ret = strcmp(reply, "");
+	if (ret != 0) {
+		DBG_ERR("SSSP: OPTIONS: Invalid reply: %s\n", reply);
+		goto virusfilter_sophos_scan_init_failed;
+	}
+
+	DBG_DEBUG("SSSP: Configured\n");
+
+	return VIRUSFILTER_RESULT_OK;
+
+virusfilter_sophos_scan_init_failed:
+
+	TALLOC_FREE(reply);
+
+	virusfilter_sophos_scan_end(config);
+
+	return VIRUSFILTER_RESULT_ERROR;
+}
+
+static void virusfilter_sophos_scan_end(
+	struct virusfilter_config *config)
+{
+	struct virusfilter_io_handle *io_h = config->io_h;
+
+	DBG_INFO("SSSP: Disconnecting\n");
+
+	virusfilter_io_disconnect(io_h);
+}
+
+static virusfilter_result virusfilter_sophos_scan(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp,
+	char **reportp)
+{
+	char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	const char *fname = fsp->fsp_name->base_name;
+	char fileurl[VIRUSFILTER_IO_URL_MAX+1];
+	int fileurl_len, fileurl_len2;
+	struct virusfilter_io_handle *io_h = config->io_h;
+	virusfilter_result result = VIRUSFILTER_RESULT_ERROR;
+	char *report = NULL;
+	char *reply = NULL;
+	char *reply_token, *reply_saveptr;
+	int ret;
+	bool ok;
+
+	DBG_INFO("Scanning file: %s/%s\n", cwd_fname, fname);
+
+	fileurl_len = virusfilter_url_quote(cwd_fname, fileurl,
+					    VIRUSFILTER_IO_URL_MAX);
+	if (fileurl_len < 0) {
+		DBG_ERR("virusfilter_url_quote failed: File path too long: "
+			"%s/%s\n", cwd_fname, fname);
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(), "File path too long");
+		goto virusfilter_sophos_scan_return;
+	}
+	fileurl[fileurl_len] = '/';
+	fileurl_len++;
+
+	fileurl_len += fileurl_len2 = virusfilter_url_quote(fname,
+		fileurl + fileurl_len, VIRUSFILTER_IO_URL_MAX - fileurl_len);
+	if (fileurl_len2 < 0) {
+		DBG_ERR("virusfilter_url_quote failed: File path too long: "
+			"%s/%s\n", cwd_fname, fname);
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(), "File path too long");
+		goto virusfilter_sophos_scan_return;
+	}
+	fileurl_len += fileurl_len2;
+
+	ok = virusfilter_io_writevl(io_h, "SSSP/1.0 SCANFILE ", 18, fileurl,
+				    fileurl_len, NULL);
+	if (!ok) {
+		DBG_ERR("SSSP: SCANFILE: Write error: %s\n",
+		      strerror(errno));
+		goto virusfilter_sophos_scan_io_error;
+	}
+
+	ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+	if (!ok) {
+		DBG_ERR("SSSP: SCANFILE: Read error: %s\n", strerror(errno));
+		goto virusfilter_sophos_scan_io_error;
+	}
+	ret = strncmp(reply, "ACC ", 4);
+	if (ret != 0) {
+		DBG_ERR("SSSP: SCANFILE: Not accepted: %s\n",
+			reply);
+		result = VIRUSFILTER_RESULT_ERROR;
+		goto virusfilter_sophos_scan_return;
+	}
+
+	TALLOC_FREE(reply);
+
+	result = VIRUSFILTER_RESULT_CLEAN;
+	for (;;) {
+		ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+		if (!ok) {
+			DBG_ERR("SSSP: SCANFILE: Read error: %s\n",
+				strerror(errno));
+			goto virusfilter_sophos_scan_io_error;
+		}
+
+		ret = strcmp(reply, "");
+		if (ret == 0) {
+			break;
+		}
+
+		reply_token = strtok_r(reply, " ", &reply_saveptr);
+
+		if (strcmp(reply_token, "VIRUS") == 0) {
+			result = VIRUSFILTER_RESULT_INFECTED;
+			reply_token = strtok_r(NULL, " ", &reply_saveptr);
+			if (reply_token != NULL) {
+				  report = talloc_strdup(talloc_tos(),
+							 reply_token);
+			} else {
+				  report = talloc_asprintf(talloc_tos(),
+							"UNKNOWN INFECTION");
+			}
+		} else if (strcmp(reply_token, "OK") == 0) {
+
+			/* Ignore */
+		} else if (strcmp(reply_token, "DONE") == 0) {
+			reply_token = strtok_r(NULL, "", &reply_saveptr);
+			if (reply_token != NULL &&
+
+			    /* Succeed */
+			    strncmp(reply_token, "OK 0000 ", 8) != 0 &&
+
+			    /* Infected */
+			    strncmp(reply_token, "OK 0203 ", 8) != 0)
+			{
+				DBG_ERR("SSSP: SCANFILE: Error: %s\n",
+					reply_token);
+				result = VIRUSFILTER_RESULT_ERROR;
+				report = talloc_asprintf(talloc_tos(),
+							 "Scanner error: %s\n",
+							 reply_token);
+			}
+		} else {
+			DBG_ERR("SSSP: SCANFILE: Invalid reply: %s\n",
+				reply_token);
+			result = VIRUSFILTER_RESULT_ERROR;
+			report = talloc_asprintf(talloc_tos(), "Scanner "
+						 "communication error");
+		}
+
+		TALLOC_FREE(reply);
+	}
+
+virusfilter_sophos_scan_return:
+	TALLOC_FREE(reply);
+
+	if (report == NULL) {
+		*reportp = talloc_asprintf(talloc_tos(),
+					   "Scanner report memory error");
+	} else {
+		*reportp = report;
+	}
+
+	return result;
+
+virusfilter_sophos_scan_io_error:
+	*reportp = talloc_asprintf(talloc_tos(),
+				   "Scanner I/O error: %s\n", strerror(errno));
+
+	return result;
+}
+
+static struct virusfilter_backend_fns virusfilter_backend_sophos ={
+	.connect = virusfilter_sophos_connect,
+	.disconnect = NULL,
+	.scan_init = virusfilter_sophos_scan_init,
+	.scan = virusfilter_sophos_scan,
+	.scan_end = virusfilter_sophos_scan_end,
+};
+
+int virusfilter_sophos_init(struct virusfilter_config *config)
+{
+	struct virusfilter_backend *backend = NULL;
+
+	if (config->socket_path == NULL) {
+		config->socket_path = VIRUSFILTER_DEFAULT_SOCKET_PATH;
+	}
+
+	backend = talloc_zero(config, struct virusfilter_backend);
+	if (backend == NULL) {
+		return -1;
+	}
+
+	backend->fns = &virusfilter_backend_sophos;
+	backend->name = "sophos";
+
+	config->backend = backend;
+	return 0;
+}
diff --git a/source3/modules/wscript_build b/source3/modules/wscript_build
index f4179477376..14fddb3b30e 100644
--- a/source3/modules/wscript_build
+++ b/source3/modules/wscript_build
@@ -512,7 +512,10 @@ bld.SAMBA3_MODULE('vfs_snapper',
 
 bld.SAMBA3_MODULE('vfs_virusfilter',
                  subsystem='vfs',
-                 source='vfs_virusfilter.c',
+                 source='''
+                 vfs_virusfilter.c
+                 vfs_virusfilter_sophos.c
+                 ''',
                  deps='samba-util VFS_VIRUSFILTER_UTILS',
                  init_function='',
                  internal_module=bld.SAMBA3_IS_STATIC_MODULE('vfs_virusfilter'),
-- 
2.16.0.rc1.238.g530d649a79-goog


From 7c4b6a92eab0d3f26c897197006a8d60dc9a4712 Mon Sep 17 00:00:00 2001
From: "Trever L. Adams" <trever.adams at gmail.com>
Date: Tue, 18 Oct 2016 13:39:20 -0600
Subject: [PATCH 4/5] Samba-VirusFilter: F-Secure AntiVirus (fsav) VFS and man
 page.

Signed-off-by: Trever L. Adams <trever.adams at gmail.com>
Signed-off-by: SATOH Fumiyasu <fumiyas at osstech.co.jp>
Reviewed-by: Jeremy Allison <jra at samba.org>
---
 docs-xml/manpages/vfs_virusfilter.8.xml  |  27 +-
 source3/modules/vfs_virusfilter.c        |   3 +
 source3/modules/vfs_virusfilter_common.h |   1 +
 source3/modules/vfs_virusfilter_fsav.c   | 451 +++++++++++++++++++++++++++++++
 source3/modules/wscript_build            |   1 +
 5 files changed, 481 insertions(+), 2 deletions(-)
 create mode 100644 source3/modules/vfs_virusfilter_fsav.c

diff --git a/docs-xml/manpages/vfs_virusfilter.8.xml b/docs-xml/manpages/vfs_virusfilter.8.xml
index c4bc8920043..2e70ab0b553 100644
--- a/docs-xml/manpages/vfs_virusfilter.8.xml
+++ b/docs-xml/manpages/vfs_virusfilter.8.xml
@@ -44,6 +44,8 @@
 		<itemizedlist>
 		  <listitem><para><emphasis>sophos</emphasis>, the Sophos AV
 		  scanner</para></listitem>
+		  <listitem><para><emphasis>fsav</emphasis>, the F-Secure AV
+		  scanner</para></listitem>
 		</itemizedlist>
 		</listitem>
 		</varlistentry>
@@ -58,6 +60,8 @@
 		</para>
 		<para>For the <emphasis>sophos</emphasis>backend the default is
 		<emphasis>/var/run/savdi/sssp.sock</emphasis>.</para>
+		<para>For the <emphasis>fsav</emphasis> backend the default is
+		<emphasis>/tmp/.fsav-0</emphasis>.</para>
 		</listitem>
 		</varlistentry>
 
@@ -219,7 +223,7 @@
 		<term>virusfilter:scan archive = true</term>
 		<listitem>
 		<para>This defines whether or not to scan archives.</para>
-		<para>Sophos supports this and defaults to false.</para>
+		<para>Sophos and F-Secure support this and it defaults to false.</para>
 		</listitem>
 		</varlistentry>
 
@@ -227,7 +231,16 @@
 		<term>virusfilter:max nested scan archive = 1</term>
 		<listitem>
 		<para>This defines the maximum depth to search nested archives.</para>
-		<para>The Sophos module supports this and defaults to 1.</para>
+		<para>The Sophos and F-Secure support this and it defaults to 1.</para>
+		</listitem>
+		</varlistentry>
+
+		<varlistentry>
+		<term>virusfilter:scan mime = true</term>
+		<listitem>
+		<para>This defines whether or not to scan mime files.</para>
+		<para>Only the <emphasis>fsav</emphasis>scanner supports this
+		option and defaults to false.</para>
 		</listitem>
 		</varlistentry>
 
@@ -309,6 +322,16 @@
 		</listitem>
 		</varlistentry>
 
+		<varlistentry>
+		<term>virusfilter:block suspected file = false</term>
+		<listitem>
+		<para>With this option on, suspected malware will be blocked as
+		well. Only the <emphasis>fsav</emphasis>scanner supports this
+		option.</para>
+		<para>If this option is not set, the default is false.</para>
+		</listitem>
+		</varlistentry>
+
 	</variablelist>
 </refsect1>
 
diff --git a/source3/modules/vfs_virusfilter.c b/source3/modules/vfs_virusfilter.c
index 0353adaa37d..d6910db9f32 100644
--- a/source3/modules/vfs_virusfilter.c
+++ b/source3/modules/vfs_virusfilter.c
@@ -445,6 +445,9 @@ static int virusfilter_vfs_connect(
 	case VIRUSFILTER_SCANNER_SOPHOS:
 		ret = virusfilter_sophos_init(config);
 		break;
+	case VIRUSFILTER_SCANNER_FSAV:
+		ret = virusfilter_fsav_init(config);
+		break;
 	default:
 		DBG_ERR("Unhandled scanner %d\n", backend);
 		return -1;
diff --git a/source3/modules/vfs_virusfilter_common.h b/source3/modules/vfs_virusfilter_common.h
index 69519c9daa4..a28ce2978fa 100644
--- a/source3/modules/vfs_virusfilter_common.h
+++ b/source3/modules/vfs_virusfilter_common.h
@@ -147,5 +147,6 @@ struct virusfilter_backend {
 };
 
 int virusfilter_sophos_init(struct virusfilter_config *config);
+int virusfilter_fsav_init(struct virusfilter_config *config);
 
 #endif /* _VIRUSFILTER_COMMON_H */
diff --git a/source3/modules/vfs_virusfilter_fsav.c b/source3/modules/vfs_virusfilter_fsav.c
new file mode 100644
index 00000000000..2b874d7db43
--- /dev/null
+++ b/source3/modules/vfs_virusfilter_fsav.c
@@ -0,0 +1,451 @@
+/*
+   Samba-VirusFilter VFS modules
+   F-Secure Anti-Virus fsavd support
+   Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+
+   This 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 3 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/>.
+*/
+
+#include "vfs_virusfilter_common.h"
+#include "vfs_virusfilter_utils.h"
+
+#ifdef FSAV_DEFAULT_SOCKET_PATH
+#  define VIRUSFILTER_DEFAULT_SOCKET_PATH	FSAV_DEFAULT_SOCKET_PATH
+#else
+#  define VIRUSFILTER_DEFAULT_SOCKET_PATH	"/tmp/.fsav-0"
+#endif
+
+/* Default values for module-specific configuration variables */
+/* 5 = F-Secure Linux 7 or later? */
+
+#define VIRUSFILTER_DEFAULT_FSAV_PROTOCOL		5
+#define VIRUSFILTER_DEFAULT_SCAN_RISKWARE		false
+#define VIRUSFILTER_DEFAULT_STOP_SCAN_ON_FIRST		true
+#define VIRUSFILTER_DEFAULT_FILTER_FILENAME		false
+
+struct virusfilter_fsav_config {
+	/* Backpointer */
+	struct virusfilter_config *config;
+
+	int fsav_protocol;
+	bool scan_riskware;
+	bool stop_scan_on_first;
+	bool filter_filename;
+};
+
+static void virusfilter_fsav_scan_end(struct virusfilter_config *config);
+
+static int virusfilter_fsav_destruct_config(
+	struct virusfilter_fsav_config *fsav_config)
+{
+	virusfilter_fsav_scan_end(fsav_config->config);
+	return 0;
+}
+
+static int virusfilter_fsav_connect(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const char *svc,
+	const char *user)
+{
+	int snum = SNUM(handle->conn);
+	struct virusfilter_fsav_config *fsav_config = NULL;
+
+	fsav_config = talloc_zero(config->backend,
+				  struct virusfilter_fsav_config);
+	if (fsav_config == NULL) {
+		return -1;
+	}
+
+	fsav_config->config = config;
+
+	fsav_config->fsav_protocol = lp_parm_int(
+		snum, "virusfilter", "fsav protocol",
+		VIRUSFILTER_DEFAULT_FSAV_PROTOCOL);
+
+	fsav_config->scan_riskware = lp_parm_bool(
+		snum, "virusfilter", "scan riskware",
+		VIRUSFILTER_DEFAULT_SCAN_RISKWARE);
+
+	fsav_config->stop_scan_on_first = lp_parm_bool(
+		snum, "virusfilter", "stop scan on first",
+		VIRUSFILTER_DEFAULT_STOP_SCAN_ON_FIRST);
+
+	fsav_config->filter_filename = lp_parm_bool(
+		snum, "virusfilter", "filter filename",
+		VIRUSFILTER_DEFAULT_FILTER_FILENAME);
+
+	talloc_set_destructor(fsav_config, virusfilter_fsav_destruct_config);
+
+	config->backend->backend_private = fsav_config;
+
+	config->block_suspected_file = lp_parm_bool(
+		snum, "virusfilter", "block suspected file", false);
+
+	return 0;
+}
+
+static virusfilter_result virusfilter_fsav_scan_init(
+	struct virusfilter_config *config)
+{
+	struct virusfilter_fsav_config *fsav_config = NULL;
+	struct virusfilter_io_handle *io_h = config->io_h;
+	char *reply = NULL;
+	bool ok;
+	int ret;
+
+	fsav_config = talloc_get_type_abort(config->backend->backend_private,
+					    struct virusfilter_fsav_config);
+
+	if (io_h->stream != NULL) {
+		DBG_DEBUG("fsavd: Checking if connection is alive\n");
+
+		/* FIXME: I don't know the correct PING command format... */
+		ok = virusfilter_io_writefl_readl(io_h, &reply, "PING");
+		if (ok)	{
+			ret = strncmp(reply, "ERROR\t", 6);
+			if (ret == 0) {
+				DBG_DEBUG("fsavd: Re-using existent "
+					  "connection\n");
+				goto virusfilter_fsav_init_succeed;
+			}
+		}
+
+		DBG_DEBUG("fsavd: Closing dead connection\n");
+		virusfilter_fsav_scan_end(config);
+	}
+
+	DBG_INFO("fsavd: Connecting to socket: %s\n",
+		 config->socket_path);
+
+	become_root();
+	ok = virusfilter_io_connect_path(io_h, config->socket_path);
+	unbecome_root();
+
+	if (!ok) {
+		DBG_ERR("fsavd: Connecting to socket failed: %s: %s\n",
+			config->socket_path, strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_readl(talloc_tos(), io_h, &reply);
+	if (!ok) {
+		DBG_ERR("fsavd: Reading greeting message failed: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "DBVERSION\t", 10);
+	if (ret != 0) {
+		DBG_ERR("fsavd: Invalid greeting message: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	DBG_DEBUG("fsavd: Connected\n");
+
+	DBG_INFO("fsavd: Configuring\n");
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply, "PROTOCOL\t%d",
+					  fsav_config->fsav_protocol);
+	if (!ok) {
+		DBG_ERR("fsavd: PROTOCOL: I/O error: %s\n", strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: PROTOCOL: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply,
+					  "CONFIGURE\tSTOPONFIRST\t%d",
+					  fsav_config->stop_scan_on_first ?
+					  1 : 0);
+	if (!ok) {
+		DBG_ERR("fsavd: CONFIGURE STOPONFIRST: I/O error: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: CONFIGURE STOPONFIRST: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply, "CONFIGURE\tFILTER\t%d",
+					  fsav_config->filter_filename ? 1 : 0);
+	if (!ok) {
+		DBG_ERR("fsavd: CONFIGURE FILTER: I/O error: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: CONFIGURE FILTER: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply,
+					  "CONFIGURE\tARCHIVE\t%d",
+					  config->scan_archive ? 1 : 0);
+	if (!ok) {
+		DBG_ERR("fsavd: CONFIGURE ARCHIVE: I/O error: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: CONFIGURE ARCHIVE: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply,
+					  "CONFIGURE\tMAXARCH\t%d",
+					  config->max_nested_scan_archive);
+	if (!ok) {
+		DBG_ERR("fsavd: CONFIGURE MAXARCH: I/O error: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: CONFIGURE MAXARCH: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply,
+					  "CONFIGURE\tMIME\t%d",
+					  config->scan_mime ? 1 : 0);
+	if (!ok) {
+		DBG_ERR("fsavd: CONFIGURE MIME: I/O error: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: CONFIGURE MIME: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	TALLOC_FREE(reply);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply, "CONFIGURE\tRISKWARE\t%d",
+					  fsav_config->scan_riskware ? 1 : 0);
+	if (!ok) {
+		DBG_ERR("fsavd: CONFIGURE RISKWARE: I/O error: %s\n",
+			strerror(errno));
+		goto virusfilter_fsav_init_failed;
+	}
+	ret = strncmp(reply, "OK\t", 3);
+	if (ret != 0) {
+		DBG_ERR("fsavd: CONFIGURE RISKWARE: Not accepted: %s\n",
+			reply);
+		goto virusfilter_fsav_init_failed;
+	}
+
+	DBG_DEBUG("fsavd: Configured\n");
+
+virusfilter_fsav_init_succeed:
+	TALLOC_FREE(reply);
+	return VIRUSFILTER_RESULT_OK;
+
+virusfilter_fsav_init_failed:
+	TALLOC_FREE(reply);
+	virusfilter_fsav_scan_end(config);
+
+	return VIRUSFILTER_RESULT_ERROR;
+}
+
+static void virusfilter_fsav_scan_end(struct virusfilter_config *config)
+{
+	struct virusfilter_io_handle *io_h = config->io_h;
+
+	DBG_INFO("fsavd: Disconnecting\n");
+	virusfilter_io_disconnect(io_h);
+}
+
+static virusfilter_result virusfilter_fsav_scan(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp,
+	char **reportp)
+{
+	char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	const char *fname = fsp->fsp_name->base_name;
+	struct virusfilter_io_handle *io_h = config->io_h;
+	virusfilter_result result = VIRUSFILTER_RESULT_CLEAN;
+	char *report = NULL;
+	char *reply = NULL;
+	char *reply_token, *reply_saveptr;
+	bool ok;
+
+	DBG_INFO("Scanning file: %s/%s\n", cwd_fname, fname);
+
+	ok = virusfilter_io_writevl(io_h, "SCAN\t", 5, cwd_fname,
+				    (int)strlen(cwd_fname), "/", 1, fname,
+				    (int)strlen(fname), NULL);
+	if (!ok) {
+		DBG_ERR("fsavd: SCAN: Write error: %s\n", strerror(errno));
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(),
+					 "Scanner I/O error: %s\n",
+					 strerror(errno));
+		goto virusfilter_fsav_scan_return;
+	}
+
+	TALLOC_FREE(reply);
+
+	for (;;) {
+		if (virusfilter_io_readl(talloc_tos(), io_h, &reply) != true) {
+			DBG_ERR("fsavd: SCANFILE: Read error: %s\n",
+				strerror(errno));
+			result = VIRUSFILTER_RESULT_ERROR;
+			report = talloc_asprintf(talloc_tos(),
+						 "Scanner I/O error: %s\n",
+						 strerror(errno));
+			break;
+		}
+
+		reply_token = strtok_r(reply, "\t", &reply_saveptr);
+
+		if (strcmp(reply_token, "OK") == 0) {
+			break;
+		} else if (strcmp(reply_token, "CLEAN") == 0) {
+
+			/* CLEAN\t<FILEPATH> */
+			result = VIRUSFILTER_RESULT_CLEAN;
+			report = talloc_asprintf(talloc_tos(), "Clean");
+		} else if (strcmp(reply_token, "INFECTED") == 0 ||
+			   strcmp(reply_token, "ARCHIVE_INFECTED") == 0 ||
+			   strcmp(reply_token, "MIME_INFECTED") == 0 ||
+			   strcmp(reply_token, "RISKWARE") == 0 ||
+			   strcmp(reply_token, "ARCHIVE_RISKWARE") == 0 ||
+			   strcmp(reply_token, "MIME_RISKWARE") == 0)
+		{
+
+			/* INFECTED\t<FILEPATH>\t<REPORT>\t<ENGINE> */
+			result = VIRUSFILTER_RESULT_INFECTED;
+			reply_token = strtok_r(NULL, "\t", &reply_saveptr);
+			reply_token = strtok_r(NULL, "\t", &reply_saveptr);
+			if (reply_token != NULL) {
+				  report = talloc_strdup(talloc_tos(),
+							 reply_token);
+			} else {
+				  report = talloc_asprintf(talloc_tos(),
+							"UNKNOWN INFECTION");
+			}
+		} else if (strcmp(reply_token, "OPEN_ARCHIVE") == 0) {
+
+			/* Ignore */
+		} else if (strcmp(reply_token, "CLOSE_ARCHIVE") == 0) {
+
+			/* Ignore */
+		} else if ((strcmp(reply_token, "SUSPECTED") == 0 ||
+			   strcmp(reply_token, "ARCHIVE_SUSPECTED") == 0 ||
+			   strcmp(reply_token, "MIME_SUSPECTED") == 0) &&
+			   config->block_suspected_file)
+		{
+			result = VIRUSFILTER_RESULT_SUSPECTED;
+			reply_token = strtok_r(NULL, "\t", &reply_saveptr);
+			reply_token = strtok_r(NULL, "\t", &reply_saveptr);
+			if (reply_token != NULL) {
+				  report = talloc_strdup(talloc_tos(),
+							 reply_token);
+			} else {
+				  report = talloc_asprintf(talloc_tos(),
+						"UNKNOWN REASON SUSPECTED");
+			}
+		} else if (strcmp(reply_token, "SCAN_FAILURE") == 0) {
+
+			/* SCAN_FAILURE\t<FILEPATH>\t0x<CODE>\t<REPORT> [<ENGINE>] */
+			result = VIRUSFILTER_RESULT_ERROR;
+			reply_token = strtok_r(NULL, "\t", &reply_saveptr);
+			reply_token = strtok_r(NULL, "\t", &reply_saveptr);
+			DBG_ERR("fsavd: SCANFILE: Scaner error: %s\n",
+				reply_token ? reply_token : "UNKNOWN ERROR");
+			report = talloc_asprintf(talloc_tos(),
+						 "Scanner error: %s",
+						 reply_token ? reply_token :
+						 "UNKNOWN ERROR");
+		} else {
+			result = VIRUSFILTER_RESULT_ERROR;
+			DBG_ERR("fsavd: SCANFILE: Invalid reply: %s\t",
+				reply_token);
+			report = talloc_asprintf(talloc_tos(),
+						 "Scanner communication error");
+		}
+
+		TALLOC_FREE(reply);
+	}
+
+virusfilter_fsav_scan_return:
+	TALLOC_FREE(reply);
+
+	if (report == NULL) {
+		*reportp = talloc_asprintf(talloc_tos(), "Scanner report memory "
+					   "error");
+	} else {
+		*reportp = report;
+	}
+
+	return result;
+}
+
+static struct virusfilter_backend_fns virusfilter_backend_fsav ={
+	.connect = virusfilter_fsav_connect,
+	.disconnect = NULL,
+	.scan_init = virusfilter_fsav_scan_init,
+	.scan = virusfilter_fsav_scan,
+	.scan_end = virusfilter_fsav_scan_end,
+};
+
+int virusfilter_fsav_init(struct virusfilter_config *config)
+{
+	struct virusfilter_backend *backend = NULL;
+
+	if (config->socket_path == NULL) {
+		config->socket_path = VIRUSFILTER_DEFAULT_SOCKET_PATH;
+	}
+
+	backend = talloc_zero(config, struct virusfilter_backend);
+	if (backend == NULL) {
+		return -1;
+	}
+
+	backend->fns = &virusfilter_backend_fsav;
+	backend->name = "fsav";
+
+	config->backend = backend;
+	return 0;
+}
diff --git a/source3/modules/wscript_build b/source3/modules/wscript_build
index 14fddb3b30e..f63c00a9955 100644
--- a/source3/modules/wscript_build
+++ b/source3/modules/wscript_build
@@ -515,6 +515,7 @@ bld.SAMBA3_MODULE('vfs_virusfilter',
                  source='''
                  vfs_virusfilter.c
                  vfs_virusfilter_sophos.c
+                 vfs_virusfilter_fsav.c
                  ''',
                  deps='samba-util VFS_VIRUSFILTER_UTILS',
                  init_function='',
-- 
2.16.0.rc1.238.g530d649a79-goog


From 935bc12c97bbf868412997867458ec4cc7696661 Mon Sep 17 00:00:00 2001
From: "Trever L. Adams" <trever.adams at gmail.com>
Date: Tue, 18 Oct 2016 13:40:01 -0600
Subject: [PATCH 5/5] Samba-VirusFilter: clamav VFS and man page.

Signed-off-by: Trever L. Adams <trever.adams at gmail.com>
Signed-off-by: SATOH Fumiyasu <fumiyas at osstech.co.jp>
Reviewed-by: Jeremy Allison <jra at samba.org>
---
 docs-xml/manpages/vfs_virusfilter.8.xml  |   4 +
 source3/modules/vfs_virusfilter.c        |   3 +
 source3/modules/vfs_virusfilter_clamav.c | 195 +++++++++++++++++++++++++++++++
 source3/modules/vfs_virusfilter_common.h |   1 +
 source3/modules/wscript_build            |   1 +
 5 files changed, 204 insertions(+)
 create mode 100644 source3/modules/vfs_virusfilter_clamav.c

diff --git a/docs-xml/manpages/vfs_virusfilter.8.xml b/docs-xml/manpages/vfs_virusfilter.8.xml
index 2e70ab0b553..ee49df11575 100644
--- a/docs-xml/manpages/vfs_virusfilter.8.xml
+++ b/docs-xml/manpages/vfs_virusfilter.8.xml
@@ -46,6 +46,8 @@
 		  scanner</para></listitem>
 		  <listitem><para><emphasis>fsav</emphasis>, the F-Secure AV
 		  scanner</para></listitem>
+		  <listitem><para><emphasis>clamav</emphasis>, the ClamAV
+		  scanner</para></listitem>
 		</itemizedlist>
 		</listitem>
 		</varlistentry>
@@ -62,6 +64,8 @@
 		<emphasis>/var/run/savdi/sssp.sock</emphasis>.</para>
 		<para>For the <emphasis>fsav</emphasis> backend the default is
 		<emphasis>/tmp/.fsav-0</emphasis>.</para>
+		<para>For the <emphasis>fsav</emphasis> backend the default is
+		<emphasis>/var/run/clamav/clamd.ctl</emphasis>.</para>
 		</listitem>
 		</varlistentry>
 
diff --git a/source3/modules/vfs_virusfilter.c b/source3/modules/vfs_virusfilter.c
index d6910db9f32..eeeed35047a 100644
--- a/source3/modules/vfs_virusfilter.c
+++ b/source3/modules/vfs_virusfilter.c
@@ -448,6 +448,9 @@ static int virusfilter_vfs_connect(
 	case VIRUSFILTER_SCANNER_FSAV:
 		ret = virusfilter_fsav_init(config);
 		break;
+	case VIRUSFILTER_SCANNER_CLAMAV:
+		ret = virusfilter_clamav_init(config);
+		break;
 	default:
 		DBG_ERR("Unhandled scanner %d\n", backend);
 		return -1;
diff --git a/source3/modules/vfs_virusfilter_clamav.c b/source3/modules/vfs_virusfilter_clamav.c
new file mode 100644
index 00000000000..d0e1fc081b7
--- /dev/null
+++ b/source3/modules/vfs_virusfilter_clamav.c
@@ -0,0 +1,195 @@
+/*
+   Samba-VirusFilter VFS modules
+   ClamAV clamd support
+   Copyright (C) 2010-2016 SATOH Fumiyasu @ OSS Technology Corp., Japan
+
+   This 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 3 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/>.
+*/
+
+/* Default values for standard "extra" configuration variables */
+
+#ifdef CLAMAV_DEFAULT_SOCKET_PATH
+#  define VIRUSFILTER_DEFAULT_SOCKET_PATH	CLAMAV_DEFAULT_SOCKET_PATH
+#else
+#  define VIRUSFILTER_DEFAULT_SOCKET_PATH	"/var/run/clamav/clamd.ctl"
+#endif
+
+#include "modules/vfs_virusfilter_common.h"
+#include "modules/vfs_virusfilter_utils.h"
+
+static int virusfilter_clamav_connect(struct vfs_handle_struct *handle,
+				      struct virusfilter_config *config,
+				      const char *svc,
+				      const char *user)
+{
+
+	/* To use clamd "zXXXX" commands */
+	virusfilter_io_set_writel_eol(config->io_h, "\0", 1);
+	virusfilter_io_set_readl_eol(config->io_h, "\0", 1);
+
+	return 0;
+}
+
+static virusfilter_result virusfilter_clamav_scan_init(
+	struct virusfilter_config *config)
+{
+	struct virusfilter_io_handle *io_h = config->io_h;
+	bool ok;
+
+	DBG_INFO("clamd: Connecting to socket: %s\n",
+		 config->socket_path);
+
+	become_root();
+	ok = virusfilter_io_connect_path(io_h, config->socket_path);
+	unbecome_root();
+
+	if (!ok) {
+		DBG_ERR("clamd: Connecting to socket failed: %s: %s\n",
+			config->socket_path, strerror(errno));
+		return VIRUSFILTER_RESULT_ERROR;
+	}
+
+	DBG_INFO("clamd: Connected\n");
+
+	return VIRUSFILTER_RESULT_OK;
+}
+
+static void virusfilter_clamav_scan_end(
+	struct virusfilter_config *config)
+{
+	struct virusfilter_io_handle *io_h = config->io_h;
+
+	DBG_INFO("clamd: Disconnecting\n");
+
+	virusfilter_io_disconnect(io_h);
+}
+
+static virusfilter_result virusfilter_clamav_scan(
+	struct vfs_handle_struct *handle,
+	struct virusfilter_config *config,
+	const struct files_struct *fsp,
+	char **reportp)
+{
+	char *cwd_fname = fsp->conn->cwd_fname->base_name;
+	const char *fname = fsp->fsp_name->base_name;
+	size_t filepath_len = strlen(cwd_fname) + 1 /* slash */ + strlen(fname);
+	struct virusfilter_io_handle *io_h = config->io_h;
+	virusfilter_result result = VIRUSFILTER_RESULT_CLEAN;
+	char *report = NULL;
+	char *reply = NULL;
+	char *reply_msg = NULL;
+	char *reply_token;
+	bool ok;
+
+	DBG_INFO("Scanning file: %s/%s\n", cwd_fname, fname);
+
+	ok = virusfilter_io_writefl_readl(io_h, &reply, "zSCAN %s/%s",
+					  cwd_fname, fname);
+	if (!ok) {
+		DBG_ERR("clamd: zSCAN: I/O error: %s\n", strerror(errno));
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(),
+					 "Scanner I/O error: %s\n",
+					 strerror(errno));
+		goto virusfilter_clamav_scan_return;
+	}
+
+	if (reply[filepath_len] != ':' ||
+	    reply[filepath_len+1] != ' ')
+	{
+		DBG_ERR("clamd: zSCAN: Invalid reply: %s\n",
+			reply);
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(),
+					 "Scanner communication error");
+		goto virusfilter_clamav_scan_return;
+	}
+	reply_msg = reply + filepath_len + 2;
+
+	reply_token = strrchr(reply, ' ');
+
+	if (reply_token == NULL) {
+		DBG_ERR("clamd: zSCAN: Invalid reply: %s\n",
+			reply);
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(),
+					 "Scanner communication error");
+		goto virusfilter_clamav_scan_return;
+	}
+	*reply_token = '\0';
+	reply_token++;
+
+	if (strcmp(reply_token, "OK") == 0) {
+
+		/* <FILEPATH>: OK */
+		result = VIRUSFILTER_RESULT_CLEAN;
+		report = talloc_asprintf(talloc_tos(), "Clean");
+	} else if (strcmp(reply_token, "FOUND") == 0) {
+
+		/* <FILEPATH>: <REPORT> FOUND */
+		result = VIRUSFILTER_RESULT_INFECTED;
+		report = talloc_strdup(talloc_tos(), reply_msg);
+	} else if (strcmp(reply_token, "ERROR") == 0) {
+
+		/* <FILEPATH>: <REPORT> ERROR */
+		DBG_ERR("clamd: zSCAN: Error: %s\n", reply_msg);
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(),
+					 "Scanner error: %s\t", reply_msg);
+	} else {
+		DBG_ERR("clamd: zSCAN: Invalid reply: %s\n", reply_token);
+		result = VIRUSFILTER_RESULT_ERROR;
+		report = talloc_asprintf(talloc_tos(),
+					 "Scanner communication error");
+	}
+
+virusfilter_clamav_scan_return:
+	TALLOC_FREE(reply);
+	if (report == NULL) {
+		*reportp = talloc_asprintf(talloc_tos(),
+					   "Scanner report memory error");
+	} else {
+		*reportp = report;
+	}
+
+	return result;
+}
+
+static struct virusfilter_backend_fns virusfilter_backend_clamav = {
+	.connect = virusfilter_clamav_connect,
+	.disconnect = NULL,
+	.scan_init = virusfilter_clamav_scan_init,
+	.scan = virusfilter_clamav_scan,
+	.scan_end = virusfilter_clamav_scan_end,
+};
+
+int virusfilter_clamav_init(struct virusfilter_config *config)
+{
+	struct virusfilter_backend *backend = NULL;
+
+	if (config->socket_path == NULL) {
+		config->socket_path = VIRUSFILTER_DEFAULT_SOCKET_PATH;
+	}
+
+	backend = talloc_zero(config, struct virusfilter_backend);
+	if (backend == NULL) {
+		return -1;
+	}
+
+	backend->fns = &virusfilter_backend_clamav;
+	backend->name = "clamav";
+
+	config->backend = backend;
+	return 0;
+}
diff --git a/source3/modules/vfs_virusfilter_common.h b/source3/modules/vfs_virusfilter_common.h
index a28ce2978fa..f71b0b949a7 100644
--- a/source3/modules/vfs_virusfilter_common.h
+++ b/source3/modules/vfs_virusfilter_common.h
@@ -148,5 +148,6 @@ struct virusfilter_backend {
 
 int virusfilter_sophos_init(struct virusfilter_config *config);
 int virusfilter_fsav_init(struct virusfilter_config *config);
+int virusfilter_clamav_init(struct virusfilter_config *config);
 
 #endif /* _VIRUSFILTER_COMMON_H */
diff --git a/source3/modules/wscript_build b/source3/modules/wscript_build
index f63c00a9955..5c529890470 100644
--- a/source3/modules/wscript_build
+++ b/source3/modules/wscript_build
@@ -516,6 +516,7 @@ bld.SAMBA3_MODULE('vfs_virusfilter',
                  vfs_virusfilter.c
                  vfs_virusfilter_sophos.c
                  vfs_virusfilter_fsav.c
+                 vfs_virusfilter_clamav.c
                  ''',
                  deps='samba-util VFS_VIRUSFILTER_UTILS',
                  init_function='',
-- 
2.16.0.rc1.238.g530d649a79-goog



More information about the samba-technical mailing list