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:
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
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])
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])
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
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
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
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)
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
-------
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
--------
--- /dev/null
+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 <fredrik@dolda2000.com>
+
+SEE ALSO
+--------
+*ashd*(7)
*-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
------
'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
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
-include dirplex.d/*.rc
-
index-file index
+dot-allow .well-known
+
+include dirplex.d/*.rc
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
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
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
# 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
def handle(self, request):
raise Exception()
def ckflush(self, req):
+ p = select.poll()
+ p.register(req, select.POLLOUT)
while len(req.buffer) > 0:
- rls, wls, els = select.select([], [req], [req])
+ p.poll()
req.flush()
def close(self):
pass
#!/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
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):
-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 *
--- /dev/null
+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)
def handle(self, request):
raise Exception()
def ckflush(self, req):
+ p = select.poll()
+ p.register(req, select.POLLOUT)
while len(req.buffer) > 0:
- rls, wls, els = select.select([], [req], [req])
+ p.poll()
req.flush()
def close(self):
pass
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:
/psendfile
/httimed
/httrcall
+/htpipe
+/ratequeue
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@
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;
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);
freepattern(pat);
}
freeca(cf->index);
+ freeca(cf->dotallow);
if(cf->capture != NULL)
free(cf->capture);
if(cf->reparse != NULL)
} 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);
#include <ctype.h>
#include <dirent.h>
#include <time.h>
+#include <fnmatch.h>
#include <sys/wait.h>
#include <sys/signal.h>
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;
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);
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));
time_t mtime, lastck;
struct child *children;
struct pattern *patterns;
- char **index;
+ char **index, **dotallow;
char *capture, *reparse;
int caproot, parsecomb;
};
#include "htparser.h"
static int plex;
-static char *pidfile = NULL;
static int daemonize, usesyslog;
struct mtbuf listeners;
/* 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);
{
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) {
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);
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);
}
}
}
if(pidout != NULL) {
fprintf(pidout, "%i\n", getpid());
- fclose(pidout);
+ fflush(pidout);
}
d = 0;
while(!d) {
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);
}
#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;
--- /dev/null
+/*
+ ashd - A Sane HTTP Daemon
+ Copyright (C) 2008 Fredrik Tolf <fredrik@dolda2000.com>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <err.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <utils.h>
+#include <log.h>
+#include <req.h>
+#include <proc.h>
+
+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);
+}
#include <errno.h>
#include <ctype.h>
#include <regex.h>
+#include <limits.h>
#include <sys/wait.h>
#ifdef HAVE_CONFIG_H
#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;
char *childnm;
struct rule **rules;
char *restpat;
+ int handler, prio, disable;
};
static struct config *gconfig, *lconfig;
} 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);
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);
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);
}
}
-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;
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;
}
}
} 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)
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)
--- /dev/null
+/*
+ ashd - A Sane HTTP Daemon
+ Copyright (C) 2008 Fredrik Tolf <fredrik@dolda2000.com>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <time.h>
+#include <signal.h>
+#include <assert.h>
+#include <sys/poll.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <utils.h>
+#include <log.h>
+#include <req.h>
+#include <resp.h>
+#include <proc.h>
+#include <cf.h>
+
+#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("<invalid ipv4>");
+ return(buf);
+ case AF_INET6:
+ memcpy(&a6, src->data, sizeof(a6));
+ if(!inet_ntop(AF_INET6, &a6, buf, sizeof(buf)))
+ return("<invalid ipv6>");
+ return(buf);
+ default:
+ return("<invalid source record>");
+ }
+}
+
+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);
+}
};
struct sslport {
- int fd;
- int sport;
+ int fd, sport, clreq;
gnutls_certificate_credentials_t creds;
gnutls_priority_t ciphers;
struct namedcreds **ncreds;
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;
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)
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);
}
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);
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) {
}
}
-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));
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);
}
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);
}
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;
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;
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));
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);
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;
--- /dev/null
+/*
+ ashd - A Sane HTTP Daemon
+ Copyright (C) 2008 Fredrik Tolf <fredrik@dolda2000.com>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+#include <utils.h>
+#include <mt.h>
+#include <mtio.h>
+#include <req.h>
+#include <log.h>
+#include <bufio.h>
+
+#include "htparser.h"
+
+#ifdef HAVE_OPENSSL
+
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+
+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