From 57a81b88f32fff63a48759b048d81a8a51a0482e Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 9 Jun 2021 13:29:31 -0700 Subject: [PATCH 1/4] Use MHD_OPTION_ARRAY to pass extra options to MHD_start_daemon The call to MHD_start_daemon() was duplicated twice, with slightly different options, for ssl vs non-ssl. Use MHD_OPTION_ARRAY so that the same call can be used for both with the ssl option passed via the array. This removes some duplicated code. Right now it is not much, because ssl is the only option. It will make a much difference when there is another option. The current method will require a different call for every possible combination of options. --- webdavd.c | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/webdavd.c b/webdavd.c index e9c288c..7e1a715 100644 --- a/webdavd.c +++ b/webdavd.c @@ -1971,6 +1971,10 @@ static void runServer() { // Start up the daemons daemons = mallocSafe(sizeof(*daemons) * config.daemonCount); for (int i = 0; i < config.daemonCount; i++) { + unsigned int flags = MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DUAL_STACK | MHD_USE_PEDANTIC_CHECKS; + // Addtional options of MHD_start_daemon. Needs terminating MHD_OPTION_END entry. + struct MHD_OptionItem ops[2]; + int nops = 0; struct sockaddr_in6 address; if (getBindAddress(&address, &config.daemons[i])) { MHD_AccessHandlerCallback callback; @@ -1987,25 +1991,17 @@ static void runServer() { config.daemons[i].host ? config.daemons[i].host : "", config.daemons[i].port); continue; } - daemons[i] = MHD_start_daemon( - MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DUAL_STACK | MHD_USE_PEDANTIC_CHECKS - | MHD_USE_SSL, 0 /* ignored */, NULL, NULL, // - callback, &config.daemons[i], // - MHD_OPTION_SOCK_ADDR, &address, // Specifies both host and port - MHD_OPTION_HTTPS_CERT_CALLBACK, &sslSNICallback, // enable ssl - MHD_OPTION_PER_IP_CONNECTION_LIMIT, config.maxConnectionsPerIp, // - MHD_OPTION_END); - } else { - // http - daemons[i] = MHD_start_daemon( - MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DUAL_STACK | MHD_USE_PEDANTIC_CHECKS, - 0 /* ignored */, - NULL, NULL, // - callback, &config.daemons[i], // - MHD_OPTION_SOCK_ADDR, &address, // Specifies both host and port - MHD_OPTION_PER_IP_CONNECTION_LIMIT, config.maxConnectionsPerIp, // - MHD_OPTION_END); + flags |= MHD_USE_SSL; + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_HTTPS_CERT_CALLBACK, 0, &sslSNICallback }; } + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_END }; + + daemons[i] = MHD_start_daemon(flags, 0 /* ignored */, NULL, NULL, + callback, &config.daemons[i], // + MHD_OPTION_SOCK_ADDR, &address, // Specifies both host and port + MHD_OPTION_PER_IP_CONNECTION_LIMIT, config.maxConnectionsPerIp, // + MHD_OPTION_ARRAY, ops, + MHD_OPTION_END); if (!daemons[i]) { stdLogError(errno, "Unable to initialise daemon on port %d", config.daemons[i].port); } From 6656f443189c3636321a4f44984801f28a0763a1 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 9 Jun 2021 23:14:54 -0700 Subject: [PATCH 2/4] Continue when socket can't be used This doesn't really do much of anything. Mostly it changes the flow so a socket bind address failure continues instead having the rest of the loop inside an if statement. So it makes a bunch of code indented one level fewer. The real reason is so the next commit, when the socket address might not be used, doesn't need to do this. That way it doesn't look like a lot of code is being re-written when it's not. --- webdavd.c | 58 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/webdavd.c b/webdavd.c index 7e1a715..7dc6a8b 100644 --- a/webdavd.c +++ b/webdavd.c @@ -1975,36 +1975,40 @@ static void runServer() { // Addtional options of MHD_start_daemon. Needs terminating MHD_OPTION_END entry. struct MHD_OptionItem ops[2]; int nops = 0; + struct sockaddr_in6 address; - if (getBindAddress(&address, &config.daemons[i])) { - MHD_AccessHandlerCallback callback; - if (config.daemons[i].forwardToPort) { - callback = (MHD_AccessHandlerCallback) &answerForwardToRequest; - } else { - callback = (MHD_AccessHandlerCallback) &answerToRequest; - } + if (!getBindAddress(&address, &config.daemons[i])) { + // Already printed error message in getBindAddress + continue; + } - if (config.daemons[i].sslEnabled) { - // https - if (sslCertificateCount == 0) { - stdLogError(0, "No certificates available for ssl %s:%d", - config.daemons[i].host ? config.daemons[i].host : "", config.daemons[i].port); - continue; - } - flags |= MHD_USE_SSL; - ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_HTTPS_CERT_CALLBACK, 0, &sslSNICallback }; - } - ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_END }; - - daemons[i] = MHD_start_daemon(flags, 0 /* ignored */, NULL, NULL, - callback, &config.daemons[i], // - MHD_OPTION_SOCK_ADDR, &address, // Specifies both host and port - MHD_OPTION_PER_IP_CONNECTION_LIMIT, config.maxConnectionsPerIp, // - MHD_OPTION_ARRAY, ops, - MHD_OPTION_END); - if (!daemons[i]) { - stdLogError(errno, "Unable to initialise daemon on port %d", config.daemons[i].port); + MHD_AccessHandlerCallback callback; + if (config.daemons[i].forwardToPort) { + callback = (MHD_AccessHandlerCallback) &answerForwardToRequest; + } else { + callback = (MHD_AccessHandlerCallback) &answerToRequest; + } + + if (config.daemons[i].sslEnabled) { + // https + if (sslCertificateCount == 0) { + stdLogError(0, "No certificates available for ssl %s:%d", + config.daemons[i].host ? config.daemons[i].host : "", config.daemons[i].port); + continue; } + flags |= MHD_USE_SSL; + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_HTTPS_CERT_CALLBACK, 0, &sslSNICallback }; + } + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_END }; + + daemons[i] = MHD_start_daemon(flags, 0 /* ignored */, NULL, NULL, + callback, &config.daemons[i], // + MHD_OPTION_SOCK_ADDR, &address, // Specifies both host and port + MHD_OPTION_PER_IP_CONNECTION_LIMIT, config.maxConnectionsPerIp, // + MHD_OPTION_ARRAY, ops, + MHD_OPTION_END); + if (!daemons[i]) { + stdLogError(errno, "Unable to initialise daemon on port %d", config.daemons[i].port); } } } From 1da09aaa6ee5cd0d22c28bfad60557d1a540b9a9 Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 9 Jun 2021 12:43:32 -0700 Subject: [PATCH 3/4] Add support for socket based activation This allows using with systemd socket units. It works like the old inetd style activation. The socket(s) to open are described in systemd socket units, and when a connection is made, systemd starts webdavd and passes the sockets to it on some file descriptors. This way webdavd doesn't need to be running until it's used. Systemd also supports a lot more options for how it can listen on a socket. E.g., sockets can be bound to a specific interface and UNIX domain sockets can be used. Multiple sockets can be used, e.g. http and https. Systemd will pass all of them to webdavd. libminihttpd supports this already and can take an existing fd as the socket. I use a systemd function that does some handy stuff, so this requires systemd's libraries. So that this isn't a hard dependency, it's protected in the build system by requiring HAVE_SYSTEMD to be set at build time, e.g. make HAVE_SYSTEMD=1. One could add support for the old inetd server, but I didn't since it's not been included with with Fedora for years. --- configuration.c | 17 +++++- configuration.h | 7 ++- makefile | 9 ++- package-with/conf.xml | 20 +++++++ webdavd.c | 128 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 169 insertions(+), 12 deletions(-) diff --git a/configuration.c b/configuration.c index abf59d4..7fce1d5 100644 --- a/configuration.c +++ b/configuration.c @@ -77,6 +77,7 @@ static int readConfigTime(xmlTextReaderPtr reader, time_t * value, const char * static int configListen(WebdavdConfiguration * config, xmlTextReaderPtr reader, const char * configFile) { //80localhostdisabled + //http int index = config->daemonCount++; config->daemons = reallocSafe(config->daemons, sizeof(*config->daemons) * config->daemonCount); memset(&config->daemons[index], 0, sizeof(config->daemons[index])); @@ -90,6 +91,8 @@ static int configListen(WebdavdConfiguration * config, xmlTextReaderPtr reader, result = readConfigInt(reader, &config->daemons[index].port, configFile); } else if (!strcmp(xmlTextReaderConstLocalName(reader), "host")) { result = readConfigString(reader, &config->daemons[index].host); + } else if (!strcmp(xmlTextReaderConstLocalName(reader), "name")) { + result = readConfigString(reader, &config->daemons[index].name); } else if (!strcmp(xmlTextReaderConstLocalName(reader), "encryption")) { const char * encryptionString; result = stepOverText(reader, &encryptionString); @@ -148,7 +151,7 @@ static int configListen(WebdavdConfiguration * config, xmlTextReaderPtr reader, result = stepOver(reader); } } - if (config->daemons[index].port == -1) { + if (!config->socketActivation && config->daemons[index].port == -1) { stdLogError(0, "port not specified for listen in %s", configFile); exit(1); } @@ -282,6 +285,17 @@ static int configUnprotectOptions(WebdavdConfiguration * config, xmlTextReaderPt return result; } +static int configSocketActivation(WebdavdConfiguration * config, xmlTextReaderPtr reader, const char * configFile) { + // +#ifdef HAVE_SYSTEMD + config->socketActivation = true; + return stepOver(reader); +#else + stdLogError(0, "Socket activation support not enable at build time"); + return false; +#endif +} + /////////////////////////// // End Handler Functions // /////////////////////////// @@ -314,6 +328,7 @@ static const ConfigurationFunction configFunctions[] = { { .nodeName = "rap-timeout", .func = &configRapTimeout }, // { .nodeName = "restricted", .func = &configRestricted }, // { .nodeName = "session-timeout", .func = &configSessionTimeout }, // + { .nodeName = "socket-activation", .func = &configSocketActivation }, // { .nodeName = "ssl-cert", .func = &configConfigSSLCert }, // { .nodeName = "static-response-dir", .func = &configResponseDir }, // { .nodeName = "unprotect-options", .func = &configUnprotectOptions } // diff --git a/configuration.h b/configuration.h index 881a02c..704e26c 100644 --- a/configuration.h +++ b/configuration.h @@ -2,6 +2,7 @@ #define WEBDAV_CONFIGURATION_H #include +#include ////////////////////////////////////// // Webdavd Configuration Structures // @@ -9,7 +10,8 @@ typedef struct DaemonConfig { int port; - const char * host; + const char * host; // For opening socket ourselves + const char * name; // For socket activation, i.e. open socket passed to daemon int sslEnabled; int forwardToIsEncrypted; int forwardToPort; @@ -55,6 +57,9 @@ typedef struct WebdavdConfiguration { // OPTIONS Requests int unprotectOptions; + // Use systemd/xinetd style socket based activation + bool socketActivation; + } WebdavdConfiguration; extern WebdavdConfiguration config; diff --git a/makefile b/makefile index 83a9dc5..c12a713 100644 --- a/makefile +++ b/makefile @@ -1,17 +1,22 @@ CFLAGS=-O3 -s STATIC_FLAGS=-Werror -Wall -Wno-pointer-sign -Wno-unused-result -std=gnu99 -pthread +ifdef HAVE_SYSTEMD +DEFS+=-DHAVE_SYSTEMD=1 +LIBSYSTEMD=-lsystemd +endif + all: build/rap build/webdavd ls -lh $^ build/webdavd: build/webdavd.o build/shared.o build/configuration.o build/xml.o - gcc ${CFLAGS} ${STATIC_FLAGS} -o $@ $(filter %.o,$^) -lmicrohttpd -lxml2 -lgnutls -luuid + gcc ${CFLAGS} ${STATIC_FLAGS} -o $@ $(filter %.o,$^) -lmicrohttpd -lxml2 -lgnutls -luuid ${LIBSYSTEMD} build/rap: build/rap.o build/shared.o build/xml.o gcc ${CFLAGS} ${STATIC_FLAGS} -o $@ $(filter %.o,$^) -lpam -lxml2 build/%.o: %.c makefile | build - gcc ${CFLAGS} ${STATIC_FLAGS} -MMD -o $@ $(filter %.c,$^) -I/usr/include/libxml2 -c + gcc ${CFLAGS} ${STATIC_FLAGS} ${DEFS} -MMD -o $@ $(filter %.c,$^) -I/usr/include/libxml2 -c build: mkdir $@ diff --git a/package-with/conf.xml b/package-with/conf.xml index b7bce47..2555f92 100644 --- a/package-with/conf.xml +++ b/package-with/conf.xml @@ -9,6 +9,18 @@ default port. Default host "any ip". Default encryption is "none". The following will listening on all ips and be unencrypted --> + + + 80 @@ -17,6 +29,11 @@ be convertied to an IP before binding. --> + + http + none + https + diff --git a/webdavd.c b/webdavd.c index 7dc6a8b..8315e71 100644 --- a/webdavd.c +++ b/webdavd.c @@ -20,6 +20,10 @@ #include #include +#if HAVE_SYSTEMD +#include +#endif + //////////////// // Structures // //////////////// @@ -90,6 +94,16 @@ typedef struct FDResponseData { RAP * session; } FDResponseData; +// Sockets passed to us by systemd +typedef struct PassedSockets { + int count; + struct { + int fd; + char * name; + bool used; // For us to mark socket as in use + } socket[]; +} PassedSockets; + //////////////////// // End Structures // //////////////////// @@ -1912,6 +1926,61 @@ static void initializeEnvVariables() { else unsetenv("WEBDAVD_CHROOT_PATH"); } +static void freePassedSockets(PassedSockets * ps) { + if (ps) { + for (int i = 0; i < ps->count; i++) + free(ps->socket[i].name); // Allocated by libsystemd + freeSafe(ps); // Allocated by us + } +} + +#if HAVE_SYSTEMD +static void initializeSocketActivation(PassedSockets ** socketsRet) { + if (!config.socketActivation) { + *socketsRet = NULL; + return; + } + + char **names; + // This will set close on exec on all sockets + int n = sd_listen_fds_with_names(1, &names); + if (n <= 0) { + *socketsRet = NULL; + return; + } + PassedSockets * ps = mallocSafe(sizeof(*ps) + sizeof(*ps->socket) * n); + ps->count = n; + + bool error = false; + for (int i = 0; i < n; i++) { + ps->socket[i].fd = SD_LISTEN_FDS_START + i; + ps->socket[i].name = names[i]; // Take ownership of string, need to free on exit + ps->socket[i].used = false; + int ret = sd_is_socket(ps->socket[i].fd, AF_UNSPEC, SOCK_STREAM, true); + if (ret <= 0) { + stdLogError(-ret, "Socket #%d '%s' unstuitable for WebDAV", i, names[i]); + error = true; + } + } + free(names); + + if (error) { + // Just clean up stuct now and refuse to use any sockets + freePassedSockets(ps); + ps = NULL; + } + + *socketsRet = ps; + return; +} +#else +// Only supported with systemd. The old inetd system on stdin could work too, +// but would need some more code to support it. +static void initializeSocketActivation(PassedSockets ** socketsRet) { + *socketsRet = NULL; +} +#endif + //////////////////////// // End Initialisation // //////////////////////// @@ -1960,6 +2029,7 @@ static void runServer() { if (!lockToUser(config.restrictedUser, NULL)) { exit(1); } + PassedSockets *passedSocks; initializeLogs(); initializeStaticResponses(); @@ -1967,19 +2037,58 @@ static void runServer() { initializeLockDB(); initializeSSL(); initializeEnvVariables(); + initializeSocketActivation(&passedSocks); + + if (config.socketActivation && !passedSocks) { + stdLogError(0, "Configured to use systemd-socket activation but no sockets passed"); + return; + } // Start up the daemons daemons = mallocSafe(sizeof(*daemons) * config.daemonCount); for (int i = 0; i < config.daemonCount; i++) { unsigned int flags = MHD_USE_THREAD_PER_CONNECTION | MHD_USE_DUAL_STACK | MHD_USE_PEDANTIC_CHECKS; // Addtional options of MHD_start_daemon. Needs terminating MHD_OPTION_END entry. - struct MHD_OptionItem ops[2]; + struct MHD_OptionItem ops[3]; int nops = 0; - struct sockaddr_in6 address; - if (!getBindAddress(&address, &config.daemons[i])) { - // Already printed error message in getBindAddress - continue; + if (config.socketActivation) { + int listenFd = -1; + // Socket should have been passed to us already + if (config.daemons[i].name) { + // Find by name + for (int j = 0; j < passedSocks->count; j++) { + if (!strcmp(passedSocks->socket[j].name, config.daemons[i].name)) { + if (passedSocks->socket[j].used) { + stdLogError(0, "Socket '%s' for listening port #%d has already been used", config.daemons[i].name, i); + continue; + } + listenFd = passedSocks->socket[j].fd; + passedSocks->socket[j].used = true; + } + } + if (listenFd == -1) { + stdLogError(0, "No socket named '%s' was passed to webdavd by systemd", config.daemons[i].name); + continue; + } + } else { + // Assign sockets to daemons in order listed + if (i >= passedSocks->count || passedSocks->socket[i].used) { + stdLogError(0, "Socket for listening port #%d not passed via socket activation", i); + continue; + } + listenFd = passedSocks->socket[i].fd; + passedSocks->socket[i].used = true; + } + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_LISTEN_SOCKET, (MHD_socket)listenFd }; + } else { + struct sockaddr_in6 address; + if (!getBindAddress(&address, &config.daemons[i])) { + // Already printed error message in getBindAddress + continue; + } + // Specifies both host and port + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_SOCK_ADDR, 0, &address }; } MHD_AccessHandlerCallback callback; @@ -1993,17 +2102,18 @@ static void runServer() { // https if (sslCertificateCount == 0) { stdLogError(0, "No certificates available for ssl %s:%d", - config.daemons[i].host ? config.daemons[i].host : "", config.daemons[i].port); + config.daemons[i].name ? config.daemons[i].name : + config.daemons[i].host ? config.daemons[i].host : "", + config.daemons[i].port); continue; } flags |= MHD_USE_SSL; ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_HTTPS_CERT_CALLBACK, 0, &sslSNICallback }; } - ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_END }; + ops[nops++] = (struct MHD_OptionItem){ MHD_OPTION_END }; daemons[i] = MHD_start_daemon(flags, 0 /* ignored */, NULL, NULL, callback, &config.daemons[i], // - MHD_OPTION_SOCK_ADDR, &address, // Specifies both host and port MHD_OPTION_PER_IP_CONNECTION_LIMIT, config.maxConnectionsPerIp, // MHD_OPTION_ARRAY, ops, MHD_OPTION_END); @@ -2011,6 +2121,8 @@ static void runServer() { stdLogError(errno, "Unable to initialise daemon on port %d", config.daemons[i].port); } } + + freePassedSockets(passedSocks); } int main(int argCount, char ** args) { From 270f57c40f7de535bcffa2fe44fcb4b968b3e9ae Mon Sep 17 00:00:00 2001 From: Trent Piepho Date: Wed, 9 Jun 2021 20:18:16 -0700 Subject: [PATCH 4/4] Add socket units and package with RPM This enables the systemd socket activation support when building the RPM. It includes two sample socket unit files. They will start webdavd if there is a connection on either the http or https port. They can be used by starting/enabling the socket units instead of the service unit. It's possible to enable either or both of the socket units. and also --- package-control/webdavd.spec | 5 ++++- package-with/webdavd-http.socket | 14 ++++++++++++++ package-with/webdavd-https.socket | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 package-with/webdavd-http.socket create mode 100644 package-with/webdavd-https.socket diff --git a/package-control/webdavd.spec b/package-control/webdavd.spec index fc356e5..430ccf4 100644 --- a/package-control/webdavd.spec +++ b/package-control/webdavd.spec @@ -13,6 +13,7 @@ BuildRequires: libxml2-devel BuildRequires: pam-devel BuildRequires: libuuid-devel BuildRequires: make +BuildRequires: systemd-devel Requires: gnutls Requires: libmicrohttpd @@ -36,7 +37,7 @@ webdavd is a WebDAV server designed to be a replace for SMBA providing access to %setup -n WebDAV-Daemon-%{version} %build -%make_build +%make_build HAVE_SYSTEMD=1 %install install -Dpm 755 build/webdavd %{buildroot}%{_sbindir}/webdavd @@ -46,6 +47,7 @@ install -Dpm 644 package-with/conf.xml %{buildroot}%{_sysconfdir}/webdavd install -d %{buildroot}%{_datadir}/webdavd install -Dpm 644 package-with/share/* %{buildroot}%{_datadir}/webdavd install -Dpm 644 package-with/systemd.service %{buildroot}%{_prefix}/lib/systemd/system/webdavd.service +install -Dpm 644 package-with/webdavd-*.socket %{buildroot}%{_prefix}/lib/systemd/system install -Dpm 644 package-with/logrotate.conf %{buildroot}%{_sysconfdir}/logrotate.d/webdavd @@ -57,6 +59,7 @@ install -Dpm 644 package-with/logrotate.conf %{buildroot}%{_sysconfdir}/logrotat %{_prefix}/lib/webdavd/webdav-worker %{_datadir}/webdavd/* %{_prefix}/lib/systemd/system/webdavd.service +%{_prefix}/lib/systemd/system/webdavd-*.socket %config %{_sysconfdir}/pam.d/webdavd %config %{_sysconfdir}/webdavd %config %{_sysconfdir}/logrotate.d/webdavd diff --git a/package-with/webdavd-http.socket b/package-with/webdavd-http.socket new file mode 100644 index 0000000..c446cbb --- /dev/null +++ b/package-with/webdavd-http.socket @@ -0,0 +1,14 @@ +[Unit] +Description=WebDAV server on HTTP socket + +[Socket] +Accept=no +ListenStream=80 +FileDescriptorName=http +DeferAcceptSec=1 +Service=webdavd.service +# Add this to only allow connections over localhost +# BindToDevice=lo + +[Install] +WantedBy=sockets.target diff --git a/package-with/webdavd-https.socket b/package-with/webdavd-https.socket new file mode 100644 index 0000000..6ea5108 --- /dev/null +++ b/package-with/webdavd-https.socket @@ -0,0 +1,14 @@ +[Unit] +Description=WebDAV server on HTTPS socket + +[Socket] +Accept=no +ListenStream=443 +FileDescriptorName=https +DeferAcceptSec=1 +Service=webdavd.service +# Add this to only allow connections over localhost +# BindToDevice=lo + +[Install] +WantedBy=sockets.target