From: Fredrik Tolf Date: Thu, 23 Nov 2023 18:53:06 +0000 (+0100) Subject: etc: Add environment option to run init.d/ashd silently. X-Git-Url: http://www.dolda2000.com/gitweb/?p=ashd.git;a=commitdiff_plain;h=HEAD;hp=6e8b9b9d6d043ecbcaeb8ef807bd9648424aedd7 etc: Add environment option to run init.d/ashd silently. --- diff --git a/ChangeLog b/ChangeLog index d8c848a..c075a84 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,11 +1,11 @@ Version 0.13: - * FreeBSD support. + * Partial FreeBSD support. * Support upgrading connections to full-duplex, for eg. websockets. * More useful dirplex/patplex configuration options. * Improved behavior under overload conditions. * Expanded accesslog's logging capabilities. - * Added httrcall. + * Added httrcall and htpipe. * Quite a slew of random bugfixes and implementation improvements. Version 0.12: diff --git a/configure.in b/configure.ac similarity index 78% rename from configure.in rename to configure.ac index 80d2389..5f2d06b 100644 --- a/configure.in +++ b/configure.ac @@ -1,7 +1,7 @@ AC_INIT([ashd], [0.13]) AC_CONFIG_SRCDIR(src/htparser.c) AM_INIT_AUTOMAKE -AM_CONFIG_HEADER(config.h) +AC_CONFIG_HEADERS(config.h) AC_USE_SYSTEM_EXTENSIONS AC_PROG_CC @@ -9,11 +9,6 @@ AM_PROG_CC_C_O AC_PROG_INSTALL AC_PROG_RANLIB -dnl Add for libtool: -dnl AM_PROG_LIBTOOL - -AC_HEADER_STDC - HAS_MAGIC=yes AC_CHECK_LIB(magic, magic_open, [:], [HAS_MAGIC=no]) AC_CHECK_HEADER(magic.h, [], [HAS_MAGIC=no]) @@ -102,8 +97,12 @@ fi AC_SUBST(XATTR_LIBS) AH_TEMPLATE(HAVE_GNUTLS, [define to use the GnuTLS library for SSL support]) +AH_TEMPLATE(HAVE_OPENSSL, [define to use the OpenSSL library for SSL support]) AC_ARG_WITH(gnutls, AS_HELP_STRING([--with-gnutls], [enable SSL support with the GnuTLS library])) +AC_ARG_WITH(openssl, AS_HELP_STRING([--with-openssl], [enable SSL support with the OpenSSL library])) HAS_GNUTLS="" +HAS_OPENSSL="" + if test "$with_gnutls" = no; then HAS_GNUTLS=no; fi if test -z "$HAS_GNUTLS"; then AC_CHECK_LIB(gnutls, gnutls_global_init, [:], [HAS_GNUTLS=no]) @@ -123,10 +122,41 @@ fi AC_SUBST(GNUTLS_CPPFLAGS) AC_SUBST(GNUTLS_LIBS) -AC_OUTPUT([ +if test "$with_openssl" = no; then + HAS_OPENSSL=no +elif test -z "$with_openssl" -a "$HAS_GNUTLS" = yes; then + HAS_OPENSSL=no +fi +if test -z "$HAS_OPENSSL"; then + AC_CHECK_LIB(ssl, SSL_CTX_new, [:], [HAS_OPENSSL=no]) +fi +if test -z "$HAS_OPENSSL"; then + AC_CHECK_LIB(crypto, ERR_get_error, [:], [HAS_OPENSSL=no]) +fi +if test -z "$HAS_OPENSSL"; then + AC_CHECK_HEADER(openssl/ssl.h, [], [HAS_OPENSSL=no]) +fi +if test "$HAS_OPENSSL" != no; then HAS_OPENSSL=yes; fi +if test "$with_openssl" = yes -a "$HAS_OPENSSL" = no; then + AC_MSG_ERROR([*** cannot find OpenSSL on this system]) +fi +if test "$HAS_OPENSSL" = yes; then + OPENSSL_LIBS="-lssl -lcrypto" + GNUTLS_CPPFLAGS= + AC_DEFINE(HAVE_OPENSSL) +fi +AC_SUBST(OPENSSL_CPPFLAGS) +AC_SUBST(OPENSSL_LIBS) + +if test "$HAS_GNUTLS" = yes -a "$HAS_OPENSSL" = yes; then + AC_MSG_ERROR([*** only one of GnuTLS and OpenSSL must be enabled]) +fi + +AC_CONFIG_FILES([ Makefile src/Makefile src/dirplex/Makefile lib/Makefile doc/Makefile ]) +AC_OUTPUT diff --git a/doc/Makefile.am b/doc/Makefile.am index baf94f3..9429bfb 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -1,7 +1,7 @@ dist_man1_MANS = callcgi.1 dirplex.1 htparser.1 patplex.1 sendfile.1 \ userplex.1 htls.1 callscgi.1 accesslog.1 htextauth.1 \ callfcgi.1 multifscgi.1 errlogger.1 httimed.1 \ - psendfile.1 httrcall.1 + psendfile.1 httrcall.1 htpipe.1 dist_man7_MANS = ashd.7 diff --git a/doc/accesslog.doc b/doc/accesslog.doc index 2baf508..7a8e8e9 100644 --- a/doc/accesslog.doc +++ b/doc/accesslog.doc @@ -199,6 +199,17 @@ instead expand into a dash. Expands into the time it took for the handler to complete the response, expressed as seconds with 6 decimals precision. +*%{*'HEADER'*}p*:: + + Expands into the HTTP response header named by 'HEADER'. If + the specified header does not exist in the request, *%p* + expands into a dash. + +*%{*'HEADER'*}P*:: + + Similar to *%p*, except that 'HEADER' is prepended with + `X-Ash-`, for simple convenience. + In any expanded field, any "unsafe" characters are escaped. Currently, this means that double-quotes and backslashes are prepended with a backslash, newlines and tabs are expressed as, respectively, `\n` and diff --git a/doc/dirplex.doc b/doc/dirplex.doc index 8a0184f..c18ca14 100644 --- a/doc/dirplex.doc +++ b/doc/dirplex.doc @@ -148,6 +148,19 @@ The following configuration directives are recognized: may be given to turn off index file searching completely. The *index-file* directive accepts no follow-up lines. +*dot-allow* ['PATTERN'...]:: + + As described under 404 RESPONSES, a path element beginning + with a dot character is normally rejected by default, but the + *dot-allow* directive allows certain dot-files or -directories + to be selectively allowed. Each 'PATTERN' is an ordinary glob + pattern, the matching of which allows access to a given path + element. When checking for access to dot-files or + -directories, only the *dot-allow* directive "closest" to the + file under consideration is used. It should be noted that the + default configuration file for *dirplex* contains a + *dot-allow* directive for the `.well-known` directory. + *child* 'NAME':: Declares a named, persistent request handler (see *ashd*(7) diff --git a/doc/htparser.doc b/doc/htparser.doc index 928e642..f400a62 100644 --- a/doc/htparser.doc +++ b/doc/htparser.doc @@ -81,6 +81,9 @@ OPTIONS After having daemonized, write the PID of the new process to 'PIDFILE'. +If the *-u*, *-r* or *-p* option is presented with an empty argument, +it will be treated as if the option had not been given. + SIGNALS ------- @@ -92,6 +95,31 @@ SIGTERM, SIGINT:: connections open for keep-alive. Upon second reception, `htparser` shuts down completely. +PID-FILE PROTOCOL +----------------- + +If the *-p* option is used to create a PID file, `htparser` will +follow a simple protocol to allow state monitoring for clean shutdown +purposes. When `htparser` is signalled to terminate, as described +under SIGNALS, then it appends a single newline at the end of the PID +file. Once all outstanding connections have been terminated, then +`htparser` will truncate the PID file to zero size just prior to +exiting. Thus, init scripts or other state-monitoring tools can know +that `htparser` is serving remaining connections as long as the PID +file contains two lines (the last of which is empty). + +Further, when `htparser` starts, it does not overwrite the contents of +an existing PID file, but rather creates a new file, replacing the old +file. Thus, if a new instance of `htparser` is started while a +previous instance is still running (or serving remaining connections), +the PID file for the new instance will not be truncated when the +previous instance terminates. + +The reason for the somewhat unorthodox protocol is that it works by +simply keeping the PID file open in the running process, allowing the +protocol to work without pathnames, and therefore even if `htparser` +is instructed to change root directory with the *-r* option. + EXAMPLES -------- diff --git a/doc/htpipe.doc b/doc/htpipe.doc new file mode 100644 index 0000000..cf90e5f --- /dev/null +++ b/doc/htpipe.doc @@ -0,0 +1,71 @@ +htpipe(1) +========== + +NAME +---- +htpipe - Ashd decoupler pipe + +SYNOPSIS +-------- +*htpipe* [*-h*] [*-CS*] 'SOCKET-PATH' ['CHILD' ['ARGS'...]] + +DESCRIPTION +----------- + +The *htpipe* handler implements piping of *ashd*(7) requests over a +named Unix socket. The main reason for doing so would be to isolate an +*ashd*(7) handler program from the effects of restarting its parent +process. In particular, it is useful for isolating the entire handler +process tree from restarting *htparser*(1), which may be done +particularly often for such reasons as certificate renewal. + +If the *-S* flag is given to start *htpipe* in server mode, *htpipe* +will start listening to a socket named by 'SOCKET-PATH', start a +handler program as specified by 'CHILD' and 'ARGS', accept connections +on said socket, and pass requests received on such connections to the +handler program. It can handle an arbitrary amount of such +connections, but it is not implemented for high performance with a +large number of connections. It is expected that the ordinary case +will involve very few connections, usually only one at a time. + +If the *-C* flag is given to start *htpipe* in client mode, *htpipe* +will connect to the socket named by 'SOCKET-PATH', accept requests +from standard input, and pass such requests over said socket, +presumably to an *htpipe* instance running in server mode on the other +end. + +By default, when neither the *-S* nor the *-C* option is given, +*htpipe* will attempt to connect to the socket named by 'SOCKET-PATH' +and, if that succeeds, go on running in client mode, just as if the +*-C* option had been given. If, however, the connection fails, it will +fork a copy of itself to run in server mode, and then reconnect to the +socket presumably created by that new process. + +*htpipe* is a persistent handler, as defined in *ashd*(7), and the +specified child handler must also be a persistent handler. + +OPTIONS +------- + +*-h*:: + + Print a brief help message to standard output and exit. + +*-C*:: + + Run in dedicated client mode, as described above. In this + mode, the 'CHILD' argument does not need to be given. + +*-S*:: + + Run in dedicated server mode, as described above. In this + mode, as well as when neither the *-C* nor the *-S* option + were given, the 'CHILD' argument is mandatory. + +AUTHOR +------ +Fredrik Tolf + +SEE ALSO +-------- +*ashd*(7) diff --git a/doc/httrcall.doc b/doc/httrcall.doc index 555284e..514ae6f 100644 --- a/doc/httrcall.doc +++ b/doc/httrcall.doc @@ -32,11 +32,11 @@ OPTIONS *-l* 'LIMIT':: If specified, only 'LIMIT' copies of the handler program are - allowed to run at one time. If furter request are received + allowed to run at one time. If further requests are received while 'LIMIT' children are running, *httrcall* will block, waiting for one to exit before processing further requests. If - no *-l* option is specified, *httrcall* imposes no limit on - the number of children that may run. + no *-l* option is specified (or specified as zero), *httrcall* + imposes no limit on the number of children that may run. AUTHOR ------ diff --git a/doc/patplex.doc b/doc/patplex.doc index 86d6ace..a9ebe69 100644 --- a/doc/patplex.doc +++ b/doc/patplex.doc @@ -92,23 +92,49 @@ rules are recognized: 'HEADER', the rule never matches. If 'FLAGS' contain the character `i`, 'REGEX' is matched case-independently. +*all*:: + + The rule always matches. + *default*:: - Matches if and only if no *match* stanza without a *default* - rule has matched. + Convenient shorthand for an *all* line followed by *priority + -10* (see below). In addition to the rules, a *match* stanza must contain exactly one -follow-up line specifying the action to take if it matches. Currently, -only the *handler* action is recognized: +follow-up line specifying the action to take if it matches. The +following actions are supported: *handler* 'HANDLER':: 'HANDLER' must be a named handler as declared by a *child* or *fchild* stanza, to which the request is passed. +*reparse*:: + + Apply any side-effects as required by the match stanza (such + as rest-string or header modifications), and then retry the + matching of the request. During the rematching, the stanza + containing the *reparse* action will be disabled. Multiple + *reparse* stanzas may be used recursively. + Additionally, a *match* stanza may contain any of the following, optional lines: +*priority* 'INTEGER':: + + Specifies the priority for the match stanza. In case more than + one stanza match a request, the one with the highest priority + is used. In case more than one stanza with the same highest + priority match a request, it is unspecified which will be + used. Match stanzas without a priority specification will + default to priority 0. Either positive or negative priorities + may be specified. + +*order* 'INTEGER':: + + A synonym for *priority*. Use whichever you like the best. + *set* 'HEADER' 'VALUE':: If the *match* stanza as a whole matches, the named HTTP diff --git a/doc/userplex.doc b/doc/userplex.doc index 1269073..db59dd9 100644 --- a/doc/userplex.doc +++ b/doc/userplex.doc @@ -99,6 +99,8 @@ LOGIN As part of the login procedure, *userplex* does the following: + * A new session is entered (with *setsid*(2)). + * The UID, GID and auxiliary groups of the new process are changed accordingly. For testing purposes, *userplex* may be running as a user other than root, and the child process will, then, simply exit diff --git a/etc/ashd/dirplex.rc b/etc/ashd/dirplex.rc index 9d46abd..cefd197 100644 --- a/etc/ashd/dirplex.rc +++ b/etc/ashd/dirplex.rc @@ -1,3 +1,4 @@ -include dirplex.d/*.rc - index-file index +dot-allow .well-known + +include dirplex.d/*.rc diff --git a/etc/debian/init.d-ashd b/etc/debian/init.d-ashd index 9fd1e42..d3738a1 100755 --- a/etc/debian/init.d-ashd +++ b/etc/debian/init.d-ashd @@ -15,6 +15,9 @@ set -e PATH=/usr/local/bin:/usr/local/sbin:$PATH HTPARSER="$(which htparser || true)" PIDFILE=/var/run/ashd.pid +GRACE_PERIOD=10 +USER=nobody +CHROOT=/var/tmp PORTSPEC="plain" ROOTSPEC="dirplex /srv/www" [ -r /etc/default/locale ] && . /etc/default/locale @@ -23,15 +26,82 @@ ROOTSPEC="dirplex /srv/www" start() { export LANG - log_daemon_msg "Starting HTTP server" "ashd" - start-stop-daemon -S -p "$PIDFILE" -qx "$HTPARSER" -- -Sf -p "$PIDFILE" -u nobody -r /var/tmp $PORTSPEC -- $ROOTSPEC - log_end_msg $? + [ -n "$SILENT_INIT" ] || log_daemon_msg "Starting HTTP server" "ashd" + if start-stop-daemon -S -p "$PIDFILE" -qa "$HTPARSER" -- -Sf -p "$PIDFILE" -u "$USER" -r "$CHROOT" $PORTSPEC -- $ROOTSPEC; then + [ -n "$SILENT_INIT" ] || log_success_msg + else + [ -n "$SILENT_INIT" ] || log_end_msg 1 + fi } -stop() { - log_daemon_msg "Stopping HTTP server" "ashd" +kill_wholly() { start-stop-daemon -K -p "$PIDFILE" -qx "$HTPARSER" - log_end_msg $? +} + +kill_listen() { + pid=$(cat "$PIDFILE" 2>/dev/null || true) + if [ -z "$pid" ]; then + log_failure_msg "no pid file" + return 1 + fi + if ! kill -0 "$pid"; then + log_failure_msg "invalid saved pid" + return 1 + fi + [ -n "$SILENT_INIT" ] || log_progress_msg "listen" + kill -TERM "$pid" + for try in 0 1 2 3 4 5; do + sleep $try + case "$(wc -l <"$PIDFILE")" in + 1) continue ;; + 0|2) return 0 ;; + *) + log_failure_msg "could not parse pid file" + return 1 + ;; + esac + done + log_failure_msg "htparser did not stop listening, killing it completely" + kill_wholly + start-stop-daemon -K -p "$PIDFILE" -qx "$HTPARSER" + return 1 +} + +stop_listen() { + [ -n "$SILENT_INIT" ] || log_daemon_msg "Stopping HTTP server" "ashd" + if kill_listen; then + [ -n "$SILENT_INIT" ] || log_success_msg + else + [ -n "$SILENT_INIT" ] || log_end_msg $? + fi +} + +stop_gracefully() { + [ -n "$SILENT_INIT" ] || log_daemon_msg "Stopping HTTP server" "ashd" + if ! kill_listen ; then + log_end_msg 1 + return 1 + fi + pid=$(cat "$PIDFILE" 2>/dev/null || true) + if kill -0 "$pid" 2>/dev/null; then + [ -n "$SILENT_INIT" ] || log_progress_msg "waiting for remaining connections..." + for try in $(seq "$GRACE_PERIOD"); do + sleep 1 + if ! kill -0 "$pid" 2>/dev/null; then + [ -n "$SILENT_INIT" ] || log_success_msg + return 0 + fi + done + else + [ -n "$SILENT_INIT" ] || log_success_msg + return 0 + fi + [ -n "$SILENT_INIT" ] || log_progress_msg "terminating remaining connections" + if kill_wholly; then + [ -n "$SILENT_INIT" ] || log_success_msg + else + log_end_msg 1 + fi } case "$1" in @@ -39,10 +109,12 @@ case "$1" in start ;; stop) - stop + stop_gracefully ;; restart) - stop + stop_listen + # Truncate PID file to allow start-stop-daemon to work despite remaining connections. + >"$PIDFILE" start ;; esac diff --git a/examples/vhosts/run b/examples/vhosts/run index 3c21690..a9d419f 100755 --- a/examples/vhosts/run +++ b/examples/vhosts/run @@ -6,4 +6,4 @@ cd "$(dirname "$0")" # Start htparser running patplex; see the patterns.conf file for # further details. -htparser plain:port=8080 -- patplex patterns.conf +htparser plain:port=8080 -- patplex ./patterns.conf diff --git a/python3/ashd-wsgi3 b/python3/ashd-wsgi3 index fa0b8b5..c10bfc9 100755 --- a/python3/ashd-wsgi3 +++ b/python3/ashd-wsgi3 @@ -1,6 +1,6 @@ #!/usr/bin/python3 -import sys, os, getopt, socket, logging, time, locale, collections, signal +import sys, os, getopt, socket, logging, time, locale, collections.abc, signal import ashd.util, ashd.serve, ashd.htlib try: import pdm.srv @@ -148,10 +148,10 @@ def mkenv(req): return env def recode(thing): - if isinstance(thing, collections.ByteString): + if isinstance(thing, collections.abc.ByteString): return thing else: - return str(thing).encode("latin-1") + return str(thing).encode("utf-8") class request(ashd.serve.wsgirequest): def __init__(self, *, bkreq, **kw): diff --git a/python3/ashd/async.py b/python3/ashd/async.py index 238f6b7..b78920f 100644 --- a/python3/ashd/async.py +++ b/python3/ashd/async.py @@ -1,300 +1 @@ -import sys, os, errno, threading, select, traceback - -class epoller(object): - exc_handler = None - - def __init__(self, check=None): - self.registered = {} - self.fdcache = {} - self.lock = threading.RLock() - self.ep = None - self.th = None - self.stopped = False - self.loopcheck = set() - if check is not None: - self.loopcheck.add(check) - self._daemon = True - - @staticmethod - def _evsfor(ch): - return ((select.EPOLLIN if ch.readable else 0) | - (select.EPOLLOUT if ch.writable else 0)) - - def _ckrun(self): - if self.registered and self.th is None: - th = threading.Thread(target=self._run, name="Async epoll thread") - th.daemon = self._daemon - th.start() - self.th = th - - def exception(self, ch, *exc): - self.remove(ch) - if self.exc_handler is None: - traceback.print_exception(*exc) - else: - self.exc_handler(ch, *exc) - - def _cb(self, ch, nm): - try: - m = getattr(ch, nm, None) - if m is None: - raise AttributeError("%r has no %s method" % (ch, nm)) - m() - except Exception as exc: - self.exception(ch, *sys.exc_info()) - - def _closeall(self): - with self.lock: - while self.registered: - fd, (ch, evs) = next(iter(self.registered.items())) - del self.registered[fd] - self.ep.unregister(fd) - self._cb(ch, "close") - - def _run(self): - ep = select.epoll() - try: - with self.lock: - try: - for fd, (ob, evs) in self.registered.items(): - ep.register(fd, evs) - except: - self.registered.clear() - raise - self.ep = ep - - while self.registered: - for ck in self.loopcheck: - ck(self) - if self.stopped: - self._closeall() - break - try: - evlist = ep.poll(10) - except IOError as exc: - if exc.errno == errno.EINTR: - continue - raise - for fd, evs in evlist: - with self.lock: - if fd not in self.registered: - continue - ch, cevs = self.registered[fd] - if fd in self.registered and evs & (select.EPOLLIN | select.EPOLLHUP | select.EPOLLERR): - self._cb(ch, "read") - if fd in self.registered and evs & select.EPOLLOUT: - self._cb(ch, "write") - if fd in self.registered: - nevs = self._evsfor(ch) - if nevs == 0: - del self.fdcache[ch] - del self.registered[fd] - ep.unregister(fd) - self._cb(ch, "close") - elif nevs != cevs: - self.registered[fd] = ch, nevs - ep.modify(fd, nevs) - - finally: - with self.lock: - self.th = None - self.ep = None - self._ckrun() - ep.close() - - @property - def daemon(self): return self._daemon - @daemon.setter - def daemon(self, value): - self._daemon = bool(value) - with self.lock: - if self.th is not None: - self.th = daemon = self._daemon - - def add(self, ch): - with self.lock: - fd = ch.fileno() - if fd in self.registered: - raise KeyError("fd %i is already registered" % fd) - evs = self._evsfor(ch) - if evs == 0: - ch.close() - return - ch.watcher = self - self.fdcache[ch] = fd - self.registered[fd] = (ch, evs) - if self.ep: - self.ep.register(fd, evs) - self._ckrun() - - def remove(self, ch, ignore=False): - with self.lock: - try: - fd = self.fdcache[ch] - except KeyError: - if ignore: - return - raise KeyError("fd %i is not registered" % fd) - pch, cevs = self.registered[fd] - if pch is not ch: - raise ValueError("fd %i registered via object %r, cannot remove with %r" % (pch, ch)) - del self.fdcache[ch] - del self.registered[fd] - if self.ep: - self.ep.unregister(fd) - ch.close() - - def update(self, ch, ignore=False): - with self.lock: - try: - fd = self.fdcache[ch] - except KeyError: - if ignore: - return - raise KeyError("fd %i is not registered" % fd) - pch, cevs = self.registered[fd] - if pch is not ch: - raise ValueError("fd %i registered via object %r, cannot update with %r" % (pch, ch)) - evs = self._evsfor(ch) - if evs == 0: - del self.fdcache[ch] - del self.registered[fd] - if self.ep: - self.ep.unregister(fd) - ch.close() - elif evs != cevs: - self.registered[fd] = ch, evs - if self.ep: - self.ep.modify(fd, evs) - - def stop(self): - if threading.current_thread() == self.th: - self.stopped = True - else: - def tgt(): - self.stopped = True - cb = callbuffer() - cb.call(tgt) - cb.stop() - self.add(cb) - -def watcher(): - return epoller() - -class channel(object): - readable = False - writable = False - - def __init__(self): - self.watcher = None - - def fileno(self): - raise NotImplementedError("fileno()") - - def close(self): - pass - -class sockbuffer(channel): - def __init__(self, socket, **kwargs): - super().__init__(**kwargs) - self.sk = socket - self.eof = False - self.obuf = bytearray() - - def fileno(self): - return self.sk.fileno() - - def close(self): - self.sk.close() - - def gotdata(self, data): - if data == b"": - self.eof = True - - def send(self, data, eof=False): - self.obuf.extend(data) - if eof: - self.eof = True - if self.watcher is not None: - self.watcher.update(self, True) - - @property - def readable(self): - return not self.eof - def read(self): - try: - data = self.sk.recv(1024) - self.gotdata(data) - except IOError: - self.obuf[:] = b"" - self.eof = True - - @property - def writable(self): - return bool(self.obuf); - def write(self): - try: - ret = self.sk.send(self.obuf) - self.obuf[:ret] = b"" - except IOError: - self.obuf[:] = b"" - self.eof = True - -class callbuffer(channel): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.queue = [] - self.rp, self.wp = os.pipe() - self.lock = threading.Lock() - self.eof = False - - def fileno(self): - return self.rp - - def close(self): - with self.lock: - try: - if self.wp >= 0: - os.close(self.wp) - self.wp = -1 - finally: - if self.rp >= 0: - os.close(self.rp) - self.rp = -1 - - @property - def readable(self): - return not self.eof - def read(self): - with self.lock: - try: - data = os.read(self.rp, 1024) - if data == b"": - self.eof = True - except IOError: - self.eof = True - cbs = list(self.queue) - self.queue[:] = [] - for cb in cbs: - cb() - - writable = False - - def call(self, cb): - with self.lock: - if self.wp < 0: - raise Exception("stopped") - self.queue.append(cb) - os.write(self.wp, b"a") - - def stop(self): - with self.lock: - if self.wp >= 0: - os.close(self.wp) - self.wp = -1 - -def currentwatcher(io, current): - def check(io): - if not current: - io.stop() - io.loopcheck.add(check) +from .asyncio import * diff --git a/python3/ashd/asyncio.py b/python3/ashd/asyncio.py new file mode 100644 index 0000000..238f6b7 --- /dev/null +++ b/python3/ashd/asyncio.py @@ -0,0 +1,300 @@ +import sys, os, errno, threading, select, traceback + +class epoller(object): + exc_handler = None + + def __init__(self, check=None): + self.registered = {} + self.fdcache = {} + self.lock = threading.RLock() + self.ep = None + self.th = None + self.stopped = False + self.loopcheck = set() + if check is not None: + self.loopcheck.add(check) + self._daemon = True + + @staticmethod + def _evsfor(ch): + return ((select.EPOLLIN if ch.readable else 0) | + (select.EPOLLOUT if ch.writable else 0)) + + def _ckrun(self): + if self.registered and self.th is None: + th = threading.Thread(target=self._run, name="Async epoll thread") + th.daemon = self._daemon + th.start() + self.th = th + + def exception(self, ch, *exc): + self.remove(ch) + if self.exc_handler is None: + traceback.print_exception(*exc) + else: + self.exc_handler(ch, *exc) + + def _cb(self, ch, nm): + try: + m = getattr(ch, nm, None) + if m is None: + raise AttributeError("%r has no %s method" % (ch, nm)) + m() + except Exception as exc: + self.exception(ch, *sys.exc_info()) + + def _closeall(self): + with self.lock: + while self.registered: + fd, (ch, evs) = next(iter(self.registered.items())) + del self.registered[fd] + self.ep.unregister(fd) + self._cb(ch, "close") + + def _run(self): + ep = select.epoll() + try: + with self.lock: + try: + for fd, (ob, evs) in self.registered.items(): + ep.register(fd, evs) + except: + self.registered.clear() + raise + self.ep = ep + + while self.registered: + for ck in self.loopcheck: + ck(self) + if self.stopped: + self._closeall() + break + try: + evlist = ep.poll(10) + except IOError as exc: + if exc.errno == errno.EINTR: + continue + raise + for fd, evs in evlist: + with self.lock: + if fd not in self.registered: + continue + ch, cevs = self.registered[fd] + if fd in self.registered and evs & (select.EPOLLIN | select.EPOLLHUP | select.EPOLLERR): + self._cb(ch, "read") + if fd in self.registered and evs & select.EPOLLOUT: + self._cb(ch, "write") + if fd in self.registered: + nevs = self._evsfor(ch) + if nevs == 0: + del self.fdcache[ch] + del self.registered[fd] + ep.unregister(fd) + self._cb(ch, "close") + elif nevs != cevs: + self.registered[fd] = ch, nevs + ep.modify(fd, nevs) + + finally: + with self.lock: + self.th = None + self.ep = None + self._ckrun() + ep.close() + + @property + def daemon(self): return self._daemon + @daemon.setter + def daemon(self, value): + self._daemon = bool(value) + with self.lock: + if self.th is not None: + self.th = daemon = self._daemon + + def add(self, ch): + with self.lock: + fd = ch.fileno() + if fd in self.registered: + raise KeyError("fd %i is already registered" % fd) + evs = self._evsfor(ch) + if evs == 0: + ch.close() + return + ch.watcher = self + self.fdcache[ch] = fd + self.registered[fd] = (ch, evs) + if self.ep: + self.ep.register(fd, evs) + self._ckrun() + + def remove(self, ch, ignore=False): + with self.lock: + try: + fd = self.fdcache[ch] + except KeyError: + if ignore: + return + raise KeyError("fd %i is not registered" % fd) + pch, cevs = self.registered[fd] + if pch is not ch: + raise ValueError("fd %i registered via object %r, cannot remove with %r" % (pch, ch)) + del self.fdcache[ch] + del self.registered[fd] + if self.ep: + self.ep.unregister(fd) + ch.close() + + def update(self, ch, ignore=False): + with self.lock: + try: + fd = self.fdcache[ch] + except KeyError: + if ignore: + return + raise KeyError("fd %i is not registered" % fd) + pch, cevs = self.registered[fd] + if pch is not ch: + raise ValueError("fd %i registered via object %r, cannot update with %r" % (pch, ch)) + evs = self._evsfor(ch) + if evs == 0: + del self.fdcache[ch] + del self.registered[fd] + if self.ep: + self.ep.unregister(fd) + ch.close() + elif evs != cevs: + self.registered[fd] = ch, evs + if self.ep: + self.ep.modify(fd, evs) + + def stop(self): + if threading.current_thread() == self.th: + self.stopped = True + else: + def tgt(): + self.stopped = True + cb = callbuffer() + cb.call(tgt) + cb.stop() + self.add(cb) + +def watcher(): + return epoller() + +class channel(object): + readable = False + writable = False + + def __init__(self): + self.watcher = None + + def fileno(self): + raise NotImplementedError("fileno()") + + def close(self): + pass + +class sockbuffer(channel): + def __init__(self, socket, **kwargs): + super().__init__(**kwargs) + self.sk = socket + self.eof = False + self.obuf = bytearray() + + def fileno(self): + return self.sk.fileno() + + def close(self): + self.sk.close() + + def gotdata(self, data): + if data == b"": + self.eof = True + + def send(self, data, eof=False): + self.obuf.extend(data) + if eof: + self.eof = True + if self.watcher is not None: + self.watcher.update(self, True) + + @property + def readable(self): + return not self.eof + def read(self): + try: + data = self.sk.recv(1024) + self.gotdata(data) + except IOError: + self.obuf[:] = b"" + self.eof = True + + @property + def writable(self): + return bool(self.obuf); + def write(self): + try: + ret = self.sk.send(self.obuf) + self.obuf[:ret] = b"" + except IOError: + self.obuf[:] = b"" + self.eof = True + +class callbuffer(channel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.queue = [] + self.rp, self.wp = os.pipe() + self.lock = threading.Lock() + self.eof = False + + def fileno(self): + return self.rp + + def close(self): + with self.lock: + try: + if self.wp >= 0: + os.close(self.wp) + self.wp = -1 + finally: + if self.rp >= 0: + os.close(self.rp) + self.rp = -1 + + @property + def readable(self): + return not self.eof + def read(self): + with self.lock: + try: + data = os.read(self.rp, 1024) + if data == b"": + self.eof = True + except IOError: + self.eof = True + cbs = list(self.queue) + self.queue[:] = [] + for cb in cbs: + cb() + + writable = False + + def call(self, cb): + with self.lock: + if self.wp < 0: + raise Exception("stopped") + self.queue.append(cb) + os.write(self.wp, b"a") + + def stop(self): + with self.lock: + if self.wp >= 0: + os.close(self.wp) + self.wp = -1 + +def currentwatcher(io, current): + def check(io): + if not current: + io.stop() + io.loopcheck.add(check) diff --git a/python3/ashd/util.py b/python3/ashd/util.py index 3818e4b..bf32637 100644 --- a/python3/ashd/util.py +++ b/python3/ashd/util.py @@ -161,7 +161,10 @@ def serveloop(handler, sock = 0): and is called once for each received request. """ while True: - req = proto.recvreq(sock) + try: + req = proto.recvreq(sock) + except InterruptedError: + continue if req is None: break try: diff --git a/src/.gitignore b/src/.gitignore index 1ecc66e..9bed7b2 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -13,3 +13,5 @@ /psendfile /httimed /httrcall +/htpipe +/ratequeue diff --git a/src/Makefile.am b/src/Makefile.am index 5422e74..3bc8963 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2,14 +2,14 @@ SUBDIRS = dirplex bin_PROGRAMS = htparser sendfile callcgi patplex userplex htls \ callscgi accesslog htextauth callfcgi multifscgi \ - errlogger httimed psendfile httrcall + errlogger httimed psendfile httrcall htpipe ratequeue -htparser_SOURCES = htparser.c htparser.h plaintcp.c ssl-gnutls.c +htparser_SOURCES = htparser.c htparser.h plaintcp.c ssl-gnutls.c ssl-openssl.c LDADD = $(top_srcdir)/lib/libht.a AM_CPPFLAGS = -I$(top_srcdir)/lib -htparser_CPPFLAGS = $(AM_CPPFLAGS) @GNUTLS_CPPFLAGS@ -htparser_LDADD = $(LDADD) @GNUTLS_LIBS@ +htparser_CPPFLAGS = $(AM_CPPFLAGS) @GNUTLS_CPPFLAGS@ @OPENSSL_CPPFLAGS@ +htparser_LDADD = $(LDADD) @GNUTLS_LIBS@ @OPENSSL_LIBS@ sendfile_LDADD = $(LDADD) -lmagic @XATTR_LIBS@ psendfile_LDADD = $(LDADD) -lmagic @XATTR_LIBS@ diff --git a/src/accesslog.c b/src/accesslog.c index af43373..0c7ef8d 100644 --- a/src/accesslog.c +++ b/src/accesslog.c @@ -96,6 +96,16 @@ static void logitem(struct logdata *data, char o, char *d) qputs(h, out); } break; + case 'p': + if(!data->resp || ((h = getheader(data->resp, d)) == NULL)) { + putc('-', out); + } else { + qputs(h, out); + } + break; + case 'P': + logitem(data, 'p', sprintf3("X-Ash-%s", d)); + break; case 'u': qputs(data->req->url, out); break; @@ -508,7 +518,7 @@ int main(int argc, char **argv) pidfile = optarg; break; case 'a': - format = "%A - - [%{%d/%b/%Y:%H:%M:%S %z}t] \"%m %u %v\" %c %o \"%R\" \"%G\""; + format = "%A - %{log-user}P [%{%d/%b/%Y:%H:%M:%S %z}t] \"%m %u %v\" %c %o \"%R\" \"%G\""; break; default: usage(stderr); diff --git a/src/dirplex/conf.c b/src/dirplex/conf.c index 9ae4ca7..0bfa6f4 100644 --- a/src/dirplex/conf.c +++ b/src/dirplex/conf.c @@ -84,6 +84,7 @@ static void freeconfig(struct config *cf) freepattern(pat); } freeca(cf->index); + freeca(cf->dotallow); if(cf->capture != NULL) free(cf->capture); if(cf->reparse != NULL) @@ -258,6 +259,9 @@ struct config *readconfig(char *file) } else if(!strcmp(s->argv[0], "index-file")) { freeca(cf->index); cf->index = cadup(s->argv + 1); + } else if(!strcmp(s->argv[0], "dot-allow")) { + freeca(cf->dotallow); + cf->dotallow = cadup(s->argv + 1); } else if(!strcmp(s->argv[0], "capture")) { if(s->argc < 2) { flog(LOG_WARNING, "%s:%i: missing argument to capture declaration", s->file, s->lno); diff --git a/src/dirplex/dirplex.c b/src/dirplex/dirplex.c index 73bb9cc..ee7f650 100644 --- a/src/dirplex/dirplex.c +++ b/src/dirplex/dirplex.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -131,6 +132,27 @@ static void handlefile(struct hthead *req, int fd, char *path) handle(req, fd, path, pat); } +static int checkaccess(char *path, char *name) +{ + int i, o; + struct config **cfs; + + if(*name == '.') { + cfs = getconfigs(sprintf3("%s/", path)); + for(i = 0; cfs[i] != NULL; i++) { + if(cfs[i]->dotallow != NULL) { + for(o = 0; cfs[i]->dotallow[o] != NULL; o++) { + if(!fnmatch(cfs[i]->dotallow[o], name, 0)) + return(1); + } + break; + } + } + return(0); + } + return(1); +} + static char *findfile(char *path, char *name, struct stat *sb) { DIR *dir; @@ -155,12 +177,20 @@ static char *findfile(char *path, char *name, struct stat *sb) continue; if(strncmp(dent->d_name, name, strlen(name))) continue; - fp = sprintf3("%s/%s", path, dent->d_name); - if(stat(fp, sb)) + fp = sprintf2("%s/%s", path, dent->d_name); + if(stat(fp, sb)) { + free(fp); + continue; + } + if(!S_ISREG(sb->st_mode)) { + free(fp); continue; - if(!S_ISREG(sb->st_mode)) + } + if(!checkaccess(path, dent->d_name)) { + free(fp); continue; - ret = sstrdup(fp); + } + ret = fp; break; } closedir(dir); @@ -216,9 +246,9 @@ static int checkentry(struct hthead *req, int fd, char *path, char *rest, char * char *newpath; int rv; - if(*el == '.') - return(0); if(!stat(sprintf3("%s/%s", path, el), &sb)) { + if(!checkaccess(path, el)) + return(0); if(S_ISDIR(sb.st_mode)) { if(!*rest) { stdredir(req, fd, 301, sprintf3("%s/", el)); diff --git a/src/dirplex/dirplex.h b/src/dirplex/dirplex.h index ffe12a6..754327d 100644 --- a/src/dirplex/dirplex.h +++ b/src/dirplex/dirplex.h @@ -17,7 +17,7 @@ struct config { time_t mtime, lastck; struct child *children; struct pattern *patterns; - char **index; + char **index, **dotallow; char *capture, *reparse; int caproot, parsecomb; }; diff --git a/src/htparser.c b/src/htparser.c index d10121c..5491855 100644 --- a/src/htparser.c +++ b/src/htparser.c @@ -40,7 +40,6 @@ #include "htparser.h" static int plex; -static char *pidfile = NULL; static int daemonize, usesyslog; struct mtbuf listeners; @@ -581,9 +580,12 @@ static void addport(char *spec) /* XXX: It would be nice to decentralize this, but, meh... */ if(!strcmp(nm, "plain")) { handleplain(pars.d, pars.b, vals.b); -#ifdef HAVE_GNUTLS +#if defined HAVE_GNUTLS } else if(!strcmp(nm, "ssl")) { handlegnussl(pars.d, pars.b, vals.b); +#elif defined HAVE_OPENSSL + } else if(!strcmp(nm, "ssl")) { + handleossl(pars.d, pars.b, vals.b); #endif } else { flog(LOG_ERR, "htparser: unknown port handler `%s'", nm); @@ -603,12 +605,12 @@ int main(int argc, char **argv) { int c, d; int i, s1; - char *root; + char *root, *pidfile, *pidtmp; FILE *pidout; struct passwd *pwent; daemonize = usesyslog = 0; - root = NULL; + root = pidfile = NULL; pwent = NULL; while((c = getopt(argc, argv, "+hSfu:r:p:")) >= 0) { switch(c) { @@ -622,16 +624,16 @@ int main(int argc, char **argv) usesyslog = 1; break; case 'u': - if((pwent = getpwnam(optarg)) == NULL) { + if(optarg[0] && ((pwent = getpwnam(optarg)) == NULL)) { flog(LOG_ERR, "could not find user %s", optarg); exit(1); } break; case 'r': - root = optarg; + root = optarg[0] ? optarg : NULL; break; case 'p': - pidfile = optarg; + pidfile = optarg[0] ? optarg : NULL; break; default: usage(stderr); @@ -656,8 +658,14 @@ int main(int argc, char **argv) bufadd(listeners, mustart(plexwatch, plex)); pidout = NULL; if(pidfile != NULL) { - if((pidout = fopen(pidfile, "w")) == NULL) { - flog(LOG_ERR, "could not open %s for writing: %s", pidfile, strerror(errno)); + pidtmp = sprintf3("%s.new", pidfile); + if((pidout = fopen(pidtmp, "w")) == NULL) { + flog(LOG_ERR, "could not open %s for writing: %s", pidtmp, strerror(errno)); + return(1); + } + if(rename(pidtmp, pidfile)) { + flog(LOG_ERR, "could not overwrite %s: %s", pidfile, strerror(errno)); + unlink(pidtmp); return(1); } } @@ -688,7 +696,7 @@ int main(int argc, char **argv) } if(pidout != NULL) { fprintf(pidout, "%i\n", getpid()); - fclose(pidout); + fflush(pidout); } d = 0; while(!d) { @@ -701,11 +709,17 @@ int main(int argc, char **argv) while(listeners.d > 0) resume(listeners.b[0], 0); flog(LOG_INFO, "no longer listening"); + if(pidout != NULL) { + putc('\n', pidout); + fflush(pidout); + } } else { d = 1; } break; } } + if(pidout != NULL) + ftruncate(fileno(pidout), 0); return(0); } diff --git a/src/htparser.h b/src/htparser.h index d9f014d..946ed51 100644 --- a/src/htparser.h +++ b/src/htparser.h @@ -20,6 +20,9 @@ void handleplain(int argc, char **argp, char **argv); #ifdef HAVE_GNUTLS void handlegnussl(int argc, char **argp, char **argv); #endif +#ifdef HAVE_OPENSSL +void handleossl(int argc, char **argp, char **argv); +#endif extern struct mtbuf listeners; diff --git a/src/htpipe.c b/src/htpipe.c new file mode 100644 index 0000000..02cbd35 --- /dev/null +++ b/src/htpipe.c @@ -0,0 +1,235 @@ +/* + ashd - A Sane HTTP Daemon + Copyright (C) 2008 Fredrik Tolf + + This 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 . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include + +static void usage(FILE *out) +{ + fprintf(out, "usge: htpipe [-h] [-CS] SOCKET-PATH [CHILD ARGS...]\n"); +} + +static int clconnect(char *path) +{ + int sk; + struct sockaddr_un unm; + + if((sk = socket(AF_UNIX, SOCK_SEQPACKET, 0)) < 0) + return(-1); + memset(&unm, 0, sizeof(unm)); + unm.sun_family = AF_UNIX; + strcpy(unm.sun_path, path); + if(connect(sk, (struct sockaddr *)&unm, sizeof(unm))) { + close(sk); + return(-1); + } + return(sk); +} + +static int mklisten(char *path) +{ + int sk; + struct sockaddr_un unm; + struct stat sb; + + if(!stat(path, &sb) && S_ISSOCK(sb.st_mode)) + unlink(path); + if((sk = socket(AF_UNIX, SOCK_SEQPACKET, 0)) < 0) + return(-1); + memset(&unm, 0, sizeof(unm)); + unm.sun_family = AF_UNIX; + strcpy(unm.sun_path, path); + if(bind(sk, (struct sockaddr *)&unm, sizeof(unm)) || listen(sk, 128)) { + close(sk); + return(-1); + } + return(sk); +} + +static void runclient(int sk) +{ + int fd; + struct hthead *req; + + while(1) { + if((fd = recvreq(0, &req)) < 0) { + if(errno == 0) + break; + flog(LOG_ERR, "htpipe: error in recvreq: %s", strerror(errno)); + exit(1); + } + if(sendreq(sk, req, fd)) { + flog(LOG_ERR, "htpipe: could not pass request across pipe: %s", strerror(errno)); + exit(1); + } + freehthead(req); + close(fd); + } +} + +static void runserver(int lsk, int ch) +{ + int i, o, ret, rfd, ncl, *cl, acl; + struct hthead *req; + + ncl = 0; + cl = NULL; + while(1) { + struct pollfd pfd[ncl + 1]; + for(i = 0; i < ncl; i++) { + pfd[i].fd = cl[i]; + pfd[i].events= POLLIN; + } + pfd[i].fd = lsk; + pfd[i].events = POLLIN; + if((ret = poll(pfd, ncl + 1, -1)) < 0) { + if(errno != EINTR) { + flog(LOG_ERR, "htpipe: error in poll: %s", strerror(errno)); + exit(1); + } + } + for(i = 0; i < ncl; i++) { + if(pfd[i].revents & POLLIN) { + if((rfd = recvreq(cl[i], &req)) < 0) { + if(errno != 0) + flog(LOG_ERR, "htpipe: error from client: %s", strerror(errno)); + close(cl[i]); + cl[i] = -1; + } else { + if(sendreq(ch, req, rfd)) { + flog(LOG_ERR, "htpipe: could not pass request to child: %s", strerror(errno)); + exit(1); + } + freehthead(req); + close(rfd); + } + } + } + if(pfd[i].revents & POLLIN) { + if((acl = accept(lsk, NULL, 0)) < 0) { + flog(LOG_ERR, "htpipe: error in accept: %s", strerror(errno)); + } else { + cl = srealloc(cl, sizeof(*cl) * (ncl + 1)); + cl[ncl++] = acl; + } + } + for(i = o = 0; i < ncl; i++) { + if(cl[i] >= 0) + cl[o++] = cl[i]; + } + ncl = o; + } +} + +int main(int argc, char **argv) +{ + int c, cli, srv, sk, ch, sst; + pid_t sproc; + char *path, **chspec; + + cli = srv = 0; + while((c = getopt(argc, argv, "+hCS")) >= 0) { + switch(c) { + case 'h': + usage(stdout); + exit(0); + case 'C': + cli = 1; + break; + case 'S': + srv = 1; + break; + } + } + if(argc - optind < 1) { + usage(stderr); + exit(1); + } + path = argv[optind++]; + chspec = argv + optind; + if(cli) { + if((sk = clconnect(path)) < 0) { + flog(LOG_ERR, "htpipe: %s: %s", path, strerror(errno)); + exit(1); + } + runclient(sk); + } else if(srv) { + if(!*chspec) { + usage(stderr); + exit(1); + } + if((sk = mklisten(path)) < 0) { + flog(LOG_ERR, "htpipe: %s: %s", path, strerror(errno)); + exit(1); + } + if((ch = stdmkchild(chspec, NULL, NULL)) < 0) { + flog(LOG_ERR, "htpipe: could not fork child: %s", strerror(errno)); + exit(1); + } + runserver(sk, ch); + } else { + if(!*chspec) { + usage(stderr); + exit(1); + } + if((sk = clconnect(path)) < 0) { + if((sproc = fork()) < 0) + err(1, "fork"); + if(sproc == 0) { + if((sk = mklisten(path)) < 0) { + flog(LOG_ERR, "htpipe: %s: %s", path, strerror(errno)); + exit(1); + } + if((ch = stdmkchild(chspec, NULL, NULL)) < 0) { + flog(LOG_ERR, "htpipe: could not fork child: %s", strerror(errno)); + exit(1); + } + daemon(0, 1); + runserver(sk, ch); + abort(); + } + if((waitpid(sproc, &sst, 0)) != sproc) { + flog(LOG_ERR, "htpipe: could not wait for server process: %s", strerror(errno)); + exit(1); + } + if((sk = clconnect(path)) < 0) { + flog(LOG_ERR, "htpipe: could not connect to newly forked server: %s", strerror(errno)); + exit(1); + } + } + runclient(sk); + } + return(0); +} diff --git a/src/patplex.c b/src/patplex.c index 9939543..e893bc0 100644 --- a/src/patplex.c +++ b/src/patplex.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #ifdef HAVE_CONFIG_H @@ -41,11 +42,13 @@ #define PAT_METHOD 2 #define PAT_HEADER 3 #define PAT_ALL 4 -#define PAT_DEFAULT 5 #define PATFL_MSS 1 #define PATFL_UNQ 2 +#define HND_CHILD 1 +#define HND_REPARSE 2 + struct config { struct child *children; struct pattern *patterns; @@ -69,6 +72,7 @@ struct pattern { char *childnm; struct rule **rules; char *restpat; + int handler, prio, disable; }; static struct config *gconfig, *lconfig; @@ -237,7 +241,14 @@ static struct pattern *parsepattern(struct cfstate *s) } else if(!strcmp(s->argv[0], "all")) { newrule(pat)->type = PAT_ALL; } else if(!strcmp(s->argv[0], "default")) { - newrule(pat)->type = PAT_DEFAULT; + newrule(pat)->type = PAT_ALL; + pat->prio = -10; + } else if(!strcmp(s->argv[0], "order") || !strcmp(s->argv[0], "priority")) { + if(s->argc < 2) { + flog(LOG_WARNING, "%s:%i: missing specification for `%s' directive", s->file, s->lno, s->argv[0]); + continue; + } + pat->prio = atoi(s->argv[1]); } else if(!strcmp(s->argv[0], "handler")) { if(s->argc < 2) { flog(LOG_WARNING, "%s:%i: missing child name for `handler' directive", s->file, s->lno); @@ -246,6 +257,9 @@ static struct pattern *parsepattern(struct cfstate *s) if(pat->childnm != NULL) free(pat->childnm); pat->childnm = sstrdup(s->argv[1]); + pat->handler = HND_CHILD; + } else if(!strcmp(s->argv[0], "reparse")) { + pat->handler = HND_REPARSE; } else if(!strcmp(s->argv[0], "restpat")) { if(s->argc < 2) { flog(LOG_WARNING, "%s:%i: missing pattern for `restpat' directive", s->file, s->lno); @@ -279,7 +293,7 @@ static struct pattern *parsepattern(struct cfstate *s) freepattern(pat); return(NULL); } - if(pat->childnm == NULL) { + if(pat->handler == 0) { flog(LOG_WARNING, "%s:%i: missing handler in match declaration", s->file, sl); freepattern(pat); return(NULL); @@ -394,7 +408,19 @@ static void qoffsets(char *buf, int *obuf, char *pstr, int unquote) } } -static struct pattern *findmatch(struct config *cf, struct hthead *req, int trydefault) +struct match { + struct pattern *pat; + char **mstr; + int rmo; +}; + +static void freematch(struct match *match) +{ + freeca(match->mstr); + free(match); +} + +static struct match *findmatch(struct config *cf, struct hthead *req, struct match *match) { int i, o; struct pattern *pat; @@ -407,6 +433,8 @@ static struct pattern *findmatch(struct config *cf, struct hthead *req, int tryd mstr = NULL; for(pat = cf->patterns; pat != NULL; pat = pat->next) { + if(pat->disable || (match && (pat->prio <= match->pat->prio))) + continue; rmo = -1; for(i = 0; (rule = pat->rules[i]) != NULL; i++) { rx = NULL; @@ -451,27 +479,32 @@ static struct pattern *findmatch(struct config *cf, struct hthead *req, int tryd } } } else if(rule->type == PAT_ALL) { - } else if(rule->type == PAT_DEFAULT) { - if(!trydefault) - break; } } if(!rule) { - if(pat->restpat) { - exprestpat(req, pat, mstr); - } else if(rmo != -1) { - replrest(req, req->rest + rmo); - } - if(mstr) - freeca(mstr); - return(pat); - } - if(mstr) { - freeca(mstr); - mstr = NULL; + if(match) + freematch(match); + omalloc(match); + match->pat = pat; + match->mstr = mstr; + match->rmo = rmo; } } - return(NULL); + return(match); +} + +static void execmatch(struct hthead *req, struct match *match) +{ + struct headmod *head; + + if(match->pat->restpat) + exprestpat(req, match->pat, match->mstr); + else if(match->rmo != -1) + replrest(req, req->rest + match->rmo); + for(head = match->pat->headers; head != NULL; head = head->next) { + headrmheader(req, head->name); + headappheader(req, head->name, head->value); + } } static void childerror(struct hthead *req, int fd) @@ -484,44 +517,42 @@ static void childerror(struct hthead *req, int fd) static void serve(struct hthead *req, int fd) { - struct pattern *pat; - struct headmod *head; + struct match *match; struct child *ch; - pat = NULL; - if(pat == NULL) - pat = findmatch(lconfig, req, 0); - if(pat == NULL) - pat = findmatch(lconfig, req, 1); - if(gconfig != NULL) { - if(pat == NULL) - pat = findmatch(gconfig, req, 0); - if(pat == NULL) - pat = findmatch(gconfig, req, 1); - } - if(pat == NULL) { + match = NULL; + match = findmatch(lconfig, req, match); + if(gconfig != NULL) + match = findmatch(gconfig, req, match); + if(match == NULL) { simpleerror(fd, 404, "Not Found", "The requested resource could not be found on this server."); return; } - ch = NULL; - if(ch == NULL) - ch = getchild(lconfig, pat->childnm); - if(gconfig != NULL) { + execmatch(req, match); + switch(match->pat->handler) { + case HND_CHILD: + ch = NULL; if(ch == NULL) - ch = getchild(gconfig, pat->childnm); - } - if(ch == NULL) { - flog(LOG_ERR, "child %s requested, but was not declared", pat->childnm); - simpleerror(fd, 500, "Configuration Error", "The server is erroneously configured. Handler %s was requested, but not declared.", pat->childnm); - return; - } - - for(head = pat->headers; head != NULL; head = head->next) { - headrmheader(req, head->name); - headappheader(req, head->name, head->value); + ch = getchild(lconfig, match->pat->childnm); + if((ch == NULL) && (gconfig != NULL)) + ch = getchild(gconfig, match->pat->childnm); + if(ch == NULL) { + flog(LOG_ERR, "child %s requested, but was not declared", match->pat->childnm); + simpleerror(fd, 500, "Configuration Error", "The server is erroneously configured. Handler %s was requested, but not declared.", match->pat->childnm); + break; + } + if(childhandle(ch, req, fd, NULL, NULL)) + childerror(req, fd); + break; + case HND_REPARSE: + match->pat->disable = 1; + serve(req, fd); + match->pat->disable = 0; + break; + default: + abort(); } - if(childhandle(ch, req, fd, NULL, NULL)) - childerror(req, fd); + freematch(match); } static void reloadconf(char *nm) diff --git a/src/psendfile.c b/src/psendfile.c index 3e26f8f..464440a 100644 --- a/src/psendfile.c +++ b/src/psendfile.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include diff --git a/src/ratequeue.c b/src/ratequeue.c new file mode 100644 index 0000000..21d6ec3 --- /dev/null +++ b/src/ratequeue.c @@ -0,0 +1,538 @@ +/* + ashd - A Sane HTTP Daemon + Copyright (C) 2008 Fredrik Tolf + + This 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 . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include + +#define SBUCKETS 7 + +struct source { + int type; + char data[16]; + unsigned int len, hash; +}; + +struct waiting { + struct hthead *req; + int fd; +}; + +struct bucket { + struct source id; + double level, last, etime, wtime; + typedbuf(struct waiting) brim; + int thpos, blocked; +}; + +struct btime { + struct bucket *bk; + double tm; +}; + +struct config { + double size, rate, retain, warnrate; + int brimsize; +}; + +static struct bucket *sbuckets[1 << SBUCKETS]; +static struct bucket **buckets = sbuckets; +static int hashlen = SBUCKETS, nbuckets = 0; +static typedbuf(struct btime) timeheap; +static int child, reload; +static double now; +static const struct config defcfg = { + .size = 100, .rate = 10, .warnrate = 60, + .retain = 10, .brimsize = 10, +}; +static struct config cf; + +static double rtime(void) +{ + static int init = 0; + static struct timespec or; + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + if(!init) { + or = ts; + init = 1; + } + return((ts.tv_sec - or.tv_sec) + ((ts.tv_nsec - or.tv_nsec) / 1000000000.0)); +} + +static struct source reqsource(struct hthead *req) +{ + int i; + char *sa; + struct in_addr a4; + struct in6_addr a6; + struct source ret; + + ret = (struct source){}; + if((sa = getheader(req, "X-Ash-Address")) != NULL) { + if(inet_pton(AF_INET, sa, &a4) == 1) { + ret.type = AF_INET; + memcpy(ret.data, &a4, ret.len = sizeof(a4)); + } else if(inet_pton(AF_INET6, sa, &a6) == 1) { + ret.type = AF_INET6; + memcpy(ret.data, &a6, ret.len = sizeof(a6)); + } + } + for(i = 0, ret.hash = ret.type; i < ret.len; i++) + ret.hash = (ret.hash * 31) + ret.data[i]; + return(ret); +} + +static int srccmp(const struct source *a, const struct source *b) +{ + int c; + + if((c = a->len - b->len) != 0) + return(c); + if((c = a->type - b->type) != 0) + return(c); + return(memcmp(a->data, b->data, a->len)); +} + +static const char *formatsrc(const struct source *src) +{ + static char buf[128]; + struct in_addr a4; + struct in6_addr a6; + + switch(src->type) { + case AF_INET: + memcpy(&a4, src->data, sizeof(a4)); + if(!inet_ntop(AF_INET, &a4, buf, sizeof(buf))) + return(""); + return(buf); + case AF_INET6: + memcpy(&a6, src->data, sizeof(a6)); + if(!inet_ntop(AF_INET6, &a6, buf, sizeof(buf))) + return(""); + return(buf); + default: + return(""); + } +} + +static void rehash(int nlen) +{ + unsigned int i, o, n, m, pl, nl; + struct bucket **new, **old; + + old = buckets; + if(nlen <= SBUCKETS) { + nlen = SBUCKETS; + new = sbuckets; + } else { + new = szmalloc(sizeof(*new) * (1 << nlen)); + } + if(nlen == hashlen) + return; + assert(old != new); + pl = 1 << hashlen; nl = 1 << nlen; m = nl - 1; + for(i = 0; i < pl; i++) { + if(!old[i]) + continue; + for(o = old[i]->id.hash & m, n = 0; n < nl; o = (o + 1) & m, n++) { + if(!new[o]) { + new[o] = old[i]; + break; + } + } + } + if(old != sbuckets) + free(old); + buckets = new; + hashlen = nlen; +} + +static struct bucket *hashget(const struct source *src) +{ + unsigned int i, n, N, m; + struct bucket *bk; + + m = (N = (1 << hashlen)) - 1; + for(i = src->hash & m, n = 0; n < N; i = (i + 1) & m, n++) { + bk = buckets[i]; + if(bk && !srccmp(&bk->id, src)) + return(bk); + } + for(i = src->hash & m; buckets[i]; i = (i + 1) & m); + buckets[i] = bk = szmalloc(sizeof(*bk)); + memcpy(&bk->id, src, sizeof(*src)); + bk->last = bk->etime = now; + bk->thpos = -1; + bk->blocked = -1; + if(++nbuckets > (1 << (hashlen - 1))) + rehash(hashlen + 1); + return(bk); +} + +static void hashdel(struct bucket *bk) +{ + unsigned int i, o, p, n, N, m; + struct bucket *sb; + + m = (N = (1 << hashlen)) - 1; + for(i = bk->id.hash & m, n = 0; n < N; i = (i + 1) & m, n++) { + assert((sb = buckets[i]) != NULL); + if(!srccmp(&sb->id, &bk->id)) + break; + } + assert(sb == bk); + buckets[i] = NULL; + for(o = (i + 1) & m; buckets[o] != NULL; o = (o + 1) & m) { + sb = buckets[o]; + p = (sb->id.hash - i) & m; + if((p == 0) || (p > ((o - i) & m))) { + buckets[i] = sb; + buckets[o] = NULL; + i = o; + } + } + if(--nbuckets <= (1 << (hashlen - 3))) + rehash(hashlen - 1); +} + +static void thraise(struct btime bt, int n) +{ + int p; + + while(n > 0) { + p = (n - 1) >> 1; + if(timeheap.b[p].tm <= bt.tm) + break; + (timeheap.b[n] = timeheap.b[p]).bk->thpos = n; + n = p; + } + (timeheap.b[n] = bt).bk->thpos = n; +} + +static void thlower(struct btime bt, int n) +{ + int c1, c2, c; + + while(1) { + c2 = (c1 = (n << 1) + 1) + 1; + if(c1 >= timeheap.d) + break; + c = ((c2 < timeheap.d) && (timeheap.b[c2].tm < timeheap.b[c1].tm)) ? c2 : c1; + if(timeheap.b[c].tm > bt.tm) + break; + (timeheap.b[n] = timeheap.b[c]).bk->thpos = n; + n = c; + } + (timeheap.b[n] = bt).bk->thpos = n; +} + +static void thadjust(struct btime bt, int n) +{ + if((n > 0) && (timeheap.b[(n - 1) >> 1].tm > bt.tm)) + thraise(bt, n); + else + thlower(bt, n); +} + +static void freebucket(struct bucket *bk) +{ + int i, n; + struct btime r; + + hashdel(bk); + if((n = bk->thpos) >= 0) { + r = timeheap.b[--timeheap.d]; + if(n < timeheap.d) + thadjust(r, n); + } + for(i = 0; i < bk->brim.d; i++) { + freehthead(bk->brim.b[i].req); + close(bk->brim.b[i].fd); + } + buffree(bk->brim); + free(bk); +} + +static void updbtime(struct bucket *bk) +{ + double tm, tm2; + + tm = (bk->level == 0) ? (bk->etime + cf.retain) : (bk->last + (bk->level / cf.rate) + cf.retain); + if((bk->blocked > 0) && ((tm2 = bk->wtime + cf.warnrate) > tm)) + tm = tm2; + + if((bk->brim.d > 0) && ((tm2 = bk->last + ((bk->level - cf.size) / cf.rate)) < tm)) + tm = tm2; + if((bk->blocked > 0) && ((tm2 = bk->wtime + cf.warnrate) < tm)) + tm = tm2; + + if(bk->thpos < 0) { + sizebuf(timeheap, ++timeheap.d); + thraise((struct btime){bk, tm}, timeheap.d - 1); + } else { + thadjust((struct btime){bk, tm}, bk->thpos); + } +} + +static void tickbucket(struct bucket *bk) +{ + double delta, ll; + + delta = now - bk->last; + bk->last = now; + ll = bk->level; + if((bk->level -= delta * cf.rate) < 0) { + if(ll > 0) + bk->etime = now + (bk->level / cf.rate); + bk->level = 0; + } + while((bk->brim.d > 0) && (bk->level < cf.size)) { + if(sendreq(child, bk->brim.b[0].req, bk->brim.b[0].fd)) { + flog(LOG_ERR, "ratequeue: could not pass request to child: %s", strerror(errno)); + exit(1); + } + freehthead(bk->brim.b[0].req); + close(bk->brim.b[0].fd); + bufdel(bk->brim, 0); + bk->level += 1; + } + if((bk->blocked > 0) && (now - bk->wtime >= cf.warnrate)) { + flog(LOG_NOTICE, "ratequeue: blocked %i requests from %s", bk->blocked, formatsrc(&bk->id)); + bk->blocked = 0; + bk->wtime = now; + } +} + +static void checkbtime(struct bucket *bk) +{ + tickbucket(bk); + if((bk->level == 0) && (now >= bk->etime + cf.retain) && (bk->blocked <= 0)) { + freebucket(bk); + return; + } + updbtime(bk); +} + +static void serve(struct hthead *req, int fd) +{ + struct source src; + struct bucket *bk; + + now = rtime(); + src = reqsource(req); + bk = hashget(&src); + tickbucket(bk); + if(bk->level < cf.size) { + bk->level += 1; + if(sendreq(child, req, fd)) { + flog(LOG_ERR, "ratequeue: could not pass request to child: %s", strerror(errno)); + exit(1); + } + freehthead(req); + close(fd); + } else if(bk->brim.d < cf.brimsize) { + bufadd(bk->brim, ((struct waiting){.req = req, .fd = fd})); + } else { + if(bk->blocked < 0) { + flog(LOG_NOTICE, "ratequeue: blocking requests from %s", formatsrc(&bk->id)); + bk->blocked = 0; + bk->wtime = now; + } + simpleerror(fd, 429, "Too many requests", "Your client is being throttled for issuing too frequent requests."); + freehthead(req); + close(fd); + bk->blocked++; + } + updbtime(bk); +} + +static int parseint(const char *str, int *dst) +{ + long buf; + char *p; + + buf = strtol(str, &p, 0); + if((p == str) || *p) + return(-1); + *dst = buf; + return(0); +} + +static int parsefloat(const char *str, double *dst) +{ + double buf; + char *p; + + buf = strtod(str, &p); + if((p == str) || *p) + return(-1); + *dst = buf; + return(0); +} + +static int readconf(char *path, struct config *buf) +{ + FILE *fp; + struct cfstate *s; + int rv; + + if((fp = fopen(path, "r")) == NULL) { + flog(LOG_ERR, "ratequeue: %s: %s", path, strerror(errno)); + return(-1); + } + *buf = defcfg; + s = mkcfparser(fp, path); + rv = -1; + while(1) { + getcfline(s); + if(!strcmp(s->argv[0], "eof")) { + break; + } else if(!strcmp(s->argv[0], "size")) { + if((s->argc < 2) || parsefloat(s->argv[1], &buf->size)) { + flog(LOG_ERR, "%s:%i: missing or invalid `size' argument"); + goto err; + } + } else if(!strcmp(s->argv[0], "rate")) { + if((s->argc < 2) || parsefloat(s->argv[1], &buf->rate)) { + flog(LOG_ERR, "%s:%i: missing or invalid `rate' argument"); + goto err; + } + } else if(!strcmp(s->argv[0], "brim")) { + if((s->argc < 2) || parseint(s->argv[1], &buf->brimsize)) { + flog(LOG_ERR, "%s:%i: missing or invalid `brim' argument"); + goto err; + } + } else { + flog(LOG_WARNING, "%s:%i: unknown directive `%s'", s->file, s->lno, s->argv[0]); + } + } + rv = 0; +err: + freecfparser(s); + fclose(fp); + return(rv); +} + +static void huphandler(int sig) +{ + reload = 1; +} + +static void usage(FILE *out) +{ + fprintf(out, "usage: ratequeue [-h] [-s BUCKET-SIZE] [-r RATE] [-b BRIM-SIZE] PROGRAM [ARGS...]\n"); +} + +int main(int argc, char **argv) +{ + int c, rv; + int fd; + struct hthead *req; + struct pollfd pfd; + double timeout; + char *cfname; + struct config cfbuf; + + cf = defcfg; + cfname = NULL; + while((c = getopt(argc, argv, "+hc:s:r:b:")) >= 0) { + switch(c) { + case 'h': + usage(stdout); + return(0); + case 'c': + cfname = optarg; + break; + case 's': + parsefloat(optarg, &cf.size); + break; + case 'r': + parsefloat(optarg, &cf.rate); + break; + case 'b': + parseint(optarg, &cf.brimsize); + break; + } + } + if(argc - optind < 1) { + usage(stderr); + return(1); + } + if(cfname) { + if(readconf(cfname, &cfbuf)) + return(1); + cf = cfbuf; + } + if((child = stdmkchild(argv + optind, NULL, NULL)) < 0) { + flog(LOG_ERR, "ratequeue: could not fork child: %s", strerror(errno)); + return(1); + } + sigaction(SIGHUP, &(struct sigaction){.sa_handler = huphandler}, NULL); + while(1) { + if(reload) { + if(cfname) { + if(!readconf(cfname, &cfbuf)) + cf = cfbuf; + } + reload = 0; + } + now = rtime(); + pfd = (struct pollfd){.fd = 0, .events = POLLIN}; + timeout = (timeheap.d > 0) ? timeheap.b[0].tm : -1; + if((rv = poll(&pfd, 1, (timeout < 0) ? -1 : (int)((timeout + 0.1 - now) * 1000))) < 0) { + if(errno != EINTR) { + flog(LOG_ERR, "ratequeue: error in poll: %s", strerror(errno)); + exit(1); + } + } + if(pfd.revents) { + if((fd = recvreq(0, &req)) < 0) { + if(errno == EINTR) + continue; + if(errno != 0) + flog(LOG_ERR, "recvreq: %s", strerror(errno)); + break; + } + serve(req, fd); + } + while((timeheap.d > 0) && ((now = rtime()) >= timeheap.b[0].tm)) + checkbtime(timeheap.b[0].bk); + } + return(0); +} diff --git a/src/ssl-gnutls.c b/src/ssl-gnutls.c index 7aa1df0..5a11f94 100644 --- a/src/ssl-gnutls.c +++ b/src/ssl-gnutls.c @@ -54,8 +54,7 @@ struct ncredbuf { }; struct sslport { - int fd; - int sport; + int fd, sport, clreq; gnutls_certificate_credentials_t creds; gnutls_priority_t ciphers; struct namedcreds **ncreds; @@ -74,6 +73,11 @@ struct savedsess { gnutls_datum_t key, value; }; +struct certbuffer { + gnutls_x509_crt_t *b; + size_t s, d; +}; + static int numconn = 0, numsess = 0; static struct btree *sessidx = NULL; static struct savedsess *sesslistf = NULL, *sesslistl = NULL; @@ -267,6 +271,8 @@ static int initreq(struct conn *conn, struct hthead *req) struct sslconn *ssl = conn->pdata; struct sockaddr_storage sa; socklen_t salen; + gnutls_datum_t sessid; + char *esessid; headappheader(req, "X-Ash-Address", formathaddress((struct sockaddr *)&ssl->name, sizeof(sa))); if(ssl->name.ss_family == AF_INET) @@ -278,6 +284,43 @@ static int initreq(struct conn *conn, struct hthead *req) headappheader(req, "X-Ash-Server-Address", formathaddress((struct sockaddr *)&sa, sizeof(sa))); headappheader(req, "X-Ash-Server-Port", sprintf3("%i", ssl->port->sport)); headappheader(req, "X-Ash-Protocol", "https"); + if(gnutls_session_get_id2(ssl->sess, &sessid) == GNUTLS_E_SUCCESS) { + esessid = base64encode((void *)sessid.data, sessid.size); + headappheader(req, "X-Ash-TLS-Session", esessid); + free(esessid); + } + return(0); +} + +static int setcreds(gnutls_session_t sess) +{ + int i, o, u; + struct sslport *pd; + unsigned int ntype; + char nambuf[256]; + size_t namlen; + + pd = gnutls_session_get_ptr(sess); + for(i = 0; 1; i++) { + namlen = sizeof(nambuf); + if(gnutls_server_name_get(sess, nambuf, &namlen, &ntype, i) != 0) + break; + if(ntype != GNUTLS_NAME_DNS) + continue; + for(o = 0; pd->ncreds[o] != NULL; o++) { + for(u = 0; pd->ncreds[o]->names[u] != NULL; u++) { + if(!strcmp(pd->ncreds[o]->names[u], nambuf)) { + gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, pd->ncreds[o]->creds); + if(pd->clreq) + gnutls_certificate_server_set_request(sess, GNUTLS_CERT_REQUEST); + return(0); + } + } + } + } + gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, pd->creds); + if(pd->clreq) + gnutls_certificate_server_set_request(sess, GNUTLS_CERT_REQUEST); return(0); } @@ -290,34 +333,6 @@ static void servessl(struct muth *muth, va_list args) struct sslconn ssl; gnutls_session_t sess; int ret; - - int setcreds(gnutls_session_t sess) - { - int i, o, u; - unsigned int ntype; - char nambuf[256]; - size_t namlen; - - for(i = 0; 1; i++) { - namlen = sizeof(nambuf); - if(gnutls_server_name_get(sess, nambuf, &namlen, &ntype, i) != 0) - break; - if(ntype != GNUTLS_NAME_DNS) - continue; - for(o = 0; pd->ncreds[o] != NULL; o++) { - for(u = 0; pd->ncreds[o]->names[u] != NULL; u++) { - if(!strcmp(pd->ncreds[o]->names[u], nambuf)) { - gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, pd->ncreds[o]->creds); - gnutls_certificate_server_set_request(sess, GNUTLS_CERT_REQUEST); - return(0); - } - } - } - } - gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, pd->creds); - gnutls_certificate_server_set_request(sess, GNUTLS_CERT_REQUEST); - return(0); - } numconn++; fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); @@ -327,6 +342,7 @@ static void servessl(struct muth *muth, va_list args) gnutls_db_set_store_function(sess, sessdbstore); gnutls_db_set_remove_function(sess, sessdbdel); gnutls_db_set_ptr(sess, NULL); + gnutls_session_set_ptr(sess, pd); gnutls_handshake_set_post_client_hello_function(sess, setcreds); gnutls_transport_set_ptr(sess, (gnutls_transport_ptr_t)(intptr_t)fd); while((ret = gnutls_handshake(sess)) != 0) { @@ -421,20 +437,59 @@ static void init(void) } } -static struct namedcreds *readncreds(char *file) +/* This implementation seems somewhat ugly, but it's the way the + * GnuTLS implements the same thing internally, so it should probably + * be interoperable, at least. */ +static int readcrtchain(struct certbuffer *ret, struct charbuf *pem) +{ + static char *headers[] = {"-----BEGIN CERTIFICATE", "-----BEGIN X509 CERTIFICATE"}; + int i, rv; + char *p, *p2, *f; + gnutls_x509_crt_t crt; + + for(i = 0, p = NULL; i < sizeof(headers) / sizeof(*headers); i++) { + f = memmem(pem->b, pem->d, headers[i], strlen(headers[i])); + if((f != NULL) && ((p == NULL) || (f < p))) + p = f; + } + if(p == NULL) + return(-GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE); + do { + if((rv = gnutls_x509_crt_init(&crt)) < 0) + goto error; + if((rv = gnutls_x509_crt_import(crt, &(gnutls_datum_t){.data = (unsigned char *)p, .size = pem->d - (p - pem->b)}, GNUTLS_X509_FMT_PEM)) < 0) { + gnutls_x509_crt_deinit(crt); + goto error; + } + bufadd(*ret, crt); + for(i = 0, p2 = NULL; i < sizeof(headers) / sizeof(*headers); i++) { + f = memmem(p + 1, pem->d - (p + 1 - pem->b), headers[i], strlen(headers[i])); + if((f != NULL) && ((p2 == NULL) || (f < p2))) + p2 = f; + } + } while((p = p2) != NULL); + return(0); +error: + for(i = 0; i < ret->d; i++) + gnutls_x509_crt_deinit(ret->b[i]); + ret->d = 0; + return(rv); +} + +static struct namedcreds *readncreds(char *file, gnutls_x509_privkey_t defkey) { int i, fd, ret; struct namedcreds *nc; - gnutls_x509_crt_t crt; + struct certbuffer crts; gnutls_x509_privkey_t key; char cn[1024]; size_t cnl; - gnutls_datum_t d; struct charbuf keybuf; struct charvbuf names; unsigned int type; bufinit(keybuf); + bufinit(crts); bufinit(names); if((fd = open(file, O_RDONLY)) < 0) { flog(LOG_ERR, "ssl: %s: %s", file, strerror(errno)); @@ -452,15 +507,12 @@ static struct namedcreds *readncreds(char *file) keybuf.d += ret; } close(fd); - d.data = (unsigned char *)keybuf.b; - d.size = keybuf.d; - gnutls_x509_crt_init(&crt); - if((ret = gnutls_x509_crt_import(crt, &d, GNUTLS_X509_FMT_PEM)) != 0) { - flog(LOG_ERR, "ssl: could not load certificate from %s: %s", file, gnutls_strerror(ret)); + if((ret = readcrtchain(&crts, &keybuf)) != 0) { + flog(LOG_ERR, "ssl: could not load certificate chain from %s: %s", file, gnutls_strerror(ret)); exit(1); } cnl = sizeof(cn) - 1; - if((ret = gnutls_x509_crt_get_dn_by_oid(crt, GNUTLS_OID_X520_COMMON_NAME, 0, 0, cn, &cnl)) != 0) { + if((ret = gnutls_x509_crt_get_dn_by_oid(crts.b[0], GNUTLS_OID_X520_COMMON_NAME, 0, 0, cn, &cnl)) != 0) { flog(LOG_ERR, "ssl: could not read common name from %s: %s", file, gnutls_strerror(ret)); exit(1); } @@ -468,23 +520,28 @@ static struct namedcreds *readncreds(char *file) bufadd(names, sstrdup(cn)); for(i = 0; 1; i++) { cnl = sizeof(cn) - 1; - if(gnutls_x509_crt_get_subject_alt_name2(crt, i, cn, &cnl, &type, NULL) < 0) + if(gnutls_x509_crt_get_subject_alt_name2(crts.b[0], i, cn, &cnl, &type, NULL) < 0) break; cn[cnl] = 0; if(type == GNUTLS_SAN_DNSNAME) bufadd(names, sstrdup(cn)); } gnutls_x509_privkey_init(&key); - if((ret = gnutls_x509_privkey_import(key, &d, GNUTLS_X509_FMT_PEM)) != 0) { - flog(LOG_ERR, "ssl: could not load key from %s: %s", file, gnutls_strerror(ret)); - exit(1); + if((ret = gnutls_x509_privkey_import(key, &(gnutls_datum_t){.data = (unsigned char *)keybuf.b, .size = keybuf.d}, GNUTLS_X509_FMT_PEM)) != 0) { + if(ret == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { + gnutls_x509_privkey_deinit(key); + key = defkey; + } else { + flog(LOG_ERR, "ssl: could not load key from %s: %s", file, gnutls_strerror(ret)); + exit(1); + } } buffree(keybuf); bufadd(names, NULL); omalloc(nc); nc->names = names.b; gnutls_certificate_allocate_credentials(&nc->creds); - if((ret = gnutls_certificate_set_x509_key(nc->creds, &crt, 1, key)) != 0) { + if((ret = gnutls_certificate_set_x509_key(nc->creds, crts.b, crts.d, key)) != 0) { flog(LOG_ERR, "ssl: could not use certificate from %s: %s", file, gnutls_strerror(ret)); exit(1); } @@ -492,7 +549,7 @@ static struct namedcreds *readncreds(char *file) return(nc); } -static void readncdir(struct ncredbuf *buf, char *dir) +static void readncdir(struct ncredbuf *buf, char *dir, gnutls_x509_privkey_t defkey) { DIR *d; struct dirent *e; @@ -509,23 +566,28 @@ static void readncdir(struct ncredbuf *buf, char *dir) continue; if(strcmp(e->d_name + es - 4, ".crt")) continue; - bufadd(*buf, readncreds(sprintf3("%s/%s", dir, e->d_name))); + bufadd(*buf, readncreds(sprintf3("%s/%s", dir, e->d_name), defkey)); } closedir(d); } void handlegnussl(int argc, char **argp, char **argv) { - int i, ret, port, fd; + int i, ret, port, fd, clreq; gnutls_certificate_credentials_t creds; gnutls_priority_t ciphers; + gnutls_x509_privkey_t defkey; struct ncredbuf ncreds; + struct charvbuf ncertf, ncertd; struct sslport *pd; char *crtfile, *keyfile, *perr; init(); port = 443; + clreq = 0; bufinit(ncreds); + bufinit(ncertf); + bufinit(ncertd); gnutls_certificate_allocate_credentials(&creds); keyfile = crtfile = NULL; ciphers = NULL; @@ -589,6 +651,7 @@ void handlegnussl(int argc, char **argp, char **argv) exit(1); } } + clreq = 1; } else if(!strcmp(argp[i], "crl")) { if((ret = gnutls_certificate_set_x509_crl_file(creds, argv[i], GNUTLS_X509_FMT_PEM)) != 0) { flog(LOG_ERR, "ssl: could not load CRL file `%s': %s", argv[i], gnutls_strerror(ret)); @@ -600,12 +663,13 @@ void handlegnussl(int argc, char **argp, char **argv) exit(1); } } + clreq = 1; } else if(!strcmp(argp[i], "port")) { port = atoi(argv[i]); } else if(!strcmp(argp[i], "ncert")) { - bufadd(ncreds, readncreds(argv[i])); + bufadd(ncertf, argv[i]); } else if(!strcmp(argp[i], "ncertdir")) { - readncdir(&ncreds, argv[i]); + bufadd(ncertd, argv[i]); } else { flog(LOG_ERR, "unknown parameter `%s' to ssl handler", argp[i]); exit(1); @@ -629,11 +693,22 @@ void handlegnussl(int argc, char **argp, char **argv) flog(LOG_ERR, "ssl: could not initialize cipher priorities: %s", gnutls_strerror(ret)); exit(1); } + if((ret = gnutls_certificate_get_x509_key(creds, 0, &defkey)) != 0) { + flog(LOG_ERR, "ssl: could not get default key: %s", gnutls_strerror(ret)); + exit(1); + } + for(i = 0; i < ncertf.d; i++) + bufadd(ncreds, readncreds(ncertf.b[i], defkey)); + for(i = 0; i < ncertd.d; i++) + readncdir(&ncreds, ncertd.b[i], defkey); + buffree(ncertf); + buffree(ncertd); gnutls_certificate_set_dh_params(creds, dhparams()); bufadd(ncreds, NULL); omalloc(pd); pd->fd = fd; pd->sport = port; + pd->clreq = clreq; pd->creds = creds; pd->ncreds = ncreds.b; pd->ciphers = ciphers; diff --git a/src/ssl-openssl.c b/src/ssl-openssl.c new file mode 100644 index 0000000..8fe7b9e --- /dev/null +++ b/src/ssl-openssl.c @@ -0,0 +1,308 @@ +/* + ashd - A Sane HTTP Daemon + Copyright (C) 2008 Fredrik Tolf + + This 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 . +*/ + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include + +#include "htparser.h" + +#ifdef HAVE_OPENSSL + +#include +#include + +struct sslport { + int fd, sport; + SSL_CTX *ctx; +}; + +struct sslconn { + struct sslport *port; + int fd; + SSL *ssl; + struct sockaddr *name; + socklen_t namelen; +}; + +static int tlsblock(int fd, int err, int to) +{ + if(err == SSL_ERROR_WANT_READ) { + if(block(fd, EV_READ, to) <= 0) + return(1); + return(0); + } else if(err == SSL_ERROR_WANT_WRITE) { + if(block(fd, EV_WRITE, to) <= 0) + return(1); + return(0); + } else { + return(1); + } +} + +static ssize_t sslread(void *cookie, void *buf, size_t len) +{ + struct sslconn *sdat = cookie; + int ret, err, nb; + size_t off; + + off = 0; + while(off < len) { + nb = ((len - off) > INT_MAX) ? INT_MAX : (len - off); + if((ret = SSL_read(sdat->ssl, buf, nb)) <= 0) { + if(off > 0) + return(off); + err = SSL_get_error(sdat->ssl, ret); + if(err == SSL_ERROR_ZERO_RETURN) { + return(0); + } else if((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { + if(tlsblock(sdat->fd, err, 60)) { + errno = ETIMEDOUT; + return(-1); + } + } else { + if(err != SSL_ERROR_SYSCALL) + errno = EPROTO; + return(-1); + } + } else { + off += ret; + } + } + return(off); +} + +static ssize_t sslwrite(void *cookie, const void *buf, size_t len) +{ + struct sslconn *sdat = cookie; + int ret, err, nb; + size_t off; + + off = 0; + while(off < len) { + nb = ((len - off) > INT_MAX) ? INT_MAX : (len - off); + if((ret = SSL_write(sdat->ssl, buf, nb)) <= 0) { + if(off > 0) + return(off); + err = SSL_get_error(sdat->ssl, ret); + if((err == SSL_ERROR_WANT_READ) || (err == SSL_ERROR_WANT_WRITE)) { + if(tlsblock(sdat->fd, err, 60)) { + errno = ETIMEDOUT; + return(-1); + } + } else { + if(err != SSL_ERROR_SYSCALL) + errno = EIO; + return(-1); + } + } else { + off += ret; + } + } + return(off); +} + +static int sslclose(void *cookie) +{ + return(0); +} + +static struct bufioops iofuns = { + .read = sslread, + .write = sslwrite, + .close = sslclose, +}; + +static int initreq(struct conn *conn, struct hthead *req) +{ + struct sslconn *sdat = conn->pdata; + struct sockaddr_storage sa; + socklen_t salen; + + headappheader(req, "X-Ash-Address", formathaddress(sdat->name, sdat->namelen)); + if(sdat->name->sa_family == AF_INET) + headappheader(req, "X-Ash-Port", sprintf3("%i", ntohs(((struct sockaddr_in *)sdat->name)->sin_port))); + else if(sdat->name->sa_family == AF_INET6) + headappheader(req, "X-Ash-Port", sprintf3("%i", ntohs(((struct sockaddr_in6 *)sdat->name)->sin6_port))); + salen = sizeof(sa); + if(!getsockname(sdat->fd, (struct sockaddr *)&sa, &salen)) + headappheader(req, "X-Ash-Server-Address", formathaddress((struct sockaddr *)&sa, salen)); + headappheader(req, "X-Ash-Server-Port", sprintf3("%i", sdat->port->sport)); + headappheader(req, "X-Ash-Protocol", "https"); + return(0); +} + +static void servessl(struct muth *muth, va_list args) +{ + vavar(int, fd); + vavar(struct sockaddr_storage, name); + vavar(struct sslport *, pd); + int ret; + SSL *ssl; + struct conn conn; + struct sslconn sdat; + + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + ssl = SSL_new(pd->ctx); + SSL_set_fd(ssl, fd); + while((ret = SSL_accept(ssl)) <= 0) { + if(tlsblock(fd, SSL_get_error(ssl, ret), 60)) + goto out; + } + memset(&conn, 0, sizeof(conn)); + memset(&sdat, 0, sizeof(sdat)); + conn.pdata = &sdat; + conn.initreq = initreq; + sdat.port = pd; + sdat.fd = fd; + sdat.ssl = ssl; + sdat.name = (struct sockaddr *)&name; + sdat.namelen = sizeof(name); + serve(bioopen(&sdat, &iofuns), fd, &conn); + while((ret = SSL_shutdown(ssl)) < 0) { + if(tlsblock(fd, SSL_get_error(ssl, ret), 60)) + goto out; + } +out: + SSL_free(ssl); + close(fd); +} + +static void listenloop(struct muth *muth, va_list args) +{ + vavar(struct sslport *, pd); + int i, ns, n; + struct sockaddr_storage name; + socklen_t namelen; + + fcntl(pd->fd, F_SETFL, fcntl(pd->fd, F_GETFL) | O_NONBLOCK); + while(1) { + namelen = sizeof(name); + if(block(pd->fd, EV_READ, 0) == 0) + goto out; + for(n = 0; n < 100; n++) { + if((ns = accept(pd->fd, (struct sockaddr *)&name, &namelen)) < 0) { + if(errno == EAGAIN) + break; + if(errno == ECONNABORTED) + continue; + flog(LOG_ERR, "accept: %s", strerror(errno)); + goto out; + } + mustart(servessl, ns, name, pd); + } + } + +out: + close(pd->fd); + free(pd); + for(i = 0; i < listeners.d; i++) { + if(listeners.b[i] == muth) + bufdel(listeners, i); + } +} + +void handleossl(int argc, char **argp, char **argv) +{ + int i, port, fd; + SSL_CTX *ctx; + char *crtfile, *keyfile; + struct sslport *pd; + + ctx = SSL_CTX_new(TLS_server_method()); + if(!ctx) { + flog(LOG_ERR, "ssl: could not create context: %s", ERR_error_string(ERR_get_error(), NULL)); + exit(1); + } + port = 443; + for(i = 0; i < argc; i++) { + if(!strcmp(argp[i], "help")) { + printf("ssl handler parameters:\n"); + printf("\tcert=CERT-FILE [mandatory]\n"); + printf("\t\tThe name of the file to read the certificate from.\n"); + printf("\tkey=KEY-FILE [same as CERT-FILE]\n"); + printf("\t\tThe name of the file to read the private key from.\n"); + printf("\tport=PORT [443]\n"); + printf("\t\tThe TCP port to listen on.\n"); + exit(0); + } else if(!strcmp(argp[i], "cert")) { + crtfile = argv[i]; + } else if(!strcmp(argp[i], "key")) { + keyfile = argv[i]; + } else if(!strcmp(argp[i], "port")) { + port = atoi(argv[i]); + } else { + flog(LOG_ERR, "unknown parameter `%s' to ssl handler", argp[i]); + exit(1); + } + } + if(crtfile == NULL) { + flog(LOG_ERR, "ssl: needs certificate file at the very least"); + exit(1); + } + if(keyfile == NULL) + keyfile = crtfile; + if(SSL_CTX_use_certificate_file(ctx, crtfile, SSL_FILETYPE_PEM) <= 0) { + flog(LOG_ERR, "ssl: could not load certificate: %s", ERR_error_string(ERR_get_error(), NULL)); + exit(1); + } + if(SSL_CTX_use_PrivateKey_file(ctx, keyfile, SSL_FILETYPE_PEM) <= 0) { + flog(LOG_ERR, "ssl: could not load certificate: %s", ERR_error_string(ERR_get_error(), NULL)); + exit(1); + } + if(!SSL_CTX_check_private_key(ctx)) { + flog(LOG_ERR, "ssl: key and certificate do not match"); + exit(1); + } + if((fd = listensock6(port)) < 0) { + flog(LOG_ERR, "could not listen on IPv65 port (port %i): %s", port, strerror(errno)); + exit(1); + } + omalloc(pd); + pd->fd = fd; + pd->sport = port; + pd->ctx = ctx; + bufadd(listeners, mustart(listenloop, pd)); + if((fd = listensock4(port)) < 0) { + if(errno != EADDRINUSE) { + flog(LOG_ERR, "could not listen on IPv4 port (port %i): Is", port, strerror(errno)); + exit(1); + } + } else { + omalloc(pd); + pd->fd = fd; + pd->sport = port; + pd->ctx = ctx; + bufadd(listeners, mustart(listenloop, pd)); + } +} + +#endif