Add a new mode to the file plugin, using directory=DIR instead of
[file=]FILE, to allow it to serve all regular/block files in a given
directory, as well as advertising the names of those files it will be
serving.
Signed-off-by: Eric Blake <eblake(a)redhat.com>
---
plugins/file/nbdkit-file-plugin.pod | 28 +++++-
tests/Makefile.am | 4 +-
plugins/file/file.c | 119 +++++++++++++++++++----
tests/test-file-dir.sh | 143 ++++++++++++++++++++++++++++
4 files changed, 271 insertions(+), 23 deletions(-)
create mode 100755 tests/test-file-dir.sh
diff --git a/plugins/file/nbdkit-file-plugin.pod b/plugins/file/nbdkit-file-plugin.pod
index dac673ae..f9ae6e97 100644
--- a/plugins/file/nbdkit-file-plugin.pod
+++ b/plugins/file/nbdkit-file-plugin.pod
@@ -6,27 +6,47 @@ nbdkit-file-plugin - nbdkit file plugin
nbdkit file [file=]FILENAME
+ nbdkit file directory=DIRNAME
+
=head1 DESCRIPTION
C<nbdkit-file-plugin> is a file serving plugin for L<nbdkit(1)>.
It serves the named C<FILENAME> over NBD. Local block devices
-(eg. F</dev/sda>) may also be served.
+(eg. F</dev/sda>) may also be served. It may also be used to serve
+any file within a given C<DIRECTORY>, according to which export name
+the guest requests.
=head1 PARAMETERS
+One of B<file> or B<directory> must be given to determine which mode
+the server will use.
+
=over 4
=item [B<file=>]FILENAME
Serve the file named C<FILENAME>. A local block device name can also
-be used here.
-
-This parameter is required.
+be used here. When this mode is used, the export name requested by
+the client is ignored.
C<file=> is a magic config key and may be omitted in most cases.
See L<nbdkit(1)/Magic parameters>.
+=item B<directory=>DIRNAME
+
+(nbdkt E<ge> 1.22)
+
+Serve all regular files and block devices located directly within the
+directory named C<DIRNAME>, including those found by following
+symbolic links. Other special files in the directory (such as
+subdirectories, fifos, or Unix sockets) are ignored. When this mode
+is used, the file to be served is chosen by the export name passed by
+the client; a client that requests the default export (C<"">) will be
+served whichever file appears first in the L<readdir(3)> listing. For
+security, when using directory mode, this plugin will not accept
+export names containing slash (C</>).
+
=back
=head1 NOTES
diff --git a/tests/Makefile.am b/tests/Makefile.am
index b5ef96a7..d9074ba9 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -636,8 +636,8 @@ test_file_block_SOURCES = test-file-block.c test.h
test_file_block_CFLAGS = $(WARNINGS_CFLAGS) $(LIBGUESTFS_CFLAGS)
test_file_block_LDADD = libtest.la $(LIBGUESTFS_LIBS)
-TESTS += test-file-extents.sh
-EXTRA_DIST += test-file-extents.sh
+TESTS += test-file-extents.sh test-file-dir.sh
+EXTRA_DIST += test-file-extents.sh test-file-dir.sh
# floppy plugin test.
TESTS += test-floppy.sh
diff --git a/plugins/file/file.c b/plugins/file/file.c
index e049864a..4afcad11 100644
--- a/plugins/file/file.c
+++ b/plugins/file/file.c
@@ -43,6 +43,7 @@
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <errno.h>
+#include <dirent.h>
#include <pthread.h>
@@ -66,9 +67,11 @@
#endif
static char *filename = NULL;
+static char *directory = NULL;
+DIR *dir = NULL;
-/* Any callbacks using lseek must be protected by this lock. */
-static pthread_mutex_t lseek_lock = PTHREAD_MUTEX_INITIALIZER;
+/* Any callbacks using readdir or lseek must be protected by this lock. */
+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
/* to enable: -D file.zero=1 */
int file_debug_zero;
@@ -83,10 +86,14 @@ static void
file_unload (void)
{
free (filename);
+ free (directory);
+ if (dir)
+ closedir (dir);
}
/* Called for each key=value passed on the command line. This plugin
- * only accepts file=<filename>, which is required.
+ * only accepts file=<filename> and directory=<dirname>, where exactly
+ * one is required.
*/
static int
file_config (const char *key, const char *value)
@@ -98,6 +105,12 @@ file_config (const char *key, const char *value)
if (!filename)
return -1;
}
+ else if (strcmp (key, "directory") == 0) {
+ free (directory);
+ directory = nbdkit_realpath (value);
+ if (!directory)
+ return -1;
+ }
else if (strcmp (key, "rdelay") == 0 ||
strcmp (key, "wdelay") == 0) {
nbdkit_error ("add --filter=delay on the command line");
@@ -111,13 +124,19 @@ file_config (const char *key, const char *value)
return 0;
}
-/* Check the user did pass a file=<FILENAME> parameter. */
+/* Check the user did pass exactly one parameter. */
static int
file_config_complete (void)
{
- if (filename == NULL) {
- nbdkit_error ("you must supply the file=<FILENAME> parameter "
- "after the plugin name on the command line");
+ if (!filename == !directory) {
+ nbdkit_error ("you must supply exactly one file=<FILENAME> or "
+ "directory=<DIRNAME> parameter after the plugin name "
+ "on the command line");
+ return -1;
+ }
+
+ if (directory && (dir = opendir (directory)) == NULL) {
+ nbdkit_error ("opendir: %m");
return -1;
}
@@ -125,7 +144,8 @@ file_config_complete (void)
}
#define file_config_help \
- "file=<FILENAME> (required) The filename to serve." \
+ "file=<FILENAME> The filename to serve." \
+ "directory=<DIRNAME> A directory containing files to serve." \
/* Print some extra information about how the plugin was compiled. */
static void
@@ -145,8 +165,47 @@ file_dump_plugin (void)
#endif
}
+static int file_list_exports (int readonly, int default_only,
+ struct nbdkit_exports *exports)
+{
+ struct dirent *entry;
+ struct stat sb;
+ int fd;
+
+ if (!directory)
+ return nbdkit_add_export (exports, "", NULL);
+
+ ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
+ rewinddir (dir);
+ fd = dirfd (dir);
+ if (fd == -1) {
+ nbdkit_error ("dirfd: %m");
+ return -1;
+ }
+ errno = 0;
+ while ((entry = readdir (dir)) != NULL) {
+ /* TODO: Optimize with d_type and/or statx when present? */
+ if (fstatat (fd, entry->d_name, &sb, 0) == 0 &&
+ (S_ISREG (sb.st_mode) || S_ISBLK (sb.st_mode))) {
+ if (nbdkit_add_export (exports, entry->d_name, NULL) == -1) {
+ close (fd);
+ return -1;
+ }
+ }
+ errno = 0;
+ }
+ if (errno) {
+ nbdkit_error ("readdir: %m");
+ close (fd);
+ return -1;
+ }
+ close (fd);
+ return 0;
+}
+
/* The per-connection handle. */
struct handle {
+ char *file;
int fd;
bool is_block_device;
int sector_size;
@@ -170,21 +229,44 @@ file_open (int readonly)
return NULL;
}
+ if (directory) {
+ const char *exportname = nbdkit_export_name ();
+
+ if (strchr (exportname, '/')) {
+ nbdkit_error ("exportname cannot contain /");
+ errno = EINVAL;
+ free (h);
+ return NULL;
+ }
+ if (asprintf (&h->file, "%s/%s", directory, exportname) == -1) {
+ nbdkit_error ("asprintf: %m");
+ free (h);
+ return NULL;
+ }
+ }
+ else
+ h->file = strdup (filename);
+ if (h->file == NULL) {
+ nbdkit_error ("malloc: %m");
+ free (h);
+ return NULL;
+ }
+
flags = O_CLOEXEC|O_NOCTTY;
if (readonly)
flags |= O_RDONLY;
else
flags |= O_RDWR;
- h->fd = open (filename, flags);
+ h->fd = open (h->file, flags);
if (h->fd == -1) {
- nbdkit_error ("open: %s: %m", filename);
+ nbdkit_error ("open: %s: %m", h->file);
free (h);
return NULL;
}
if (fstat (h->fd, &statbuf) == -1) {
- nbdkit_error ("fstat: %s: %m", filename);
+ nbdkit_error ("fstat: %s: %m", h->file);
free (h);
return NULL;
}
@@ -194,7 +276,8 @@ file_open (int readonly)
else if (S_ISREG (statbuf.st_mode))
h->is_block_device = false;
else {
- nbdkit_error ("file is not regular or block device: %s", filename);
+ nbdkit_error ("file is not regular or block device: %s", h->file);
+ free (h->file);
close (h->fd);
free (h);
return NULL;
@@ -204,7 +287,7 @@ file_open (int readonly)
#ifdef BLKSSZGET
if (h->is_block_device) {
if (ioctl (h->fd, BLKSSZGET, &h->sector_size))
- nbdkit_debug ("cannot get sector size: %s: %m", filename);
+ nbdkit_debug ("cannot get sector size: %s: %m", h->file);
}
#endif
@@ -232,6 +315,7 @@ file_close (void *handle)
{
struct handle *h = handle;
+ free (h->file);
close (h->fd);
free (h);
}
@@ -239,7 +323,7 @@ file_close (void *handle)
#define THREAD_MODEL NBDKIT_THREAD_MODEL_PARALLEL
/* For block devices, stat->st_size is not the true size. The caller
- * grabs the lseek_lock.
+ * grabs the lock.
*/
static int64_t
block_device_size (int fd)
@@ -262,7 +346,7 @@ file_get_size (void *handle)
struct handle *h = handle;
if (h->is_block_device) {
- ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lseek_lock);
+ ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
return block_device_size (h->fd);
} else {
/* Regular file. */
@@ -554,7 +638,7 @@ file_can_extents (void *handle)
/* A simple test to see whether SEEK_HOLE etc is likely to work on
* the current filesystem.
*/
- ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lseek_lock);
+ ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
r = lseek (h->fd, 0, SEEK_HOLE);
if (r == -1) {
nbdkit_debug ("extents disabled: lseek: SEEK_HOLE: %m");
@@ -628,7 +712,7 @@ static int
file_extents (void *handle, uint32_t count, uint64_t offset,
uint32_t flags, struct nbdkit_extents *extents)
{
- ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lseek_lock);
+ ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
return do_extents (handle, count, offset, flags, extents);
}
#endif /* SEEK_HOLE */
@@ -662,6 +746,7 @@ static struct nbdkit_plugin plugin = {
.config_help = file_config_help,
.magic_config_key = "file",
.dump_plugin = file_dump_plugin,
+ .list_exports = file_list_exports,
.open = file_open,
.close = file_close,
.get_size = file_get_size,
diff --git a/tests/test-file-dir.sh b/tests/test-file-dir.sh
new file mode 100755
index 00000000..efe3b6cd
--- /dev/null
+++ b/tests/test-file-dir.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+# nbdkit
+# Copyright (C) 2018-2020 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of Red Hat nor the names of its contributors may be
+# used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+# Test the use of the directory mode of the file plugin.
+
+source ./functions.sh
+set -e
+set -x
+
+# Hack: This rejects libnbd 1.3.9, but we really need libnbd >= 1.3.11,
+# which does not have its own decent witness...
+requires nbdsh -c 'print (h.get_list_export_description)'
+
+requires nbdinfo --version
+requires jq --version
+
+files="file-dir file-dir.out file-dir.witness"
+rm -rf $files
+cleanup_fn rm -rf $files
+fail=0
+
+# do_nbdkit_list [--no-sort] EXPOUT
+# Check that the advertised list of exports matches EXPOUT
+do_nbdkit_list ()
+{
+ sort=' | sort'
+ if [ "$1" = --no-sort ]; then
+ sort=
+ shift
+ fi
+ nbdkit -U - -v file directory=file-dir \
+ --run 'nbdinfo --list --json "$uri"' >file-dir.out
+ cat file-dir.out
+ diff -u <(jq -c '[.exports[]."export-name"]'"$sort"
file-dir.out) \
+ <(printf %s\\n "$1") || fail=1
+}
+
+# do_nbdkit_fail NAME
+# Check that attempting to connect to export NAME fails
+do_nbdkit_fail ()
+{
+ # The --run script occurs only if nbdkit gets past .config_complete;
+ # testing for witness proves that our failure was during .open and
+ # not at some earlier point
+ rm -f file-dir.witness
+ nbdkit -U - -v -e "$1" file directory=file-dir \
+ --run 'touch file-dir.witness; nbdsh -u "$uri" -c
"quit()"' && fail=1
+ test -f file-dir.witness || fail=1
+}
+
+# do_nbdkit_pass NAME DATA
+# Check that export NAME serves DATA as its first byte
+do_nbdkit_pass ()
+{
+ out=$(nbdkit -U - -v -e "$1" file directory=file-dir \
+ --run 'nbdsh -u "$uri" -c "print (h.pread (1, 0).decode
(\"utf-8\"))"')
+ test "$out" = "$2" || fail=1
+}
+
+# Not possible to serve a missing directory
+nbdkit -vf file directory=nosuchdir && fail=1
+
+# Serving an empty directory
+mkdir file-dir
+do_nbdkit_list '[]'
+do_nbdkit_fail ''
+do_nbdkit_fail 'a'
+do_nbdkit_fail '..'
+do_nbdkit_fail '/'
+
+# Serving a directory with one file
+echo 1 > file-dir/a
+do_nbdkit_list '["a"]'
+do_nbdkit_pass '' 1
+do_nbdkit_pass a 1
+do_nbdkit_fail b
+
+# Serving a directory with multiple files.
+# Use 'find' to match readdir's raw order (a is not always first!)
+echo 2 > file-dir/b
+raw=$(find file-dir -type f | xargs echo)
+exp=$(echo $raw | sed 's,file-dir/\(.\),"\1",g; s/ /,/')
+do_nbdkit_list --no-sort "[$exp]"
+do_nbdkit_list '["a","b"]'
+case $raw in
+ file-dir/a*) byte=1 ;;
+ file-dir/b*) byte=2 ;;
+ *) fail=1 ;;
+esac
+do_nbdkit_pass '' $byte
+do_nbdkit_pass 'a' 1
+do_nbdkit_pass 'b' 2
+do_nbdkit_fail 'c'
+
+# Serving a directory with non-regular files
+ln -s b file-dir/c
+mkfifo file-dir/d
+mkdir file-dir/e
+ln -s /dev/null file-dir/f
+ln -s . file-dir/g
+ln -s dangling file-dir/h
+do_nbdkit_list '["a","b","c"]'
+do_nbdkit_pass 'a' 1
+do_nbdkit_pass 'b' 2
+do_nbdkit_pass 'c' 2
+do_nbdkit_fail 'd'
+do_nbdkit_fail 'e'
+do_nbdkit_fail 'f'
+do_nbdkit_fail 'g'
+do_nbdkit_fail 'h'
+do_nbdkit_fail './a'
+do_nbdkit_fail '../file-dir/a'
+
+exit $fail
--
2.28.0