crossroads

Git mirror of https://crossroads.e-tunity.com/
git clone git://git.finwo.net/app/crossroads
Log | Files | Refs | LICENSE

commit 9cd7d67d498bd3e2093eb453e1c58f307ad42136
parent 778f28485845813047ba9b1745ea965e85dc71bd
Author: finwo <finwo@pm.me>
Date:   Sat,  3 Jan 2026 19:36:21 +0100

2.31

Diffstat:
MChangeLog | 44++++++++++++++++++++++++++++++++++++++------
MMakefile | 34++++++++++++++++++++++++++++++----
Mdoc/xr.odt | 0
Mdoc/xr.pdf | 0
Atest/ntimes | 21+++++++++++++++++++++
Mtest/sampleconf.xml | 230++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Atest/test.cgi | 41+++++++++++++++++++++++++++++++++++++++++
Mxr/Makefile | 1+
Mxr/ThreadsAndMutexes/thread/start.cc | 2++
Mxr/backend/backend | 34+++++++++++++++++++++++++++++-----
Mxr/backend/backend1.cc | 2+-
Mxr/backend/backend2.cc | 2+-
Mxr/backend/check.cc | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mxr/backend/connect.cc | 15++-------------
Axr/backendcheck/backendcheck | 45+++++++++++++++++++++++++++++++++++++++++++++
Axr/backendcheck/backendcheck1.cc | 6++++++
Axr/backendcheck/description.cc | 39+++++++++++++++++++++++++++++++++++++++
Axr/backendcheck/parse.cc | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Axr/backendcheck/setting.cc | 22++++++++++++++++++++++
Mxr/backenddef/backenddef | 34++++++++++++++++++++--------------
Dxr/balancer/addbackend.cc | 7-------
Axr/balancer/addbackend1.cc | 6++++++
Axr/balancer/addbackend2.cc | 14++++++++++++++
Mxr/balancer/balancer | 6+++++-
Axr/balancer/deletebackend.cc | 15+++++++++++++++
Mxr/balancer/init.cc | 2++
Mxr/balancer/serve.cc | 31+++++++++++++++++++------------
Axr/config/changeallow.cc | 12++++++++++++
Axr/config/changedeny.cc | 12++++++++++++
Mxr/config/config | 35+++++++++++++++++++++++++++++++----
Mxr/config/config1.cc | 3+++
Axr/config/deleteallow.cc | 10++++++++++
Axr/config/deletedeny.cc | 10++++++++++
Mxr/config/parsecmdline.cc | 23+++++++++++++++++++++--
Mxr/config/setbackend.cc | 13+++++++++----
Axr/dnsentry/dnsentry | 18++++++++++++++++++
Axr/dnsentry/resolve.cc | 20++++++++++++++++++++
Mxr/etc/Makefile.class | 3++-
Mxr/etc/status.xslt | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mxr/etc/usage.txt | 60++++++++++++++++++++++++++++++++++++++++++------------------
Mxr/fdset/readable.cc | 15++++++++++++++-
Mxr/fdset/writeable.cc | 2++
Mxr/httpbuffer/httpbuffer | 7+++++++
Axr/httpbuffer/setheader1.cc | 16++++++++++++++++
Mxr/httpdispatcher/dispatch.cc | 2+-
Mxr/httpdispatcher/handle.cc | 2+-
Mxr/httpdispatcher/senderrorpage.cc | 2++
Mxr/netbuffer/copy.cc | 3++-
Mxr/netbuffer/netbuffer | 1+
Mxr/netbuffer/netbuffer4.cc | 9+--------
Mxr/netbuffer/netread.cc | 3++-
Axr/netbuffer/setstring.cc | 10++++++++++
Axr/netbuffer/stringat.cc | 11+++++++++++
Mxr/sys/main.cc | 4++++
Mxr/sys/str2parts.cc | 25+++++++++++++++++++++----
Mxr/sys/sys | 1+
Mxr/tcpdispatcher/dispatch.cc | 5+++++
Mxr/tcpdispatcher/execute.cc | 33+++++++++++++++++++++++++++++++--
Mxr/tcpdispatcher/tcpdispatcher1.cc | 3+++
Mxr/webinterface/answer.cc | 222++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mxr/webinterface/answerstatus.cc | 43+++++++++++++++++++++++++++++++++++++++++--
Mxrctl/xrctl | 1041++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
62 files changed, 2128 insertions(+), 629 deletions(-)

diff --git a/ChangeLog b/ChangeLog @@ -1,9 +1,41 @@ -2.30 [KK 2008-10-25] -- Reversioned to 2.30 in prepration for STABLE release. -- Bugfix in Netbuffer::netwrite() (debug output of written data) -- SIGPIPE gets ignored, see sys/main.cc -- Fixed re-entrancy issues for gethostbyname() that applies to some - unices. See Backend::connect() (xr/backend/connect.cc). +2.31 [KK 2008-10-30] +- Changes related to XML-style configuration file support. A + new-style xrctl is in provided and during "make install" put in + BINDIR (normally: /usr/sbin). See test/sampleconf.xml for an example + of a configuration file. +- Added webinterface URI's to control DOS-protection related settings. +- Added DOS-protection variables to XML-output of the web interface. +- Server-wide maxconnection tag output moved into dosprotection block. +- Added display of such variables to the style sheet that renders the + XML in a browser. +- Added option close-sockets-fast to XML output and to website URI + controls. +- Added allow-from and deny-from lists to XML output and to website + URI controls. +- Added the primary distribution site to the top-level Makefile as + macro. This now shows up in "xr -V". The version ID and site also + show up at the bottom of the web interface screen. +- Target "uninstall" added to the top-level Makefile. +- Bugfix in Netbuffer::netwrite(). When the remote connection would be + hung up, XR would be blissfully unaware. SIGPIPE signals are now ignored. +- Implemented flag -g / --backend-check. Alternatives: + connect:ip:port, get:ip:port[/uri], external:program. +- Added back end check type to the web interface reports, created + control at web interface for /backend/NR/backendcheck/VALUE to + change it. Added to XML configuration parsing. +- Docs updated, ofc. +- Status of balancer shown in web interface. Minor bugfix in xrctl. +- Added flags -E/-e (hard/soft-maxconn-excess, to call an external + program). Also added to web interface with controls and updated docs. +- Added mutex locks around cerr output catch-blocks of exceptions. +- Added more checks for memory allocation faults. +- Implemented DNS caching of back end host names (flag -F, + --dns-cache-timeout). Also implemented in web interface output and + controls. +- Implemented adding and/or deleting back ends from the user + interface, including scripting URI's. +- Implemented generation of a new configuration using "xrctl + generateconfig". 2.22 [KK 2008-10-16] - Implemented up/down state in back ends. Fixed up the docs. diff --git a/Makefile b/Makefile @@ -1,11 +1,12 @@ # Top-level Makefile for XR # ------------------------- -VER = 2.30 +VER = 2.31 BINDIR = /usr/sbin TAR = /tmp/crossroads-$(VER).tar.gz AUTHOR = Karel Kubat <karel@kubat.nl> MAINTAINER = Karel Kubat <karel@kubat.nl> +DISTSITE = http://crossroads.e-tunity.com BASE = $(shell pwd) foo: @@ -14,6 +15,7 @@ foo: @echo ' make local - local program construction' @echo ' make localprof - local, with profiling info' @echo ' make install - installation to $(BINDIR)' + @echo ' make uninstall - removes installed programs' @echo ' make clean - removal after local/install' @echo ' make tar - pack sources in an archive' @echo ' make commit - commit to repository (maintainer only)' @@ -24,22 +26,46 @@ local: xr/etc/gettools /usr/local/bin xr/etc c-conf e-ver xr/etc/e-ver ChangeLog $(VER) BASE=$(BASE) AUTHOR='$(AUTHOR)' MAINTAINER='$(MAINTAINER)' \ + DISTSITE='$(DISTSITE)' \ VER='$(VER)' PROF=$(PROF) PROFILER=$(PROFILER) $(MAKE) -C xr localprof: PROF=-pg PROFILER=-DPROFILER make local -install: local +install: local $(BINDIR)/xrctl mkdir -p $(BINDIR) BASE=$(BASE) AUTHOR='$(AUTHOR)' MAINTAINER='$(MAINTAINER)' \ + DISTSITE='$(DISTSITE)' \ VER='$(VER)' BINDIR=$(BINDIR) $(MAKE) -C xr install @echo @echo ' The balancer program xr is now installed to $(BINDIR).' - @echo ' Consider configuring xrctl/xrctl and copying it to $(BINDIR).' - @echo ' The helper xrctl is not installed automatically!!' + @echo ' The control script xrctl is installed there too. In order to' + @echo ' use it, you will have to create /etc/xrctl.xml (if you have' + @echo ' not done so yet). See test/sampleconf.xml for an example.' + @echo @echo ' Have fun with Crossroads $(VER),' @echo ' -- $(MAINTAINER)' @echo +$(BINDIR)/xrctl: xrctl/xrctl + cp xrctl/xrctl $(BINDIR)/xrctl + chmod +x $(BINDIR)/xrctl + +uninstall: + rm -f $(BINDIR)/xr $(BINDIR)/xrctl + @echo + @echo 'The balancer binary xr and the control script xrctl have been' + @echo 'removed from $(BINDIR).' + @echo + @if [ -f /etc/xrctl.xml ] ; then \ + echo 'The configuration /etc/xrctl.xml still exists. Remove this' ; \ + echo 'by hand if you are sure you will not be needing it.'; \ + else \ + echo 'Configuration /etc/xrctl.xml was not found. Maybe you have'; \ + echo 'it in a different location or under a different name.'; \ + echo 'If so, consider removing it by hand.'; \ + fi; + @echo + @echo 'XR was uninstalled!' clean: rm -rf xr/build/* diff --git a/doc/xr.odt b/doc/xr.odt Binary files differ. diff --git a/doc/xr.pdf b/doc/xr.pdf Binary files differ. diff --git a/test/ntimes b/test/ntimes @@ -0,0 +1,21 @@ +#!/usr/bin/perl + +# ntimes <command> - fork and run it + +die ("Usage: ntimes TIMES COMMAND\n", + "Forks TIMES and each fork runs the COMMAND.\n") if ($#ARGV != 1); +for my $i (1..$ARGV[0]) { + my $pid = fork(); + die ("$0: cannot fork, $!\n") unless (defined($pid)); + if (!$pid) { + system($ARGV[1]); + exit(); + } +} + +while (1) { + my $kid = wait(); + last if ($kid < 1); + print ("$0: Child $kid terminated\n"); +} +print ("All forks have finished, done.\n"); diff --git a/test/sampleconf.xml b/test/sampleconf.xml @@ -1,53 +1,185 @@ -<!-- Sample XR Configuration. - Just doodling around here for an XML format for the config. - No relevance (yet). --> +<?xml version="1.0" encoding="UTF-8"> <configuration> - - <!-- Global settings, applicable to all services --> - - <piddir>/var/run</piddir> - <uselogger>true</uselogger> - <logdir>/var/log</logdir> - <maxlogsize>100000</maxlogsize> - <path> - <dir>/bin</dir> - <dir>/sbin</dir> - <dir>/usr/bin</dir> - <dir>/usr/sbin</dir> - <dir>/usr/local/bin</dir> - <dir>/usr/local/sbin</dir> - <dir>/opt/local/bin</dir> - <dir>/opt/local/sbin</dir> - </path> - - <!-- Service descriptors --> - - <service name="web"> - <!-- Multi-host balancing. "www.onesite.org" or anything matching - "onesite" goes to the 10.1.1 back ends. Anything matching - "othersite" goes to the 10.1.9 back ends. --> - <server>http:0:81</server> - <dispatchmode>least-connections</dispatchmode> - <backends hostmatch="onesite"> - <backend> - <address>10.1.1.1:80</address> - <weight>5</weight> - </backend> - <backend> - <address>10.1.1.2:80</address> - <maxconnections>10</maxconnections> - </backend> - </backends> - <backends hostmatch="othersite"> - <backend> - <address>10.1.9.1:80</address> - </backend> - <backend> - <address>10.1.9.2:80</address> - </backend> - </backends> - <verbose>true</verbose> + + <!-- General system configuration section --> + + <system> + <!-- Where do PID files get stored? --> + <piddir>/var/run</piddir> + <!-- "ps" command that shows the PID and command. On Solaris, use + /usr/bin/ps -ef "pid comm" --> + <pscmd>/bin/ps ax -o pid,command</pscmd> + <!-- Use "logger" to add output to syslog or not? Logger will be + used if the binary can be found, and if uselogger is true. --> + <uselogger>true</uselogger> + <!-- If logger is not used: where do logs get written? --> + <logdir>/var/log</logdir> + <!-- If logger is not used: how big may the logs become? + Manipulated during "xrctl rotate". --> + <maxlogsize>100000</maxlogsize> + <!-- If logger is not used: how many history logs to keep? --> + <loghistory>10</loghistory> + <!-- Path where the "xr" binary is searched, and zippers as "gzip" + and "bzip2" --> + <path>/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/opt/local/bin:/opt/local/sbin</path> + </system> + + <!-- Service descriptions: This section defines all balancing + services that you want to start. Each service will lead to one + invocation of "xr". --> + + <!-- Very simple TCP service that dispatches SSH connections on + port 20.000 to three back ends. Most options are left to + their defaults. --> + <service> + <!-- Service name, must be unique --> + <name>ssh</name> + <server> + <!-- Type (tcp/http, here: tcp), and IP-address/port to bind + to. Use "0" for IP-address to bind to all interfaces. The + web interface will listen to localhost, port 20.001. --> + <type>tcp</type> + <address>0:20000</address> + <webinterface>0:20001</webinterface> + <!-- Clients may be idle for 30 minutes, then they are logged + out. --> + <clienttimeout>1800</clienttimeout> + </server> + + <!-- Back ends for the service. --> + <backend> + <!-- IP:port to dispatch to. --> + <address>server1:22</address> + </backend> + <backend> + <address>server2:22</address> + </backend> + <backend> + <address>server2:22</address> + </backend> </service> - + + <!-- Here is an HTTP service for web balancing. It shows more + advanced features. --> + <service> + <name>webone</name> + + <!-- Balancer server description --> + <server> + <!-- Server binding. XR will listen to any IP interface, on port + 20.010. It'll be an HTTP balancer. The web interface will + be on port 20.011. --> + <address>0:20010</address> + <type>http</type> + <webinterface>127.0.0.1:20011</webinterface> + + <!-- A non-default dispatch mode, here: by client IP.--> + <dispatchmode>lax-hashed-ip</dispatchmode> + + <!-- Checks. Dead back ends are checked each 3 seconds. There is + no checking of dead and live back ends (checkupinterval 0). --> + <checks> + <wakeupinterval>3</wakeupinterval> + <checkupinterval>0</checkupinterval> + </checks> + + <debugging> + <!-- Let's go with full messaging: verbose, debug, and logging + of transmitted messages. --> + <verbose>yes</verbose> + <debug>yes</debug> + <logtrafficdir>/tmp</logtrafficdir> + </debugging> + + <!-- If the balancer runs out of sockets because too many + closing connections are in TIME_WAIT state, use: --> + <closesocketsfast>yes</closesocketsfast> + + <!-- Access restrictions: we allow from two IP ranges, and deny + from one IP address. The overall results:will be: + - Access will be allowed from 10.*.*.* + - And allowed from 192.168.1.*, but not from 192.168.1.100 --> + <acl> + <allowfrom>10.255.255.255</allowfrom> + <allowfrom>192.168.1.255</allowfrom> + <denyfrom>192.168.1.100</denyfrom> + </acl> + + <dosprotection> + <!-- Here is some basic DOS protection. Connections from IP's + are counted over timeinterval seconds (here: 2 sec). When a + client exceeds the hard limit hardmaxconnrate (here: 200), + then it is denied access. When it exceeds the soft limit + softmaxconnrate (here: 150), then each connection is + delayed for defertime microsecs (here: 1.000.000, one + sec). + Finally, the entire balancer will be allowed to serve up + to 400 simultaneous connections. + --> + <timeinterval>2</timeinterval> + <hardmaxconnrate>200</hardmaxconnrate> + <softmaxconnrate>150</softmaxconnrate> + <defertime>1000000</defertime> + <maxconnections>400</maxconnections> + + <!-- Let's add some more protection. When a user exceeds their + hard maxconn rate, "/path/to/program" will be invoked + with the IP as argument. That program may eg. call + iptables to block the client. There is also a tag + softmaxconnexcess (not shown here). --> + <hardmaxconnexcess>/path/to/program</hardmaxconnexcess> + + </dosprotection> + + <http> + <!-- Since this is an HTTP balancer, let's add some goodies: + no header for the XR version, + a header X-Forwarded-For: client-ip + no sticky http sessions + two serverheaders to insert --> + <addxrversion>off</addxrversion> + <addxforwardedfor>on</addxforwardedfor> + <stickyhttp>off</stickyhttp> + <serverheaders> + <header>MyFirstHeader: Whatever</header> + <header>MySecondHeader: WhateverElse</header> + </serverheaders> + </http> + </server> + + <!-- Back end definitions --> + <backend> + <!-- Backend lives on server1:80 and is very big (weight 2). + XR will forward up to 300 connections to it. The back end + checking is left to the default, which is: connect to the + IP and port of the back end. Requests for host + www.mysite.org will be serviced here. --> + <address>server1:80</address> + <weight>2</weight> + <maxconnections>300</maxconnections> + <hostmatch>www.mysite.org</hostmatch> + </backend> + <backend> + <!-- Backend lives on server2:80, has the default weight 1. + XR will forward up to 100 connections to it. The back end + checking is done by connecting to an alternative port 81. + This back end will be eligible for requests for the site + www.myothersite.org. --> + <address>server2:80</address> + <maxconnections>100</maxconnections> + <backendcheck>connect::81</backendcheck> + <hostmatch>www.myothersite.org</hostmatch> + </backend> + <backend> + <!-- Backend lives on server3:80, has the standard weight and no + limitations for the max nr. of connections. Back end + checking is done by retrieving /healthcheck.cgi from the + server. The back end is eligible for www.myothersite.org. --> + <address>server3:80</address> + <backendcheck>get:server3:80/healthcheck.cgi</backendcheck> + <hostmatch>www.myothersite.org</hostmatch> + </backend> + </service> + </configuration> diff --git a/test/test.cgi b/test/test.cgi @@ -0,0 +1,41 @@ +#!/usr/bin/perl + +# Simple script for benchmarking purposes. Invoke as: +# http://whereever/test.cgi?bytes=XYZZY&usec=PLUGH +# Will spam XYZZY bytes as payload, and delay for PLUGH microsecs. +# The payload files are created in $tmpdir if they don't yet exist +# so that CPU looping is avoided. + +use strict; +use Time::HiRes qw(usleep); +use CGI qw(:standard); + +my $tmpdir = '/tmp'; + +# CGI Header +print ("Content-Type: text/plain\r\n\r\n"); + +# Delay for 'usec' microsecs. +my $usec = param('usec') or 0; +usleep($usec); + +# Check that we have a file for the payload. If not, create it. +my $bytes = param('bytes'); +my $file = "$tmpdir/test.cgi.$bytes"; +if (! -f $file) { + open (my $of, ">$file") or die ("Cannot write $file: $!\n"); + for (my $i = 0; $i < $bytes; $i++) { + print $of ('X'); + } + close ($of); +} +# Send the file to the browser. +my $buf; +open (my $if, $file) or die ("Cannot read $file: $!\n"); +while (sysread($if, $buf, 2048)) { + print ($buf); +} + +# All done. Return control to the web server. + + diff --git a/xr/Makefile b/xr/Makefile @@ -35,6 +35,7 @@ subdirs: $(BUILDDIR)/usage.h $(BUILDDIR)/status.xslt.h echo "Making: $$f"; \ BASE=$(BASE) CC=$(CONF_CC) BUILDDIR=$(BUILDDIR) VER='$(VER)' \ AUTHOR='$(AUTHOR)' MAINTAINER='$(MAINTAINER)' \ + DISTSITE='$(DISTSITE)' \ CONF_CC='$(CONF_CC)' CONF_LIB='$(CONF_LIB)' \ CONF_GETOPT=$(CONF_GETOPT) CONF_GETOPT_LONG=$(CONF_GETOPT_LONG) \ CONF_INET_ATON=$(CONF_INET_ATON) CONF_OPTFLAGS='$(CONF_OPTFLAGS)' \ diff --git a/xr/ThreadsAndMutexes/thread/start.cc b/xr/ThreadsAndMutexes/thread/start.cc @@ -7,7 +7,9 @@ static void *_run (void *data) { try { t->execute(); } catch (Error const &e) { + Mutex::lock(&cerr); cerr << e.what() << "\n"; + Mutex::unlock(&cerr); } // Cleanups diff --git a/xr/backend/backend b/xr/backend/backend @@ -7,6 +7,9 @@ #include "error/error" #include "ThreadsAndMutexes/mutex/mutex" #include "profiler/profiler" +#include "backendcheck/backendcheck" +#include "httpbuffer/httpbuffer" +#include "dnsentry/dnsentry" using namespace std; @@ -16,31 +19,43 @@ public: Backend (BackendDef const &b); virtual ~Backend(); bool connect(); + int sock() const { return (clsocket); } + void check(); string description() const; + bool available() const; string availablestr() const; - void live (bool state); + + bool live() const { return (islive); }; + void live (bool state); string livestr() const; + void up (bool state); - string upstr() const; - - bool live() const { return (islive); }; bool up() const { return (isup); } - int sock() const { return (clsocket); } + string upstr() const; + string const &server() const { return (bdef.server()); } + void server(string s) { bdef.server(s); } + int port() const { return (bdef.port()); } + void port(int p) { bdef.port(p); } + unsigned maxconn() const { return (bdef.maxconn()); } void maxconn (unsigned m) { bdef.maxconn(m); } + string const &hostmatch() const { return (bdef.hostmatch()); } void hostmatch(string const &s) { bdef.hostmatch(s); } regex_t const &hostregex() const { return (bdef.hostregex()); } + unsigned weight() const { return (bdef.weight()); } void weight (unsigned w) { bdef.weight(w); } unsigned adjustedweight() const { return (bdef.adjustedweight()); } + unsigned connections() const { return (nconn); } double bytesserved() const { return (bytes_served); } unsigned clientsserved() const { return (totconn); } + double loadavg() const { return (loadaverage); } void loadavg(double l) { loadaverage = l; } @@ -52,6 +67,14 @@ public: return (bdef); } + BackendCheck const &backendcheck() { + return bdef.backendcheck(); + } + void backendcheck(BackendCheck const &b) { + bdef.backendcheck(b); + } + + private: BackendDef bdef; bool islive; @@ -60,6 +83,7 @@ private: unsigned nconn, totconn; double bytes_served; double loadaverage; + DNSEntry dnsentry; }; #endif diff --git a/xr/backend/backend1.cc b/xr/backend/backend1.cc @@ -3,5 +3,5 @@ Backend::Backend () : islive(true), isup(true), clsocket(-1), nconn(0), totconn(0), bytes_served(0), - loadaverage(0.1) { + loadaverage(0.1), dnsentry() { } diff --git a/xr/backend/backend2.cc b/xr/backend/backend2.cc @@ -2,5 +2,5 @@ Backend::Backend (BackendDef const &b) : bdef(b), islive(true), clsocket(-1), nconn(0), totconn(0), - bytes_served(0), loadaverage(0.1) { + bytes_served(0), loadaverage(0.1), dnsentry() { } diff --git a/xr/backend/check.cc b/xr/backend/check.cc @@ -1,6 +1,97 @@ #include "backend" void Backend::check() { - connect(); - socketclose (clsocket); + debugmsg(Mstr("About to check back end ") + description() + ". " + + Mstr(backendcheck().description()) + "\n"); + + ostringstream o; + Backend tester; + Httpbuffer httpbuffer; + + switch (backendcheck().checktype()) { + case BackendCheck::c_connect: + if (backendcheck().server() == "" && backendcheck().port() == 0) { + // Most common: TCP connect to the actual back end + connect(); + socketclose (sock()); + } else { + // TCP connects to an alternative server or port. + // We instantiate a dummy backend and let it connect to the "other" + // values. + tester = *this; + if (backendcheck().server() != "") + tester.server(backendcheck().server()); + if (backendcheck().port() != 0) + tester.port(backendcheck().port()); + tester.connect(); + socketclose (tester.sock()); + live(tester.live()); + msg (Mstr("Alternative back end for testing ") + + tester.description() + " is " + livestr() + "\n"); + } + break; + + case BackendCheck::c_get: + // HTTP GET to stated server, port, uri + tester.server(backendcheck().server()); + tester.port(backendcheck().port()); + tester.connect(); + if (! tester.live()) { + warnmsg((Mstr("HTTP GET checker: host ") + + backendcheck().server()) + + (Mstr(", port ") + backendcheck().port()) + + " not responding\n"); + live(false); + } else { + o << "GET " << backendcheck().uri() << " HTTP/1.0\r\n" + "Host: " << backendcheck().server() << "\r\n" + "Connection: close\r\n" + "\r\n"; + httpbuffer.setstring (o.str()); + httpbuffer.netwrite(tester.sock(), config.backend_timeout()); + httpbuffer.reset(); + while (!httpbuffer.headersreceived()) + httpbuffer.netread(tester.sock(), config.backend_timeout()); + msg((Mstr("HTTP GET checker got answer: '") + + httpbuffer.firstline()) + "'\n"); + if (httpbuffer.stringat(9, 3) == "200") + live(true); + else + debugmsg("Back end assumed dead.\n"); + socketclose(tester.sock()); + } + break; + + case BackendCheck::c_external: + // External program to be called, with arguments: + // IP:PORT availability current-connections + o << backendcheck().program() << ' ' << description() << ' ' + << availablestr() << ' ' << connections(); + FILE *f; + int result; + if (! (f = popen(o.str().c_str(), "r")) ) { + live(false); + warnmsg(Mstr("Failed to start external checker '") + o.str() + + "': " + strerror(errno) + "\n"); + } else { + if (fscanf(f, "%d", &result) < 1) { + live(false); + warnmsg(Mstr("External checker '") + o.str() + + Mstr("' did not reply with a number\n")); + } else { + msg((Mstr("External checker '") + o.str()) + + (Mstr("' replied: ") + result) + '\n'); + live(result == 0); + } + if (pclose(f)) { + warnmsg((Mstr("External checker '") + o.str()) + + "' terminated with error\n"); + live(false); + } + } + break; + + default: + throw static_cast<Error>("Internal fry in Backend::check()"); + } } diff --git a/xr/backend/connect.cc b/xr/backend/connect.cc @@ -15,21 +15,10 @@ bool Backend::connect() { // Resolve hostname, prepare binding struct sockaddr_in backendaddr; + backendaddr.sin_family = AF_INET; backendaddr.sin_port = htons(bdef.port()); - struct hostent *hostaddr; - static int locker; - - Mutex::lock(&locker); - if ( (hostaddr = gethostbyname(bdef.server().c_str())) ) - memcpy ((char *) &backendaddr.sin_addr.s_addr, - hostaddr->h_addr_list[0], hostaddr->h_length); - Mutex::unlock(&locker); - if (! hostaddr) { - socketclose (clsocket); - throw static_cast<Error>("Failed to resolve backend host '") + - bdef.server(); - } + backendaddr.sin_addr.s_addr = dnsentry.resolve(bdef.server()); // Client socket goes into nonblocking mode, so we can connect // and enforce a timeout later. diff --git a/xr/backendcheck/backendcheck b/xr/backendcheck/backendcheck @@ -0,0 +1,45 @@ +#ifndef _BACKENDCHECK_ +#define _BACKENDCHECK_ + +#include "sys/sys" +#include "error/error" + +class BackendCheck { +public: + enum CheckType { + c_connect, + c_get, + c_external, + }; + + BackendCheck(); + + CheckType checktype() const { return check_type; } + void checktype(CheckType t) { check_type = t; } + + string server() const { return srv; } + void server(string s) { srv = s; } + + int port() const { return prt; } + void port(int p) { prt = p; } + + string uri() const { return geturi; } + void uri(string u) { geturi = u; } + + string program() const { return extprog; } + void program(string const &p) { extprog = p; } + + void parse(string setting); + + string setting() const; + string description() const; + +private: + CheckType check_type; + string srv; + int prt; + string geturi; + string extprog; +}; + +#endif diff --git a/xr/backendcheck/backendcheck1.cc b/xr/backendcheck/backendcheck1.cc @@ -0,0 +1,6 @@ +#include "backendcheck" + +BackendCheck::BackendCheck() : check_type(c_connect), srv(""), prt(0), + geturi(""), extprog("") +{ +} diff --git a/xr/backendcheck/description.cc b/xr/backendcheck/description.cc @@ -0,0 +1,39 @@ +#include "backendcheck" + +string BackendCheck::description() const { + ostringstream o; + + o << "Back end check type: "; + switch (check_type) { + case c_connect: + o << "TCP connect to "; + if (srv == "") + o << "backend IP, "; + else + o << "alternative IP '" << srv << "', "; + if (prt == 0) + o << "backend port"; + else + o << "alternative port '" << prt << "'"; + break; + case c_get: + o << "HTTP GET to "; + if (srv == "") + o << "backend IP, "; + else + o << "alternative IP '" << srv << "', "; + if (prt == 0) + o << "backend port"; + else + o << "alternative port '" << prt << "'"; + break; + case c_external: + o << "External program " << extprog; + break; + default: + throw static_cast<Error>("Internal jam in BackendCheck::description"); + } + + return (o.str()); +} + diff --git a/xr/backendcheck/parse.cc b/xr/backendcheck/parse.cc @@ -0,0 +1,66 @@ +#include "backendcheck" + +static int parse_port(string const &s) { + int ret; + if (sscanf(s.c_str(), "%d", &ret) < 1) + ret = 0; + return ret; +} + +static string parse_uri(string const &s) { + size_t slash = s.find_first_of('/'); + if (slash == string::npos) + return ("/"); + return s.substr(slash); +} + +void BackendCheck::parse(string setting) { + // Resets to default + if (!setting.size()) { + check_type = c_connect; + srv = ""; + prt = 0; + geturi = ""; + extprog = ""; + return; + } + + vector<string> parts = str2parts(setting, ':'); + + // connect:IP:PORT + if (parts.size() == 3 && parts[0] == "connect") { + check_type = c_connect; + srv = parts[1]; + prt = parse_port(parts[2]); + geturi = ""; + extprog = ""; + return; + } + + // get:IP:PORT + // get:IP:PORT/URI + if (parts.size() == 3 && parts[0] == "get") { + check_type = c_get; + srv = parts[1]; + prt = parse_port(parts[2]); + geturi = parse_uri(parts[2]); + extprog = ""; + return; + } + + // external:PROGRAM + if (parts.size() == 2 && parts[0] == "external") { + check_type = c_external; + srv = ""; + prt = 0; + geturi = ""; + extprog = parts[1]; + return; + } + + // No luck today + throw static_cast<Error> + ("Back end check specifiers must be either an empty string, " + "or 'connect:IP:PORT' or 'get:IP:PORT' or 'get'IP:PORT/URI' " + "or 'external:PROGRAM'"); +} diff --git a/xr/backendcheck/setting.cc b/xr/backendcheck/setting.cc @@ -0,0 +1,22 @@ +#include "backendcheck" + +string BackendCheck::setting() const { + ostringstream o; + + if (check_type == c_external) + o << "external:" << extprog; + else { + if (check_type == c_connect) + o << "connect:"; + else + o << "get:"; + if (srv != "") + o << srv; + o << ':'; + if (prt) + o << prt; + if (check_type == c_get) + o << geturi; + } + return o.str(); +} diff --git a/xr/backenddef/backenddef b/xr/backenddef/backenddef @@ -1,33 +1,38 @@ #ifndef _BACKENDDEF_ #define _BACKENDDEF_ -#include "../sys/sys" -#include "../error/error" +#include "sys/sys" +#include "error/error" #include "profiler/profiler" +#include "backendcheck/backendcheck" using namespace std; class BackendDef { public: -BackendDef(): srv(""), prt(-1), max(0), host_match(""), wt(1) {} - BackendDef (string s, string p, string m = "", string w = "1"); + BackendDef(): srv(""), prt(-1), max(0), + host_match(""), wt(1), backend_check() {} + BackendDef(string s, string p, string m = "", string w = "1"); - void server(string s) { srv = s; } - string const &server() const { return (srv); } + void server(string s) { srv = s; } + string const &server() const { return (srv); } - void port (int p) { prt = p; } - int port() const { return (prt); } + void port (int p) { prt = p; } + int port() const { return (prt); } - unsigned maxconn() const { return (max); } - void maxconn (unsigned m) { max = m; } + unsigned maxconn() const { return (max); } + void maxconn (unsigned m) { max = m; } - unsigned weight() const { return wt; } + unsigned weight() const { return wt; } void weight (unsigned w); - unsigned adjustedweight() const { return min_wt + max_wt - wt; } + unsigned adjustedweight() const { return min_wt + max_wt - wt; } void hostmatch(string const &s); - string const &hostmatch() const { return (host_match); } - regex_t const &hostregex() const { return (host_regex); } + string const &hostmatch() const { return (host_match); } + regex_t const &hostregex() const { return (host_regex); } + + BackendCheck const &backendcheck() { return backend_check; } + void backendcheck(BackendCheck const &b) { backend_check = b; } private: string srv; @@ -38,6 +43,7 @@ private: unsigned wt; static unsigned min_wt, max_wt; static bool minmax_wt_set; + BackendCheck backend_check; }; #endif diff --git a/xr/balancer/addbackend.cc b/xr/balancer/addbackend.cc @@ -1,7 +0,0 @@ -#include "balancer" - -void Balancer::addbackend (BackendDef const &b) { - Backend newb (b); - backends.push_back (newb); - backends[backends.size() - 1].check(); -} diff --git a/xr/balancer/addbackend1.cc b/xr/balancer/addbackend1.cc @@ -0,0 +1,6 @@ +#include "balancer" + +void Balancer::addbackend (BackendDef const &b) { + Backend newb (b); + addbackend(newb); +} diff --git a/xr/balancer/addbackend2.cc b/xr/balancer/addbackend2.cc @@ -0,0 +1,14 @@ +#include "balancer" + +void Balancer::addbackend (Backend const &b, + bool is_up, bool is_live, bool do_check) { + Mutex::lock(&backends); + backends.push_back (b); + Mutex::unlock(&backends); + + backends[backends.size() - 1].up(is_up); + backends[backends.size() - 1].live(is_live); + + if (do_check) + backends[backends.size() - 1].check(); +} diff --git a/xr/balancer/balancer b/xr/balancer/balancer @@ -20,7 +20,11 @@ class Balancer { public: Balancer (); void init(); - void addbackend (BackendDef const &b); + void addbackend(BackendDef const &b); + void addbackend(Backend const &b, + bool is_up = true, bool is_live = true, + bool do_check = true); + void deletebackend(unsigned i); void serve(); unsigned nbackends() { return (backends.size()); } diff --git a/xr/balancer/deletebackend.cc b/xr/balancer/deletebackend.cc @@ -0,0 +1,15 @@ +#include "balancer" + +void Balancer::deletebackend(unsigned i) { + if (backend(i).up()) + throw static_cast<Error>("Only 'down' back ends can be deleted."); + if (backend(i).connections()) + throw static_cast<Error>("Back end cannot be deleted, there are still ") + + backend(i).connections() + " connections"; + + + Mutex::lock(&backends); + backends.erase(backends.begin() + i, + backends.begin() + i + 1); + Mutex::unlock(&backends); +} diff --git a/xr/balancer/init.cc b/xr/balancer/init.cc @@ -11,6 +11,8 @@ void Balancer::init() { // Start the web interface if requested. if (config.usewebinterface()) { Webinterface *w = new Webinterface(); + if (! w) + throw static_cast<Error>("Memory fault in Balancer::init"); w->start(); } diff --git a/xr/balancer/serve.cc b/xr/balancer/serve.cc @@ -9,11 +9,15 @@ void Balancer::serve() { if (config.wakeupsec() && !config.foregroundmode() && config.sport()) { msg ("Starting wakeup thread.\n"); Wakeupthread *wt = new Wakeupthread(); + if (!wt) + throw static_cast<Error>("Memory fault in Balancer::serve"); wt->start(); } if (config.checkupsec() && !config.foregroundmode() && config.sport()) { msg ("Starting checkup thread.\n"); Checkupthread *ct = new Checkupthread(); + if (!ct) + throw static_cast<Error>("Memory fault in Balancer::serve"); ct->start(); } @@ -66,7 +70,7 @@ void Balancer::serve() { // Got activity! request_nr++; - + if (server_fd) { // If tcp-serving: server_fd > 0; serve and loop again int size; @@ -76,7 +80,7 @@ void Balancer::serve() { if ( (clsock = accept (server_fd, (struct sockaddr *) &clname, (socklen_t*) &size)) < 0 ) throw static_cast<Error>("Failed to accept network connection"); - + string clientip = inet_ntoa(clname.sin_addr); // If there is an allow list, the client must match it. @@ -126,7 +130,7 @@ void Balancer::serve() { (Mstr(" connections , max ") + backend(i).maxconn()) + (Mstr(", status ") + backend(i).availablestr()) + "\n"); } - + // We got action! Check if the total connections to the // balancer doesn't exceed the max. if (config.maxconn() && connections() >= config.maxconn()) { @@ -149,6 +153,8 @@ void Balancer::serve() { "can't choose dispatcher"); break; } + if (!d) + throw static_cast<Error>("Memory fault in Balancer::serve"); // Allocation boundary printout if (config.debug()) { @@ -157,14 +163,14 @@ void Balancer::serve() { _debugmsg (Mstr("Allocation boundary at dispatcher start: ") + mem + "\n"); } - + d->start(); } else { // If fd-serving, serve and close. Don't thread it up. - TcpDispatcher *d; + TcpDispatcher *d; struct in_addr dummy; inet_aton ("0.0.0.0", &dummy); - + switch (config.stype()) { case Servertype::t_tcp: d = new TcpDispatcher (server_fd, dummy); @@ -177,10 +183,12 @@ void Balancer::serve() { "can't choose dispatcher"); break; } + if (!d) + throw static_cast<Error>("Memory fault in Balancer::serve"); d->execute(); break; } - + // If we exceed the max # of requests, stop.. if (config.quitafter()) { msg ((Mstr("Request ") + requestnr()) + @@ -192,8 +200,11 @@ void Balancer::serve() { } } + // We're stopping now. If a PID stamp was created, remove it. + if (config.pidfile() != "") + unlink (config.pidfile().c_str()); - // We're stopping XR now. Wait for running threads to die off. + // Wait for running threads to die off. socketclose (server_fd); shutdown (server_fd, SHUT_RDWR); unsigned prev_conn = 0x19081962; @@ -208,8 +219,4 @@ void Balancer::serve() { sleep (1); } msg ("XR is idle, stopping.\n"); - - // If a PID stamp was created, remove it now. - if (config.pidfile() != "") - unlink (config.pidfile().c_str()); } diff --git a/xr/config/changeallow.cc b/xr/config/changeallow.cc @@ -0,0 +1,12 @@ +#include "config" + +void Config::changeallow (string &a, unsigned index) { + if (index >= allowlist.size()) + throw static_cast<Error>("No such allow-from specifier"); + + struct in_addr in; + if (!inet_aton (a.c_str(), &in)) + throw static_cast<Error>("Bad allow-from specfier '") + a + "'"; + allowlist[index] = (in); +} + diff --git a/xr/config/changedeny.cc b/xr/config/changedeny.cc @@ -0,0 +1,12 @@ +#include "config" + +void Config::changedeny (string &a, unsigned index) { + if (index >= denylist.size()) + throw static_cast<Error>("No such deny-from specifier"); + + struct in_addr in; + if (!inet_aton (a.c_str(), &in)) + throw static_cast<Error>("Bad deny-from specfier '") + a + "'"; + denylist[index] = (in); +} + diff --git a/xr/config/config b/xr/config/config @@ -7,6 +7,7 @@ #include "dispatchmode/dispatchmode" #include "error/error" #include "ThreadsAndMutexes/mutex/mutex" +#include "backendcheck/backendcheck" using namespace std; @@ -95,11 +96,21 @@ public: unsigned connrate_time() const { return connrate_timeinterval; } void connrate_time(unsigned n) { connrate_timeinterval = n; } + unsigned dnscachetimeout() const { return dns_cache_timeout; } + void dnscachetimeout(unsigned t) { dns_cache_timeout = t; } + unsigned nallow() const { return (allowlist.size()); } unsigned ndeny() const { return (denylist.size()); } + void addallow (string a); + void adddeny (string d); + void changeallow(string &a, unsigned index); + void changedeny(string &a, unsigned index); + void deleteallow(unsigned index); + void deletedeny(unsigned index); + int ipstoretimeout() const { return (ipstore_timeout); } void ipstoretimeout(int t); - struct in_addr allow(unsigned n) const { + struct in_addr allow(unsigned n) const { return (allowlist[n]); } struct in_addr deny(unsigned n) const { @@ -116,14 +127,27 @@ public: return (dmode.modestr()); } + string softmaxconnexcess() const { + return soft_maxconn_excess_prog; + } + void softmaxconnexcess(string const &s) { + soft_maxconn_excess_prog = s; + } + string hardmaxconnexcess() const { + return hard_maxconn_excess_prog; + } + void hardmaxconnexcess(string const &s) { + hard_maxconn_excess_prog = s; + } + + private: - void setbackend (string s, string hostmatch); + void setbackend (string const &s, string const &hostmatch, + BackendCheck const &bc); void setwebinterface (string s); void setserver (string s); void setdispatchmode (string s); int setinteger (string s) const; - void addallow (string a); - void adddeny (string d); static bool verbose_flag; static int lport; @@ -159,6 +183,9 @@ private: static unsigned defer_time; static unsigned connrate_timeinterval; static unsigned quit_after; + static string soft_maxconn_excess_prog; + static string hard_maxconn_excess_prog; + static unsigned dns_cache_timeout; }; extern Config config; diff --git a/xr/config/config1.cc b/xr/config/config1.cc @@ -34,6 +34,9 @@ unsigned Config::hard_maxconnrate = 0; unsigned Config::defer_time = 500000; unsigned Config::connrate_timeinterval = 1; unsigned Config::quit_after = 0; +string Config::soft_maxconn_excess_prog = ""; +string Config::hard_maxconn_excess_prog = ""; +unsigned Config::dns_cache_timeout = 3600; Config::Config () { } diff --git a/xr/config/deleteallow.cc b/xr/config/deleteallow.cc @@ -0,0 +1,10 @@ +#include "config" + +void Config::deleteallow(unsigned index) { + if (index >= allowlist.size()) + throw static_cast<Error> + ("Index out of range, cannot delete allow-from"); + allowlist.erase(allowlist.begin() + index, + allowlist.begin() + index + 1); +} + diff --git a/xr/config/deletedeny.cc b/xr/config/deletedeny.cc @@ -0,0 +1,10 @@ +#include "config" + +void Config::deletedeny(unsigned index) { + if (index >= denylist.size()) + throw static_cast<Error> + ("Index out of range, cannot delete deny-from"); + denylist.erase(denylist.begin() + index, + denylist.begin() + index + 1); +} + diff --git a/xr/config/parsecmdline.cc b/xr/config/parsecmdline.cc @@ -16,7 +16,8 @@ void Config::parsecmdline (int ac, char **av) { throw static_cast<Error>("Bad command line '") + cmdline + "'\n" + USAGE; -# define OPTSTRING "?a:A:B:b:c:CDd:fhH:l:m:M:nPp:Q:r:R:Ss:t:T:u:U:vVW:w:xX" +# define OPTSTRING "?a:A:B:b:c:CDd:E:e:fF:g:hH:l:" \ + "m:M:nPp:Q:r:R:Ss:t:T:u:U:vVW:w:xX" # ifdef HAVE_GETOPT_LONG static struct option longopts[] = { { "allow-from", required_argument, 0, 'a' }, @@ -27,7 +28,11 @@ void Config::parsecmdline (int ac, char **av) { { "close-sockets-fast", no_argument, 0, 'C' }, { "debug", no_argument, 0, 'D' }, { "dispatch-mode", required_argument, 0, 'd' }, + { "hard-maxconn-excess", required_argument, 0, 'E' }, + { "soft-maxconn-excess", required_argument, 0, 'e' }, + { "dns-cache-timeout", required_argument, 0, 'F' }, { "foreground", no_argument, 0, 'f' }, + { "backend-check", required_argument, 0, 'g' }, { "help", no_argument, 0, 'h' }, { "add-server-header", required_argument, 0, 'H' }, { "log-traffic-dir", required_argument, 0, 'l' }, @@ -59,6 +64,7 @@ void Config::parsecmdline (int ac, char **av) { bool backend_set = false; bool tryout = false, wakeup_used = false; string current_hostmatch = ""; + BackendCheck current_backendcheck; # ifdef HAVE_GETOPT_LONG while ( (opt = getopt_long (ac, av, OPTSTRING, longopts, 0)) > 0) @@ -74,7 +80,7 @@ void Config::parsecmdline (int ac, char **av) { adddeny (optarg); break; case 'b': - setbackend (optarg, current_hostmatch); + setbackend (optarg, current_hostmatch, current_backendcheck); backend_set = true; break; case 'B': @@ -93,9 +99,21 @@ void Config::parsecmdline (int ac, char **av) { case 'd': setdispatchmode (optarg); break; + case 'E': + hard_maxconn_excess_prog = optarg; + break; + case 'e': + soft_maxconn_excess_prog = optarg; + break; + case 'F': + dns_cache_timeout = (unsigned)setinteger(optarg); + break; case 'f': foreground_mode = true; break; + case 'g': + current_backendcheck.parse(optarg); + break; case 'h': case '?': throw static_cast<Error>(USAGE); @@ -155,6 +173,7 @@ void Config::parsecmdline (int ac, char **av) { cout << "XR version : " << VER << "\n" << "Written by : " << AUTHOR << "\n" << "Maintained by : " << MAINTAINER << "\n" + << "Primary site : " << DISTSITE << "\n" << "Compiled with : " << CONF_CC << "\n" << "Optimization : " << CONF_OPTFLAGS << "\n" << "System : " << SYS << "\n" diff --git a/xr/config/setbackend.cc b/xr/config/setbackend.cc @@ -1,11 +1,13 @@ #include "config" -void Config::setbackend (string str, string host) { +void Config::setbackend (string const &str, string const &host, + BackendCheck const &backend_check) { vector<string> parts = str2parts (str, ':'); if (parts.size() < 2 || parts.size() > 4) - throw static_cast<Error>("Bad back end specifier in '-b") + str + - "', expected: SERVER:PORT or SERVER:PORT:MAXCONNECTIONS or " - "SERVER:PORT:MAXCONNECTIONS:WEIGHT"; + throw static_cast<Error> + ("Bad back end specifier in '-b") + str + + "', expected: SERVER:PORT or SERVER:PORT:MAXCONNECTIONS or " + "SERVER:PORT:MAXCONNECTIONS:WEIGHT"; BackendDef *bdp = 0; if (parts.size() == 2) @@ -14,7 +16,10 @@ void Config::setbackend (string str, string host) { bdp = new BackendDef(parts[0], parts[1], parts[2]); else if (parts.size() == 4) bdp = new BackendDef(parts[0], parts[1], parts[2], parts[3]); + if (!bdp) + throw static_cast<Error>("Memory fault in Config::setbackend"); bdp->hostmatch(host); + bdp->backendcheck(backend_check); blist.push_back (*bdp); delete bdp; } diff --git a/xr/dnsentry/dnsentry b/xr/dnsentry/dnsentry @@ -0,0 +1,18 @@ +#ifndef _DNSENTRY_ +#define _DNSENTRY_ + +#include "config/config" +#include "error/error" +#include "ThreadsAndMutexes/mutex/mutex" + +class DNSEntry { +public: + DNSEntry(): result(0), timestamp(0) {} + in_addr_t &resolve(string const &str); + +private: + in_addr_t result; + time_t timestamp; +}; + +#endif diff --git a/xr/dnsentry/resolve.cc b/xr/dnsentry/resolve.cc @@ -0,0 +1,20 @@ +#include "dnsentry" + +in_addr_t &DNSEntry::resolve (string const &h) { + // If the entry is there and if it's up to date, run with it + if (timestamp && + time(0) <= timestamp + (time_t)config.dnscachetimeout()) + return result; + + // Resolve now. + struct hostent *hostaddr; + Mutex::lock((void*)gethostbyname); + if ( (hostaddr = gethostbyname(h.c_str())) ) + memcpy (&result, hostaddr->h_addr_list[0], hostaddr->h_length); + Mutex::unlock((void*)gethostbyname); + + if (!hostaddr) + throw static_cast<Error>("Failed to resolve host '") + h + "'"; + + return result; +} diff --git a/xr/etc/Makefile.class b/xr/etc/Makefile.class @@ -9,7 +9,8 @@ $(BASE)/xr/$(BUILDDIR)/$(DIR)_%.o: %.cc @echo "Compiling: " `pwd` $< @$(CONF_CC) $(PROF) $(PROFILER) $(CONF_OPTFLAGS) \ -DVER='"$(VER)"' -DAUTHOR='"$(AUTHOR)"' \ - -DMAINTAINER='"$(MAINTAINER)"' -DSYS='"$(SYS)"' -D$(SYS) \ + -DMAINTAINER='"$(MAINTAINER)"' -DDISTSITE='"$(DISTSITE)"' \ + -DSYS='"$(SYS)"' -D$(SYS) \ -DCONF_CC='"$(CONF_CC)"' -DCONF_LIB='"$(CONF_LIB)"' \ -DCONF_OPTFLAGS='"$(CONF_OPTFLAGS)"' $(CONF_STRNSTR) \ $(CONF_GETOPT) $(CONF_GETOPT_LONG) $(CONF_INET_ATON) \ diff --git a/xr/etc/status.xslt b/xr/etc/status.xslt @@ -15,7 +15,7 @@ } body { font-family: Verdana,Helvetica; - font-size: 10pt; + font-size: 8pt; } td { font-family: Verdana,Helvetica; @@ -58,8 +58,33 @@ <xsl:template match="/status"> <table> - <xsl:apply-templates/> + <xsl:apply-templates select="/status/server"/> + <xsl:apply-templates select="/status/backend"/> + + <tr> <td colspan="4"><hr/></td></tr> + <tr> + <td class="backend" colspan="2"> + <b>Add back end ip:port</b> + </td> + <td class="backend" colspan="2" align="right"> + <input type="text" size="30" name="addbackend" id="addbackend" + onchange="goto('/server/addbackend/', 'addbackend');"/> + </td> + </tr> + <tr> <td colspan="4"><hr/></td></tr> + </table> + <xsl:apply-templates select="/status/id"/> +</xsl:template> + +<xsl:template match="/status/id"> + <i> + Powered by Crossroads V<xsl:value-of select="version"/>. + Visit + <a href="{distsite}" + target="_blank"><xsl:value-of select="distsite"/></a> + for more info. + </i> </xsl:template> <xsl:template match="/status/server"> @@ -67,11 +92,29 @@ <td class="server" colspan="3"> <b>Server <xsl:value-of select="address"/> </b> </td> - <td> + <td class="server"> <input type="button" onclick="goto('/', '');" value="Refresh"/> </td> </tr> <tr> + <td>Status</td> + <td colspan="3"> + <xsl:choose> + <xsl:when test="terminating = 0"> + Accepting connections, + <xsl:value-of select="connections"/> concurrent client(s), + </xsl:when> + <xsl:otherwise> + <font color="red"> + Terminating, still serving + <xsl:value-of select="connections"/> connections, + </font> + </xsl:otherwise> + </xsl:choose> + <xsl:value-of select="backends"/> defined back ends + </td> + </tr> + <tr> <td>Type</td> <td colspan="3"> <xsl:value-of select="type"/> </td> </tr> @@ -155,7 +198,47 @@ onchange="goto('/server/backendtimeout/', 'backendtimeout');"/> </td> </tr> - <tr> + <tr> + <td></td> + <td>DNS</td> + <td> + <xsl:choose> + <xsl:when test="dnscachetimeout = 0"> + unused + </xsl:when> + <xsl:otherwise> + sec + </xsl:otherwise> + </xsl:choose> + </td> + <td> + <input type="text" size="8" name="dnscachetimeout" + id="dnscachetimeout" value="{dnscachetimeout}" + onchange="goto('/server/dnscachetimeout/', 'dnscachetimeout');"/> + </td> + </tr> + + <tr> + <td>Fast sockets closing</td> + <td colspan="2">eliminates TIME_WAIT state</td> + <td> + <xsl:choose> + <xsl:when test="closesocketsfast = 0"> + <select onchange="goto('/server/closesocketsfast/on', '');"> + <option value="yes">yes</option> + <option value="no" selected="1">no</option> + </select> + </xsl:when> + <xsl:otherwise> + <select onchange="goto('/server/closesocketsfast/off', '');"> + <option value="yes" selected="1">yes</option> + <option value="no">no</option> + </select> + </xsl:otherwise> + </xsl:choose> + </td> + </tr> + <tr> <td>Debugging</td> <td colspan="2">Verbose logging</td> <td> @@ -214,10 +297,11 @@ </td> </tr> <tr> - <td> Max. connections </td> - <td colspan="2"> + <td>DOS Protection</td> + <td>Max. connections </td> + <td> <xsl:choose> - <xsl:when test="maxconnections = 0"> + <xsl:when test="/dosprotection/maxconnections = 0"> unlimited </xsl:when> <xsl:otherwise> @@ -227,10 +311,111 @@ </td> <td> <input type="text" size="8" name="setservermaxcon" class="input" - id="setservermaxcon" value="{maxconnections}" + id="setservermaxcon" value="{dosprotection/maxconnections}" onchange="goto('/server/maxconnections/', 'setservermaxcon');"/> </td> </tr> + + <tr> + <td></td> + <td>Time interval</td> + <td>sec</td> + <td> + <input type="text" size="8" name="timeinterval" class="input" + id="timeinterval" value="{dosprotection/timeinterval}" + onchange="goto('/server/timeinterval/', 'timeinterval');"/> + </td> + </tr> + <tr> + <td></td> + <td>Hard max connection rate</td> + <td> + <xsl:choose> + <xsl:when test="/dosprotection/hardmaxconnrate = 0"> + unlimited + </xsl:when> + <xsl:otherwise> + sessions per time interval (0 for unlimited) + </xsl:otherwise> + </xsl:choose> + </td> + <td> + <input type="text" size="8" name="hardmaxconnrate" class="input" + id="hardmaxconnrate" value="{dosprotection/hardmaxconnrate}" + onchange="goto('/server/hardmaxconnrate/', 'hardmaxconnrate');"/> + </td> + </tr> + <tr> + <td></td> + <td>Soft max connection rate</td> + <td> + <xsl:choose> + <xsl:when test="/dosprotection/softmaxconnrate = 0"> + unlimited + </xsl:when> + <xsl:otherwise> + sessions per time interval (0 for unlimited) + </xsl:otherwise> + </xsl:choose> + </td> + <td> + <input type="text" size="8" name="softmaxconnrate" class="input" + id="softmaxconnrate" value="{dosprotection/softmaxconnrate}" + onchange="goto('/server/softmaxconnrate/', 'softmaxconnrate');"/> + </td> + </tr> + <tr> + <td></td> + <td>Defer time</td> + <td>in microsec, 1.000.000 = 1 sec</td> + <td> + <input type="text" size="8" name="defertime" class="input" + id="defertime" value="{dosprotection/defertime}" + onchange="goto('/server/defertime/', 'defertime');"/> + </td> + </tr> + + <tr> + <td></td> + <td>Hard excess signal program</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="hardexcess" class="input" + id="hardexcess" value="{dosprotection/hardmaxconnexcess}" + onchange="goto('/server/hardmaxconnexcess/', 'hardexcess');"/> + </td> + </tr> + + <tr> + <td></td> + <td>Soft excess signal program</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="softexcess" class="input" + id="softexcess" value="{dosprotection/softmaxconnexcess}" + onchange="goto('/server/softmaxconnexcess/', 'softexcess');"/> + </td> + </tr> + + <tr> + <td>Access Control Lists</td> + <td>New allow-from</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="addallowfrom" class="input" + id="addallowfrom" + onchange="goto('/server/addallowfrom/', 'addallowfrom');"/> + </td> + </tr> + <xsl:apply-templates select="/status/server/acl/allow"/> + <tr> + <td></td> + <td>New deny-from</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="adddenyfrom" class="input" + id="adddenyfrom" + onchange="goto('/server/adddenyfrom/', 'adddenyfrom');"/> + </td> + </tr> + <xsl:apply-templates select="/status/server/acl/deny"/> + <xsl:if test="/status/server/type = 'http'"> <xsl:apply-templates select="/status/server/http"/> </xsl:if> @@ -239,9 +424,13 @@ <xsl:template match="/status/backend"> <tr> <td colspan="4"><hr/></td></tr> <tr> - <td class="backend" colspan="4"> + <td class="backend" colspan="3"> <b> Back end <xsl:value-of select="address"/> </b> </td> + <td class="backend"> + <input type="button" value="Delete" + onclick="goto('/server/deletebackend/{nr}', '');"/> + </td> </tr> <tr> <td><b>State</b></td> @@ -294,17 +483,26 @@ </tr> <tr> <td></td> - <td colspan="2">load average</td> + <td colspan="2">Load average</td> <td> <input type="text" size="8" name="setloadaverage{nr}" id="setloadaverage{nr}" value="{loadavg}" onchange="goto('/backend/{nr}/loadavg/', 'setloadaverage{nr}');"/> </td> </tr> + <tr> + <td></td> + <td colspan="2">Backend check (. to reset)</td> + <td> + <input type="text" size="8" name="backendcheck{nr}" + id="backendcheck{nr}" value="{backendcheck}" + onchange="goto('/backend/{nr}/backendcheck/', 'backendcheck{nr}');"/> + </td> + </tr> <xsl:if test="/status/server/type = 'http'"> <tr> <td></td> - <td>host match</td> + <td>Host match</td> <td> <xsl:choose> <xsl:when test="hostmatch = '.'"> @@ -347,7 +545,7 @@ <xsl:template match="/status/server/http"> <tr> <td>HTTP Goodies</td> - <td colspan="2">add X-Forwarded-For</td> + <td colspan="2">Add X-Forwarded-For</td> <td> <xsl:choose> <xsl:when test="addxforwardedfor = 0"> @@ -367,7 +565,7 @@ </tr> <tr> <td></td> - <td colspan="2">sticky HTTP</td> + <td colspan="2">Sticky HTTP</td> <td> <xsl:choose> <xsl:when test="stickyhttp = 0"> @@ -388,13 +586,41 @@ <xsl:apply-templates select="/status/server/http/serverheaders"/> </xsl:template> +<xsl:template match="/status/server/acl/allow"> + <xsl:for-each select="allowfrom"> + <tr> + <td></td> + <td>Allow from</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="allowfrom{nr}" + id="allowfrom{nr}" value="{mask}" + onchange="goto('/server/allowfrom/{nr}/', 'allowfrom{nr}');"/> + </td> + </tr> + </xsl:for-each> +</xsl:template> + +<xsl:template match="/status/server/acl/deny"> + <xsl:for-each select="denyfrom"> + <tr> + <td></td> + <td>Deny from</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="denyfrom{nr}" + id="denyfrom{nr}" value="{mask}" + onchange="goto('/server/denyfrom/{nr}/', 'denyfrom{nr}');"/> + </td> + </tr> + </xsl:for-each> +</xsl:template> + <xsl:template match="/status/server/http/serverheaders"> <xsl:for-each select="serverheader"> <tr> <td></td> - <td colspan="2">server header (. to delete)</td> - <td> - <input type="text" size="8" name="serverheader{nr}" + <td>Server header</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="serverheader{nr}" id="serverheader{nr}" value="{header}" onchange="goto('/server/changeheader/{nr}/', 'serverheader{nr}');"/> </td> @@ -402,9 +628,9 @@ </xsl:for-each> <tr> <td></td> - <td colspan="2">new server header</td> - <td> - <input type="text" size="8" name="newserverheader" + <td>New server header</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="newserverheader" id="newserverheader" onchange="goto('/server/newheader/', 'newserverheader');"/> </td> diff --git a/xr/etc/usage.txt b/xr/etc/usage.txt @@ -24,34 +24,58 @@ may not exist on your platform): Sets debugging on, more verbosity on top of -v -d METHOD, --dispatch-mode METHOD Defines how to dispatch over back ends, the method may be: - f, first-available - first live back end gets all traffic - e:EXT, external:EXT - external program EXT is queried - h, strict-hashed-ip - client IP is hashed to determine a back - end, client is denied when back end is down - H, lax-hashed-ip - client IP is hashed, fallback to least- - connections when target back end is down - l, least-connections - back end with least connections is taken - r, round-robin - back ends take turns - L, weighted-load - randomly picks from back end with favor - given to backends with lower load average. - (NOTE: load average must be updated by the - backend, e.g. using the web interface). + f, first-available - first live back end gets all traffic + e:EXT, external:EXT - external program EXT is queried + h, strict-hashed-ip - client IP is hashed to determine a back end, + the client is denied when back end is down. + H, lax-hashed-ip - client IP is hashed, fallback to least-connections + when target back end is down + l, least-connections - back end with least connections is taken + r, round-robin - back ends take turns + L, weighted-load - randomly picks from back end with favor given + to backends with lower load average. (NOTE: load average must + be updated by the backend, e.g. using the web interface). s:SEC, strict-stored-ip:SEC - if client connected before within SEC - seconds, then the same backend is used. - Client is denied if that backend is down. - Else a new is found by least-connections. - S:SEC, lax-stored-ip:SEC - same as strict-stored-ip, but falls back - to least-connections when a previously - used back end is down + seconds, then the same backend is used. Client is denied if + that backend is down. Else a new is found by least-connections. + S:SEC, lax-stored-ip:SEC - same as strict-stored-ip, but falls back + to least-connections when a previously used back end is down. Default method is l (least-connections). When external mode is selected, program EXT is started with arguments <nbackends> <b0> <b0-availability> <b0-connections> (b0 repeated for all back ends). Here <b0> is the back end definition, eg. "10.1.1.1:80"; <b0-availablility> is "available" or "unavailable", <b0-connections> is the nr. of connections. The program must reply with a back end number (0..max) on stdout. + -E PROGRAM, --hard-maxconn-excess PROGRAM + When a client exceeds the hard maxconnection rate, PROGRAM is + invoked with the client's IP as argument. The program may e.g. + invoke iptables to block the offending IP. + -e PROGRAM, --soft-maxconn-excess PROGRAM + When a client exceeds the soft maxconnection rate, PROGRAM is + invoked with the client's IP as argument. + -F SEC, --dns-cache-timeout SEC + DNS results for back end hostnames are cached for SEC seconds. + The default is 3600 (1 hour). Use 0 to suppress. -f, --foreground Suppresses forking/threading, only for debugging. Also suppresses wakeups (-w), checkups (-c) and the webinterface (-W). + -g, --backend-check METHOD + Defines how back ends are checked. This flag must be specified + PRIOR to defining back ends with -b... The checker will then + apply to all next back ends. Alternatives are: + connect:IP:PORT - successful TCP connects at IP:PORT indicate + that the back end is alive. When IP is not stated, the back + end's IP is assumed. + get:IP:PORT/URI - A HTTP GET is sent to IP:PORT/URI. When an + HTTP status 200 is seen, the back end is assumed alive. When + /URI is not given, then "/" is assumed. + external:PROGRAM - The PROGRAM is called with the arguments + "IP:PORT", availability as "available" or "unavailable", and + the number of connections. The program must exit(0) to + indicate that the back end is alive. + The default behavior is a TCP connect, to the back end's IP, at + the back end's port. Use "-g connect::" to reset previous flags + to the default. -h, -?, --help This text. -H HDR, --add-server-header HDR diff --git a/xr/fdset/readable.cc b/xr/fdset/readable.cc @@ -10,10 +10,21 @@ int Fdset::readable() const { if (set.size() < 1) return (-1); + if (config.debug()) { + ostringstream o; + o << "Candidate readable fd's:"; + for (unsigned i = 0; i < set.size(); i++) + o << ' ' << set[i]; + _debugmsg(o.str() + '\n'); + } + // Prepare select sets. FD_ZERO (&readset); FD_ZERO (&exceptset); + int max = 0; for (unsigned i = 0; i < set.size(); i++) { + if (set[i] > max) + max = set[i]; FD_SET (set[i], &readset); FD_SET (set[i], &exceptset); } @@ -28,7 +39,9 @@ int Fdset::readable() const { // Run the select. Signal interrupts are returned as -1, so that // the caller can handle them gracefully. - if (select (FD_SETSIZE, &readset, 0, &exceptset, tvp) < 0) { + if (select (max + 1, &readset, 0, &exceptset, tvp) < 0) { + debugmsg (Mstr("Select interrupted with errno ") + errno + + " while waiting for readable fd\n"); if (errno != EINTR) throw static_cast<Error> ("Select failure: failed to wait for readable state: ") + diff --git a/xr/fdset/writeable.cc b/xr/fdset/writeable.cc @@ -28,6 +28,8 @@ int Fdset::writeable() const { // Run the select. if (select (FD_SETSIZE, 0, &writeset, &exceptset, tvp) < 0) { + debugmsg (Mstr("Select interrupted with errno ") + errno + + " while waiting for writeable fd\n"); if (errno != EINTR) throw static_cast<Error> ("Select failure: failed to wait for writable state: ") + diff --git a/xr/httpbuffer/httpbuffer b/xr/httpbuffer/httpbuffer @@ -21,12 +21,19 @@ public: string headerval (string var); string &firstline(); + bool setversion(char v); + void setheader (string var, string val); + void setheader (string h); + void addheader (string var, string val); void addheader (string h); + string cookievalue (string var); + RequestMethod requestmethod(); + string requesturi(); private: diff --git a/xr/httpbuffer/setheader1.cc b/xr/httpbuffer/setheader1.cc @@ -0,0 +1,16 @@ +#include "httpbuffer" + +void Httpbuffer::setheader (string h) { + PROFILE("Httpbuffer::setheader(string)"); + + unsigned i; + for (i = 0; i < h.size(); i++) + if (h[i] == ':') { + string var = h.substr(0, i); + i++; + while (isspace(h[i])) + i++; + string val = h.substr(i); + setheader (var, val); + } +} diff --git a/xr/httpdispatcher/dispatch.cc b/xr/httpdispatcher/dispatch.cc @@ -16,7 +16,7 @@ void HttpDispatcher::dispatch() { while (!buf.headersreceived()) if (!buf.netread(clientfd(), config.client_timeout())) throw static_cast<Error>("Didn't receive a valid " - "client request.\n"); + "client request."); if (config.verbose()) msg ("Received client request: '" + buf.firstline() + "'\n"); diff --git a/xr/httpdispatcher/handle.cc b/xr/httpdispatcher/handle.cc @@ -16,7 +16,7 @@ void HttpDispatcher::handle() { if (config.addxforwardedfor()) buf.addheader ("X-Forwarded-For", string(inet_ntoa(clientip()))); for (unsigned n = 0; n < config.nserverheaders(); n++) - buf.addheader (config.serverheader(n)); + buf.setheader (config.serverheader(n)); // Flush client info received so far to the back end. debugmsg("Sending client request to back end\n"); diff --git a/xr/httpdispatcher/senderrorpage.cc b/xr/httpdispatcher/senderrorpage.cc @@ -29,6 +29,8 @@ void HttpDispatcher::senderrorpage() { Netbuffer buf(mess.str()); buf.netwrite(clientfd(), config.client_timeout()); } catch (Error const &e) { + Mutex::lock(&cerr); cerr << e.what() << " (while sending error page)\n"; + Mutex::unlock(&cerr); } } diff --git a/xr/netbuffer/copy.cc b/xr/netbuffer/copy.cc @@ -3,7 +3,8 @@ void Netbuffer::copy (Netbuffer const &other) { buf_sz = other.buf_sz; buf_alloced = other.buf_alloced; - buf_data = new char[buf_sz]; + if (! (buf_data = (char*)malloc(buf_sz)) ) + throw static_cast<Error>("Memory fault in Netbuffer::copy"); memcpy (buf_data, other.buf_data, buf_sz); } diff --git a/xr/netbuffer/netbuffer b/xr/netbuffer/netbuffer @@ -28,6 +28,7 @@ public: unsigned charfind (char ch, unsigned start = 0) const; bool setchar(unsigned offset, char ch); + void setstring(string const &s); string stringat(unsigned index, unsigned len); diff --git a/xr/netbuffer/netbuffer4.cc b/xr/netbuffer/netbuffer4.cc @@ -2,12 +2,5 @@ Netbuffer::Netbuffer (string const &s): buf_data(0), buf_sz(0), buf_alloced(0) { - - check_space(s.size() + 1); - - buf_sz = s.size(); - - memcpy (buf_data, s.c_str(), buf_sz); - buf_data[buf_sz] = 0; - debugmsg((Mstr("Created netbuffer from string, ") + buf_sz) + " bytes\n"); + setstring(s); } diff --git a/xr/netbuffer/netread.cc b/xr/netbuffer/netread.cc @@ -15,7 +15,8 @@ unsigned Netbuffer::netread (int fd, int timeout) { ssize_t nread = read (fd, buf_data + buf_sz, config.buffersize()); if (nread < 0) - throw static_cast<Error>("Read failed on fd ") + fd; + throw static_cast<Error>("Read failed on fd ") + fd + ": " + + strerror(errno); buf_sz += nread; if (config.debug() && nread) { diff --git a/xr/netbuffer/setstring.cc b/xr/netbuffer/setstring.cc @@ -0,0 +1,10 @@ +#include "netbuffer" + +void Netbuffer::setstring(string const &s) { + destroy(); + check_space(s.size() + 1); + buf_sz = s.size(); + memcpy (buf_data, s.c_str(), buf_sz); + buf_data[buf_sz] = 0; + debugmsg((Mstr("Set netbuffer to string, ") + buf_sz) + " bytes\n"); +} diff --git a/xr/netbuffer/stringat.cc b/xr/netbuffer/stringat.cc @@ -0,0 +1,11 @@ +#include "netbuffer" + +string Netbuffer::stringat(unsigned index, unsigned len) { + string ret; + for (unsigned i = index; i < index + len; i++) { + if (i >= buf_sz) + break; + ret += buf_data[i]; + } + return ret; +} diff --git a/xr/sys/main.cc b/xr/sys/main.cc @@ -11,10 +11,14 @@ Config config; Balancer balancer; static void sigcatcher (int sig) { + debugmsg ("Seen signal " + sig + '\n'); if (sig == SIGHUP) balancer.report(true); else if (sig != SIGPIPE) balancer.terminate(true); + // Actually we wouldn't need to test for SIGPIPE, it's ignored (see below). + // Leaving the test in place for future versions, better an extra if + // than forgetting it later. } int main (int argc, char **argv) { diff --git a/xr/sys/str2parts.cc b/xr/sys/str2parts.cc @@ -5,16 +5,33 @@ vector<string> str2parts (string const &s, char sep) { PROFILE("str2parts"); string str = s; - int pos; + unsigned pos; vector<string> parts; - - while ( (pos = str.find_first_of(sep)) >= 0) { - if (pos > 0) + + bool sep_is_first; + while ( (pos = str.find_first_of(sep)) != string::npos) { + if (!pos) { + sep_is_first = true; + parts.push_back(""); + } else { + sep_is_first = true; parts.push_back (str.substr(0, pos)); + } str = str.substr(pos + 1); } if (str.length() > 0) parts.push_back (str); + else if (sep_is_first) + parts.push_back(""); + + /* + ostringstream o; + o << "str2parts: "; + for (unsigned int i = 0; i < parts.size(); i++) + o << "[" << parts[i] << "] "; + o << "\n"; + _debugmsg(o.str()); + */ return (parts); } diff --git a/xr/sys/sys b/xr/sys/sys @@ -25,6 +25,7 @@ #include <sys/socket.h> #include <sys/time.h> #include <sys/types.h> +#include <sys/wait.h> #ifdef INADDR_NONE # define HAVE_INADDR_NONE diff --git a/xr/tcpdispatcher/dispatch.cc b/xr/tcpdispatcher/dispatch.cc @@ -1,6 +1,11 @@ #include "tcpdispatcher" void TcpDispatcher::dispatch() { + // Check that a working algorithm is available. May be missing if + // constructor's "new" failed. + if (!algorithm) + throw static_cast<Error>("No algorithm in Tcpdispatcher::dispatch"); + bool connected = false; // Build up the target list, if not yet done so. The HTTP dispatcher diff --git a/xr/tcpdispatcher/execute.cc b/xr/tcpdispatcher/execute.cc @@ -3,7 +3,30 @@ static map <unsigned long, queue <time_t> > accesslog; static time_t accesslog_lastclean = 0; -void TcpDispatcher::execute() { +// Execute an external program upon excess of hard/soft rates +static void run_excess(string const &prog, char const *ip) { + ostringstream o; + o << prog << ' ' << ip; + msg ((Mstr("Max connection rate exceeded, invoking '") + o.str()) + + "'\n"); + int ret = system(o.str().c_str()); + if (ret == -1) + throw static_cast<Error>("Failed to start system call: ") + + strerror(errno); + else if (WIFEXITED(ret)) { + int exitstat = WEXITSTATUS(ret); + if (exitstat) + warnmsg((Mstr("Program '") + o.str()) + + (Mstr("' exited with exit status ") + exitstat) + + "\n"); + else + msg ("Program terminated normally.\n"); + } else + warnmsg((Mstr("Program '") + o.str()) + + "' terminated abnormally!\n"); +} + +void TcpDispatcher::execute() { msg ((Mstr("Dispatch request for client fd ") + clientfd()) + "\n"); // Check 'softmaxconnrate' and 'hardmaxconnrate' now! @@ -71,6 +94,7 @@ void TcpDispatcher::execute() { << " connections recorded). Client is refused.\n"; warnmsg (o.str()); socketclose(clientfd()); + run_excess(config.hardmaxconnexcess(), inet_ntoa(client_ip)); return; } else if (config.softmaxconnrate() && (accesslog[client_ip.s_addr].size() >= @@ -85,6 +109,7 @@ void TcpDispatcher::execute() { << " connections recorded). Client is deferred for " << config.defertime() << " microseconds.\n"; warnmsg (o.str()); + run_excess(config.softmaxconnexcess(), inet_ntoa(client_ip)); usleep(config.defertime()); } } @@ -92,12 +117,14 @@ void TcpDispatcher::execute() { try { dispatch(); } catch (Error const &e) { + Mutex::lock(&cerr); cerr << e.what() << "\n"; + Mutex::unlock(&cerr); socketclose (clientfd()); return; } - msg ((Mstr("Dispatchign client fd ") + clientfd()) + + msg ((Mstr("Dispatching client fd ") + clientfd()) + (Mstr(" to ") + balancer.backend(target_backend).description()) + (Mstr(", fd ") + backendfd()) + "\n"); @@ -106,7 +133,9 @@ void TcpDispatcher::execute() { try { handle(); } catch (Error const &e) { + Mutex::lock(&cerr); cerr << e.what() << "\n"; + Mutex::unlock(&cerr); } balancer.backend(target_backend).endconnection(); diff --git a/xr/tcpdispatcher/tcpdispatcher1.cc b/xr/tcpdispatcher/tcpdispatcher1.cc @@ -31,4 +31,7 @@ TcpDispatcher::TcpDispatcher(int cfd, struct in_addr cip): algorithm = new Leastconn; break; } + + // NOTE: Memory errors for algorithm pointer are not handled here, + // but in dispatch() (don't want to throw up in the constructor) } diff --git a/xr/webinterface/answer.cc b/xr/webinterface/answer.cc @@ -40,9 +40,9 @@ bool str2bool (string const &s, string const &desc) { if (sscanf (s.c_str(), "%d", &i) > 0) ret = (i != 0); - else if (s == "on") + else if (s == "on" || s == "yes" || s == "true") ret = true; - else if (s == "off") + else if (s == "off" || s == "no" || s == "false") ret = false; else throw static_cast<Error>("Bad ") + desc + " switch '" + s + "'"; @@ -94,6 +94,8 @@ void Webinterface::answer(Httpbuffer req) { return; } + if (uri[0] == '/') + uri = uri.substr(1); vector<string> parts = str2parts (uri, '/'); for (unsigned i = 0; i < parts.size(); i++) parts[i] = decode(parts[i]); @@ -111,10 +113,10 @@ void Webinterface::answer(Httpbuffer req) { // /server/maxconnections/ // /server/maxconnections/NUMBER - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "maxconnections") ) { + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "maxconnections") { unsigned num = 0; - if (parts.size() == 3) + if (parts[2] != "") num = str2uns (parts[2], "server weight"); config.maxconn(num); answer_status(); @@ -146,20 +148,19 @@ void Webinterface::answer(Httpbuffer req) { } // /server/newheader/NEWHEADER - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "newheader") ) { - if (parts.size() == 3) - config.addserverheader(parts[2]); + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "newheader") { + config.addserverheader(parts[2]); answer_status(); return; } // /server/changeheader/NR // /server/changeheader/NR/VALUE - if ( (parts.size() == 3 || parts.size() == 4) && - (parts[0] == "server" && parts[1] == "changeheader") ) { + if (parts.size() == 4 && + parts[0] == "server" && parts[1] == "changeheader") { unsigned ind = headerindex(parts[2]); - if (parts.size() == 3) + if (parts[3] == "") config.removeserverheader(ind); else config.changeserverheader(ind, parts[3]); @@ -185,22 +186,19 @@ void Webinterface::answer(Httpbuffer req) { // /server/logtrafficdir // /server/logtrafficdir/VALUE - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "logtrafficdir") ) { - if (parts.size() == 2) - config.dumpdir(""); - else - config.dumpdir(parts[2]); + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "logtrafficdir") { + config.dumpdir(parts[2]); answer_status(); return; } // /server/clienttimeout // /server/clienttimeout/NUMBER - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "clienttimeout") ) { + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "clienttimeout") { unsigned num = 0; - if (parts.size() == 3) + if (parts[2] != "") num = str2uns (parts[2], "client timeout"); config.client_timeout(num); answer_status(); @@ -209,22 +207,34 @@ void Webinterface::answer(Httpbuffer req) { // /server/backendtimeout // /server/backendtimeout/NUMBER - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "backendtimeout") ) { + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "backendtimeout") { unsigned num = 0; - if (parts.size() == 3) - num = str2uns (parts[2], "client timeout"); + if (parts[2] != "") + num = str2uns (parts[2], "back end timeout"); config.backend_timeout(num); answer_status(); return; } + // /server/dnscachetimeout + // /server/dnscachetimeout/NUMBER + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "dnscachetimeout") { + unsigned num = 0; + if (parts[2] != "") + num = str2uns (parts[2], "DNS cache timeout"); + config.dnscachetimeout(num); + answer_status(); + return; + } + // /server/wakeupinterval // /server/wakeupinterval/NUMBER - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "wakeupinterval") ) { + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "wakeupinterval") { unsigned num = 0; - if (parts.size() == 3) + if (parts[2] != "") num = str2uns (parts[2], "wakeup interval"); if (num) config.checkupsec(0); @@ -235,10 +245,10 @@ void Webinterface::answer(Httpbuffer req) { // /server/checkupinterval // /server/checkupinterval/NUMBER - if ( (parts.size() == 2 || parts.size() == 3) && - (parts[0] == "server" && parts[1] == "checkupinterval") ) { + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "checkupinterval") { unsigned num = 0; - if (parts.size() == 3) + if (parts[2] != "") num = str2uns (parts[2], "checkup interval"); if (num) config.wakeupsec(0); @@ -247,6 +257,135 @@ void Webinterface::answer(Httpbuffer req) { return; } + // /server/timeinterval/SECS + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "timeinterval") { + unsigned num = str2uns(parts[2], "time interval"); + if (num < 1) + throw static_cast<Error>("Time interval may not be less than 1"); + config.connrate_time(num); + answer_status(); + return; + } + + // /server/hardmaxconnrate/NUMBER + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "hardmaxconnrate") { + config.hardmaxconnrate(str2uns(parts[2], "hard maxconnrate")); + answer_status(); + return; + } + + // /server/softmaxconnrate/NUMBER + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "softmaxconnrate") { + config.softmaxconnrate(str2uns(parts[2], "soft maxconnrate")); + answer_status(); + return; + } + + // /server/defertime/NUMBER + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "defertime") { + unsigned num = str2uns(parts[2], "defer time"); + if (num < 1) + throw static_cast<Error>("Defer time may not be less than 1"); + config.defertime(num); + answer_status(); + return; + } + + // /server/closesocketsfast/BOOL + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "closesocketsfast") { + config.fastclose(str2bool(parts[2], "close sockets fast")); + answer_status(); + return; + } + + // /server/addallowfrom/ADDRESS + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "addallowfrom") { + config.addallow(parts[2]); + answer_status(); + return; + } + + // /server/allowfrom/NR + // /server/allowfrom/NR/ADDRESS + if (parts.size() == 4 && + parts[0] == "server" && parts[1] == "allowfrom") { + unsigned ind = str2uns(parts[2], "allowfrom index"); + if (parts[3] != "") + config.changeallow(parts[3], ind); + else + config.deleteallow(ind); + answer_status(); + return; + } + + // /server/adddenyfrom/ADDRESS + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "adddenyfrom") { + config.adddeny(parts[2]); + answer_status(); + return; + } + + // /server/denyfrom/NR + // /server/denyfrom/NR/ADDRESS + if (parts.size() == 4 && + parts[0] == "server" && parts[1] == "denyfrom") { + unsigned ind = str2uns(parts[2], "denyfrom index"); + if (parts[3] != "") + config.changedeny(parts[3], ind); + else + config.deletedeny(ind); + answer_status(); + return; + } + + // /server/hardmaxconnexcess/ + // /server/hardmaxconnexcess/PROGRAM + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "hardmaxconnexcess") { + config.hardmaxconnexcess(parts[2]); + answer_status(); + return; + } + + // /server/softmaxconnexcess/ + // /server/softmaxconnexcess/PROGRAM + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "softmaxconnexcess") { + config.softmaxconnexcess(parts[2]); + answer_status(); + return; + } + + // /server/addbackend/IP:PORT + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "addbackend") { + vector<string> address = str2parts(parts[2], ':'); + if (address.size() != 2) + throw static_cast<Error> + ("When adding back ends, the address must be IP:PORT"); + Backend b; + b.server(address[0]); + b.port(str2uns(address[1], "back end port")); + balancer.addbackend(b, false, false, false); + answer_status(); + return; + } + + // /server/deletebackend/NR + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "deletebackend") { + balancer.deletebackend(backendindex(parts[2])); + answer_status(); + return; + } + // /backend/NR/weight/NUMBER if (parts.size() == 4 && parts[0] == "backend" && parts[2] == "weight") { @@ -281,10 +420,10 @@ void Webinterface::answer(Httpbuffer req) { // /backend/NR/hostmatch/EXPRESSION // /backend/NR/hostmatch - if ( (parts.size() == 3 || parts.size() == 4) && - (parts[0] == "backend" && parts[2] == "hostmatch") ) { + if (parts.size() == 4 && + parts[0] == "backend" && parts[2] == "hostmatch") { unsigned ind = backendindex(parts[1]); - balancer.backend(ind).hostmatch(parts.size() == 3 ? "" : parts[3]); + balancer.backend(ind).hostmatch(parts[3]); answer_status(); return; } @@ -297,5 +436,18 @@ void Webinterface::answer(Httpbuffer req) { return; } - throw static_cast<Error>("No action for URI '") + uri + "'"; + // /backend/NR/backendcheck/ + // /backend/NR/backendcheck/VALUE + if (parts.size() == 4 && + parts[0] == "backend" && parts[2] == "backendcheck") { + unsigned ind = backendindex(parts[1]); + BackendCheck check; + if (parts[3] != "") + check.parse(parts[3]); + balancer.backend(ind).backendcheck(check); + answer_status(); + return; + } + + throw static_cast<Error>("No action for URI '/") + uri + "'"; } diff --git a/xr/webinterface/answerstatus.cc b/xr/webinterface/answerstatus.cc @@ -11,14 +11,20 @@ void Webinterface::answer_status() { "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" "<?xml-stylesheet type=\"text/xsl\" href=\"/xslt\"?>\n" "<status>\n" + " <id>\n" + " <version>" << VER << "</version>\n" + " <distsite>" << DISTSITE << "</distsite>\n" + " </id>\n" " <server>\n" " <address>" << config.sipaddr() << ":" << config.sport() << "</address>\n" " <type>" << config.stypestr() << "</type>\n" - " <dispatchmode>" << config.dispatchmodestr() << "</dispatchmode>\n" - " <maxconnections>" << config.maxconn() << "</maxconnections>\n" " <clienttimeout>" << config.client_timeout() << "</clienttimeout>\n" " <backendtimeout>" << config.backend_timeout() << "</backendtimeout>\n" + " <dispatchmode>" << config.dispatchmodestr() << "</dispatchmode>\n" + " <webinterface>" << config.webinterfaceip() << ':' << config.webinterfaceport() << "</webinterface>\n" + " <dnscachetimeout>" << config.dnscachetimeout() << "</dnscachetimeout>\n" " <buffersize>" << config.buffersize() << "</buffersize>\n" + " <closesocketsfast>" << config.fastclose() << "</closesocketsfast>\n" " <checks>\n" " <wakeupinterval>" << config.wakeupsec() << "</wakeupinterval>\n" " <checkupinterval>" << config.checkupsec() << "</checkupinterval>\n" @@ -28,6 +34,35 @@ void Webinterface::answer_status() { " <debug>" << config.debug() << "</debug>\n" " <logtrafficdir>" << config.dumpdir() << "</logtrafficdir>\n" " </debugging>\n" + " <dosprotection>\n" + " <maxconnections>" << config.maxconn() << "</maxconnections>\n" + " <timeinterval>" << config.connrate_time() << "</timeinterval>\n" + " <hardmaxconnrate>" << config.hardmaxconnrate() << "</hardmaxconnrate>\n" + " <softmaxconnrate>" << config.softmaxconnrate() << "</softmaxconnrate>\n" + " <defertime>" << config.defertime() << "</defertime>\n" + " <hardmaxconnexcess>" << config.hardmaxconnexcess() << "</hardmaxconnexcess>\n" + " <softmaxconnexcess>" << config.softmaxconnexcess() << "</softmaxconnexcess>\n" + " </dosprotection>\n" + " <acl>\n" + " <allow>\n"; + for (unsigned i = 0; i < config.nallow(); i++) + o << + " <allowfrom>\n" + " <nr>" << i << "</nr>\n" + " <mask>" << inet_ntoa(config.allow(i)) << "</mask>\n" + " </allowfrom>\n"; + o << + " </allow>\n" + " <deny>\n"; + for (unsigned i = 0; i < config.ndeny(); i++) + o << + " <denyfrom>\n" + " <nr>" << i << "</nr>\n" + " <mask>" << inet_ntoa(config.deny(i)) << "</mask>\n" + " </denyfrom>\n"; + o << + " </deny>\n" + " </acl>\n" " <http>\n" " <addxrversion>" << config.addxrversion() << "</addxrversion>\n" " <addxforwardedfor>" << config.addxforwardedfor() << "</addxforwardedfor>\n" @@ -44,6 +79,9 @@ void Webinterface::answer_status() { o << " </serverheaders>\n" " </http>\n" + " <backends>" << balancer.nbackends() << "</backends>\n" + " <terminating>" << balancer.terminate() << "</terminating>\n" + " <connections>" << balancer.connections() << "</connections>\n" " </server>\n" ; @@ -62,6 +100,7 @@ void Webinterface::answer_status() { " <bytesserved>" << balancer.backend(i).bytesserved() << "</bytesserved>\n" " <clientsserved>" << balancer.backend(i).clientsserved() << "</clientsserved>\n" " <hostmatch>" << balancer.backend(i).hostmatch() << "</hostmatch>\n" + " <backendcheck>" << balancer.backend(i).backendcheck().setting() << "</backendcheck>\n" " </backend>\n" ; o << diff --git a/xrctl/xrctl b/xrctl/xrctl @@ -1,495 +1,716 @@ #!/usr/bin/perl use strict; +use Getopt::Std; + +# -------------------------------------------------------------------------- +# xrctl: used to start, stop, restart etc. the XR balancer. + +# Default configuration file to read and default logging facility +my $default_conf = '/etc/xrctl.xml'; +my $default_logger = 'logger'; + +# Default settings, must match xr's defaults +my $default_dispatchmode = 'least-connections'; +my $default_maxconnections = 0; +my $default_clienttimeout = 30; +my $default_backendtimeout = 30; +my $default_buffersize = 2048; +my $default_wakeupinterval = 5; +my $default_checkupinterval = 0; +my $default_weight = 1; +my $default_hostmatch = '.'; +my $default_backendcheck = 'connect::'; +my $default_timeinterval = 1; +my $default_hardmaxconnrate = 0; +my $default_softmaxconnrate = 0; +my $default_defertime = 500000; +my $default_hardmaxconnexcess = 0; +my $default_softmaxconnexcess = 0; +my $default_dnscachetimeout = 3600; + +# Cmd line flags +my %opts = (v => 0, + c => $default_conf, + ); +usage() unless (getopts('vc:', \%opts)); +usage() if ($#ARGV == -1); + +# Load configuration +my $xml; +open (my $if, $opts{c}) or die ("Cannot read configuration $opts{c}: $!\n"); +while (my $line = <$if>) { + $xml .= $line; +} +close ($if); +my $xp = new XMLParser($xml); + +# Load up the system config. +my %sysconf; +my $sysxp = new XMLParser($xp->data('system')); +for my $tag qw(piddir pscmd uselogger logdir maxlogsize loghistory path) { + $sysconf{$tag} = $sysxp->data($tag); + msg("System config $tag: $sysconf{$tag}\n"); +} -# Configuration section. Enter your favorite values here. -# ------------------------------------------------------- - -# Directory where PID stamp files are stored, named xr-{service}.pid -my $piddir = '/var/run'; - -# 'ps' command that prints a process ID and the invoking command. The -# following is right for Linux, MacOSX (Darwin) and Solaris. -my $pscmd = '/bin/ps ax -o pid,command'; -$pscmd = '/usr/bin/ps -ef "pid comm"' if (`uname` =~ /SunOS/); - -# Use 'logger' to send output to syslog? 0 means no. -my $use_logger = 1; - -# Directory where log files are stored, named xr-{service}.log. Used -# when logger isn't available or wanted. -my $logdir = '/var/log'; - -# Max log file size in bytes (used by xrctl rotate). Used when logger isn't -# available or wanted. -my $maxlogsize = 100000; - -# Nr. of historical log files to keep (used by xrctl rotate). Used when logger -# isn't available or wanted. -my $loghistory = 10; - -# Paths where executables are searched. -my @bindirs = qw(/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin - /opt/local/bin /opt/local/sbin); - -# Services to balance. All non-default flags for XR must be specified. The -# primary key into hash %services is a self-chosen name. xrctl will supply -# --prefix-timestamp when logging to a bare file (when logger isn't used). -# Also xrctl will supply --pidfile for run control purposes. -# The strings like --server are passed plaintext to the XR invocation. The -# only exception is --host-match - in that case, there's a more complex -# configuration that also states all back ends. -# See the examples below to help you model your favorite dispatcher. -my %services = - ( - - # Web servers balancing to 3 back ends at 10.0.0.1 thru 3. The - # balancer will use HTTP mode and add X-Forwarded-For headers. - 'webone' => - { '--server' => [ qw(http:0:80) ], - '--backend' => [ qw(10.0.0.1:80 10.0.0.2:80 10.0.0.3:80) ], - '--verbose' => undef, - '--add-x-forwarded-for' => undef, - }, - - # Web servers balancing to 3 back ends at 10.1.1.1 thru 3. The - # balancer will use HTTP mode, add X-Forwarded-For headers, and make the - # HTTP sessions sticky to their back ends. NOTE - this server starts on - # port 81 for demo purposes (or it would interfere with webone above). - 'webtwo' => - { '--server' => [ qw(http:0:81) ], - '--backend' => [ qw(10.1.1.1:80 10.1.1.2:80 10.1.1.3:80) ], - '--verbose' => undef, - '--add-x-forwarded-for' => undef, - '--sticky-http' => undef, - }, - - # An Access Control List (ACL) example, again using web balancing. - # Allowed clients are 127.0.0.1 (localhost) and 192.168.*.*, except - # for 192.168.1.250. Also, flag -C / --close-sockets-fast is added to - # avoid TIME_WAIT states under heavy load. - 'webthree' => - { '--server' => [ qw(http:0:82) ], - '--backend' => [ qw(10.1.1.1:80 10.1.1.2:80 10.1.1.3:80) ], - '--verbose' => undef, - '--add-x-forwarded-for' => undef, - '--allow-from' => [ qw(127.0.0.1 192.168.255.255) ], - '--deny-from' => [ qw(192.168.1.250) ], - '--close-sockets-fast' => undef, - }, - - # Multi-hosting two websites. Site "www.onesite.org" has two back ends - # in the 10.1.1 series, "www.othersite.org" has two back ends in the - # 10.1.9 series. Note that the server mode must be http to use this. - 'webfour' => - { '--server' => [ qw(http:0:82) ], - '--host-match' => { 'onesite' => [ qw(10.1.1.1:80 - 10.1.1.2:80) ], - 'othersite' => [ qw(10.1.9.1:80 - 10.1.9.2:80) ], - }, - '--verbose' => undef, - # Other options as --allow-from etc. can also be added here - }, - - # An SSH session balancer on port 2222. We set the client time out - # to 2 hours. Requests are balanced to server1, server2 and server3, - # all to port 22. - 'ssh' => - { '--server' => [ qw(tcp:0:2222) ], - '--backend' => [ qw(server1:22 server2:22 server3:22) ], - '--verbose' => undef, - '--client-timeout' => [ qw(7200) ], - }, - - # Windows Remote Desktop Protocol (RDP) balancing. Windows supports - # only one concurrent client, and we don't want new connections to 'steal' - # existing sessions - so we set the max connections of each back end to 1. - 'rdp' => - { '--server' => [ qw(tcp:0:3389) ], - '--backend' => [ qw(win1:3389:1 win2:3389:1 win33:3389:1) ], - '--verbose' => undef, - '--client-timeout' => [ qw(7200) ], - }, - - # A HTTP forwarder for travelling. Depending on the site where I plug - # in, this reaches some proxy - or localhost:3128, which is a local squid. - # I configure my browser to use localhost:8080 as proxy, and don't have - # to reconfigure browsers anymore. Note the dispatch method which is - # first-available, the first downstream proxy that works is OK for me. - # Note also that the server type is TCP, I don't need HTTP goodies. - # Also the server listens to 127.0.0.1 - only for localhost usage. - 'proxy' => - { '--server' => [ qw(tcp:127.0.0.1:8080) ], - '--backend' => [ qw(10.120.114.2:8080 192.168.1.250:80 - localhost:3128) ], - '--dispatch-mode' => [ qw(first-available) ], - '--verbose' => undef, - }, - - # Simple tunnel to easily access an external proxy at 10.1.1.250. The - # proxy requires authentication, user 'user', password 'secret', which - # is base64-encoded: dXNlcjpzZWNyZXQ= - # Next I can "export http_proxy localhost:8090" and use wget etc. without - # typing the proxy credentials. - 'authproxy' => - { '--server' => [ qw(http:127.0.0.1:8090) ], - '--backend' => [ qw(10.1.1.250:80) ], - '--add-server-header' => [ 'Proxy-Authorization: '. - 'Basic dXNlcjpzZWNyZXQ=' ], - '--verbose' => undef, - '--debug' => undef, - }, - ); - -# Main starts here, configuration ends. -# ------------------------------------- - -# Get the action -my $action = shift (@ARGV); - -# Prepare service list unless given on the command line -if ($#ARGV == -1) { - push (@ARGV, sort (keys (%services))); -} else { - for my $s (@ARGV) { - die ("xrctl: No such service $s\n") unless ($services{$s}); - } -} - -# Verify the configuration -verifyconf (@ARGV); - -# Take appropriate action -if ($action eq 'list') { - list(@ARGV); -} elsif ($action eq 'status') { - status(@ARGV); -} elsif ($action eq 'start') { - start(@ARGV); -} elsif ($action eq 'stop') { - stop(@ARGV); -} elsif ($action eq 'force') { - force(@ARGV); -} elsif ($action eq 'rotate') { - rotate(@ARGV); -} elsif ($action eq 'restart') { - restart(@ARGV); +# Load up the service names. +my @service_name; +for (my $i = 0; ; $i++) { + my $serviceblock = $xp->data('service', $i) or last; + my $servicexp = new XMLParser($serviceblock); + my $name = $servicexp->data('name') + or die ("<service> block lacks <name>\n"); + push (@service_name, $name); + msg ("Service '$name' seen\n"); +} +die ("No service blocks seen\n") if ($#service_name == -1); + +# Take action +$|++; +my $cmd = shift(@ARGV); +@ARGV = @service_name if ($#ARGV == -1); +msg ("Acting on command: $cmd\n"); +if ($cmd eq 'list') { + cmd_list(@ARGV); +} elsif ($cmd eq 'start') { + cmd_start(@ARGV); +} elsif ($cmd eq 'stop') { + cmd_stop(@ARGV); +} elsif ($cmd eq 'force') { + cmd_force(@ARGV); +} elsif ($cmd eq 'restart') { + cmd_restart(@ARGV); +} elsif ($cmd eq 'status') { + cmd_status(@ARGV); +} elsif ($cmd eq 'rotate') { + cmd_rotate(@ARGV); +} elsif ($cmd eq 'configtest') { + cmd_configtest(@ARGV); +} elsif ($cmd eq 'generateconfig') { + cmd_generateconfig(@ARGV); } else { - usage(); + die ("Missing or unknown action $cmd\n"); } -# Show usage and stop -# ------------------- -sub usage() { - die <<"ENDUSAGE"; - -Usage: xrctl list [SERVICE] - show configuration of a service, or of all - xrctl start [SERVICE] - start a service, or all configured services - xrctl stop [SERVICE] - stop a service, or all configured services - xrctl force [SERVICE] - start a service (or all) if not running - xrctl restart [SERVICE] - stop and start a service, or all - xrctl status [SERVICE] - show running status of a service, or of all - xrctl rotate [SERVICE] - rotate logs of a service or of all +# -------------------------------------------------------------------------- +# Top level commands -ENDUSAGE +sub cmd_list { + for my $s (@_) { + print ("Service: $s\n"); + print (" Process name : ", process_name($s), "\n"); + print (" PID file : ", pid_file($s), "\n"); + print (" Logging : ", log_file($s), "\n"); + print (" XR command : ", xr_command($s), "\n"); + } } -# List services and command lines -# ------------------------------- -sub list { - print ("Configured services: ", - join (', ', sort (keys (%services))), - "\n"); +sub cmd_start { for my $s (@_) { - print ("Service $s:\n", - " Process : ", "xr-$s\n", - " PID file : ", pidfile($s), "\n", - " Log file : ", logfile($s), "\n", - " XR command:"); - my @parts = xrcommand($s); - for (my $i = 0; $i <= $#parts; $i++) { - next if ($i == 1); - my $p = $parts[$i]; - $p = "'$p'" if ($p =~ /\s/); - print (" $p"); - } - print ("\n"); + die ("Cannot start service $s, already running\n") + if (is_running($s)); + } + for my $s (@_) { + print ("Service $s: "); + start_service($s); + print ("started\n"); } } -# Show the status of the commands -# ------------------------------- -sub status { +sub cmd_stop { + my @pids; for my $s (@_) { - die ("xrctl: No such service '$s'\n") unless ($services{$s}); - print ("Service $s: ", getstatus($s), "\n"); + my @p = is_running($s) + or die ("Cannot stop service $s, not running\n"); + print ("Service $s: running at @p\n"); + push (@pids, @p); + } + for my $p (@pids) { + msg ("About to kill PID: '$p'\n"); } + kill (15, @pids) if ($#pids > -1); + print ("Services @_: stopped\n"); } -# Start service(s) -# ---------------- -sub start { +sub cmd_force { for my $s (@_) { print ("Service $s: "); - my $status = getstatus($s); - if ($status !~ /^not/) { - print ("already $status\n"); + if (is_running($s)) { + print ("already running\n"); } else { - rundaemon ($s, xrcommand($s)); + start_service($s); print ("started\n"); } } } -# Stop service(s) -# --------------- -sub stop { +sub cmd_restart { + my @pids; for my $s (@_) { - print ("Service $s: "); - my $status = getstatus($s); - if ($status =~ /^not/) { - print ($status, "\n"); - } else { - my $pid = servicebypidfile($s); - $pid = servicebypslist($s) unless ($pid); - die ("Failed to get PID\n") unless ($pid); - kill (15, $pid); - print ("stopping\n"); - } + my @p = is_running($s) + or die ("Cannot restart service $s, not running\n"); + push (@pids, @p); } -} - -# Restart service(s) -# ------------------ -sub restart { + print ("Service(s) @_: "); + kill (15, @pids) if ($#pids > -1); + print ("stoppped\n"); for my $s (@_) { print ("Service $s: "); - my $status = getstatus($s); - if ($status =~ /^not/) { - print ($status, "\n"); - } else { - my $pid = servicebypidfile($s); - $pid = servicebypslist($s) unless ($pid); - die ("Failed to get PID\n") unless ($pid); - kill (15, $pid); - rundaemon ($s, xrcommand($s)); - print ("restarted\n"); - } + start_service($s); + print ("started\n"); } } -# Force service(s) up -# ------------------- -sub force { +sub cmd_status { for my $s (@_) { print ("Service $s: "); - my $status = getstatus($s); - if ($status =~ /^not/) { - rundaemon ($s, xrcommand($s)); - print ("started\n"); - } else { - print ($status, "\n"); - } + print ("not ") unless (is_running($s)); + print ("running\n"); } } -# Rotate logs -# ----------- -sub rotate { - if ($use_logger and findbin('logger')) { - print ("Rotating disabled, logging via logger/syslog\n"); +sub cmd_rotate { + if ($sysconf{uselogger} and find_bin('logger')) { + print ("Rotating not necessary, logging goes via logger\n"); return; } for my $s (@_) { print ("Service $s: "); - my $f = logfile($s); + my $f = log_file($s); + print ("log file $f, "); + if (substr($s, 0, 1) ne '>') { + print ("not a file\n"); + next; + } + $f = substr($f, 1); if (! -f $f) { - print ("no logfile $f\n"); - } elsif ((stat($f))[7] < $maxlogsize) { + print ("not present\n"); + next; + } + if ((stat($f))[7] < $sysconf{maxlogsize}) { print ("no rotation necessary\n"); - } else { - unlink ("$f.$loghistory", "$f.$loghistory.bz2", - "$f.$loghistory.gz"); - for (my $i = $loghistory - 1; $i >= 0; $i--) { - my $src = "$f.$i"; - my $dst = sprintf ("$f.%d", $i + 1); - rename ($src, $dst); - rename ("$src.bz2", "$dst.bz2"); - rename ("$src.gz", "$dst.gz"); - } - rename ($f, "$f.0"); - print ("rotated"); - my $zipper; - if ($zipper = findbin("bzip2") or - $zipper = findbin("gzip")) { - system ("$zipper $f.0"); - print (", zipped"); - } - print ("\n"); - restart($s); + next; + } + unlink("$f.$sysconf{loghistory}", + "$f.$sysconf{loghistory}.bz2", + "$f.$sysconf{loghistory}.gz"); + for (my $i = $sysconf{loghistory} - 1; $i >= 0; $i--) { + my $src = "$f.$i"; + my $dst = sprintf("$f.%d", $i + 1); + rename($src, $dst); + rename("$src.bz2", "$dst.bz2"); + rename("$src.gz", "$dst.gz"); + } + rename($f, "$f.0"); + print("rotated, "); + my $zipper; + if ($zipper = find_bin('bzip2') or $zipper = find_bin('gzip')) { + system ("$zipper $f.0"); + print ("zipped, "); + } + if (my @p = is_running($s)) { + kill (15, @p) if ($#p > -1); + print ("stopped, "); + start_service($s); + print ("started, "); } + print ("done\n"); } } -# Verify a configuration -# ---------------------- -sub verifyconf { +sub cmd_configtest { for my $s (@_) { - my @p = xrcommand($s); - my $cmd = "$p[0] -n"; - for my $i (2..$#p) { - $cmd .= " '$p[$i]'"; - } + print ("Service $s: "); + my $cmd = xr_command($s) . ' --tryout'; if (system ($cmd)) { - die ("xrctl: Configuration of service '$s' probably bad\n", - "Testing command was:\n", - " $cmd\n"); + print ("FAILED, command: $cmd\n"); + } else { + print ("configuration ok\n"); } } } -# Get the status of one balancer service -# -------------------------------------- -sub getstatus($) { - my $s = shift; - die ("xrctl: No such service '$s'\n") unless ($services{$s}); - my $fpid = servicebypidfile($s); - my $ppid = servicebypslist($s); - - # print ("getstatus: fpid=$fpid, ppid=$ppid\n"); - - if (! $fpid and ! $ppid) { - return ("not running"); - } elsif ($fpid == $ppid) { - return ("running"); - } elsif ($fpid and ! $ppid) { - return ("not running (stale pidfile found)"); - } elsif (! $fpid and $ppid) { - return ("running (but no pidfile found)"); - } else { - return ("running (stale pidfile found)"); +sub cmd_generateconfig { + print ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", + "<configuration>\n", + "\n", + " <!-- System description -->\n", + " <system>\n"); + for my $k (sort (keys (%sysconf))) { + print (" <$k>$sysconf{$k}</$k>\n") if ($sysconf{$k} ne ''); } + print (" </system>\n"); + + for my $s (@_) { + generateconfig($s); + } + + print ("</configuration>\n"); +} + + +# -------------------------------------------------------------------------- +# Small utility functions + +# Show usage and die. +sub usage() { + die <<"ENDUSAGE"; + +Usage: xrctl [-FLAGS] action [SERVICE ...] +Flags are: + -v increases verbosity + -c CONFIG specifies the configuration, default $default_conf +Actions are: + configtest builds invocations from the configuration file and validates them + list shows the xr command line + start starts the service(s) if they are not yet running + stop stops the service(s) if they are running + force forces the service(s) up: starts if not running + restart restarts the service(s) if they are running + status shows which services are running + rotate rotates logs of the service(s) + generateconfig queries running XR's for the current configuration and + shows it in the format of $default_conf +Services are the services stated in the configuration. When absent, all +named services are handled. + +ENDUSAGE } -# Return a command to start XR -# ---------------------------- -sub xrcommand ($) { +# Is a service running? +sub is_running { my $s = shift; - my $xr = findbin('xr') or die ("xrctl: Failed to find xr along @bindirs\n"); - my %opts = %{ $services{$s} }; - my @ret = ($xr, "xr-$s", "--pidfile", pidfile($s)); - push (@ret, "--prefix-timestamp") if (! $use_logger or - ! findbin('logger')); - for my $o (sort (keys (%opts))) { - if ($o eq '--host-match') { - my %def = %{ $opts{$o} }; - for my $host (sort (keys (%def))) { - push (@ret, '--host-match', $host); - for my $b(@{ $def{$host} }) { - push (@ret, '--backend', $b); - } - } - } elsif (! $opts{$o}) { - push (@ret, $o); - } else { - my @args = @{ $opts{$o} }; - if ($#args == -1) { - push (@ret, $o); - } else { - for my $arg (@args) { - push (@ret, $o); - push (@ret, $arg); - } - } + open (my $if, "$sysconf{pscmd} |") + or die ("Cannot start '$sysconf{pscmd}': $!\n"); + my @ret; + while (my $line = <$if>) { + chomp ($line); + $line =~ s/^\s*//; + my ($pid, $cmd) = split(/\s+/, $line); + msg("Command '$cmd' at pid '$pid' (line $line)\n"); + if ($cmd =~ /^xr-$s/) { + push (@ret, $pid); + msg ("Candidate PID: $pid\n"); } } return (@ret); } -# Return the PID file of a given service -# -------------------------------------- -sub pidfile ($) { +# Unconditionally start a given service +sub start_service { my $s = shift; - return ("$piddir/xr-$s.pid"); + my @args = xr_cmdarr($s); + my $xr = find_bin('xr'); + my $logstr = log_file($s); + my $logtype = substr($logstr, 0, 1); + my $logout = substr($logstr, 1); + + # Try out the command line + my $cmdline = xr_command($s) . ' --tryout'; + system ($cmdline) + and die ("Command line '$cmdline' fails to parse\n"); + + my $pid = fork(); + die ("Cannot fork: $!\n") unless (defined ($pid)); + return if ($pid > 0); + + # Child branch + open (STDIN, '/dev/null') or die ("Cannot read /dev/null: $!\n"); + + if ($logtype eq '|') { + open (STDOUT, "|$logout") + or die ("Cannot pipe stdout to $logout: $!\n"); + open (STDERR, "|$logout") + or die ("Cannot pipe stderr to $logout: $!\n"); + } else { + open (STDOUT, ">>$logout") + or die ("Cannot append stdout to $logout: $!\n"); + open (STDERR, ">>$logout") + or die ("Cannot append stderr to $logout: $!\n"); + } + exec ({$xr} @args); + exit (1); } -# Examine the contents of a PID file -# ---------------------------------- -sub servicebypidfile($) { - my $s = shift; - my $p = pidfile($s); - if (open (my $f, $p)) { - my $pid = <$f>; - chomp ($pid); - return ($pid); +# Verbose message. +sub msg { + return unless ($opts{v}); + print (@_); +} + +# Find a binary along the path +sub find_bin { + my $bin = shift; + for my $d (split (/:/, $sysconf{path})) { + return ("$d/$bin") if (-x "$d/$bin"); + } + return (undef); +} + +# Process name according to a service name +sub process_name { + my $service = shift; + return ("xr-$service"); +} + +# PID file according to a service name +sub pid_file { + my $service = shift; + return ($sysconf{piddir} . '/' . process_name($service) . '.pid') + if ($sysconf{piddir}); + return (undef); +} + +# Log file according to a service name +sub log_file { + my $service = shift; + my $logger = find_bin($default_logger); + if (istrue($sysconf{uselogger}) and defined($logger)) { + return ("|$logger -t 'xr-$service'"); } else { - return (undef); + return ('>' . $sysconf{logdir} . '/' . + process_name($service) . '.log'); } } -# Get the PID of a service using the PS list -# ------------------------------------------- -sub servicebypslist($) { - my $s = shift; - open (my $if, "$pscmd |") or die ("xrctl: Cannot start '$pscmd': $!\n"); - while (my $line = <$if>) { - chomp ($line); - my $p = sprintf ("%d", $line); - next unless ($p); - my $c = $line; - $c =~ s/^[\d\s]*//; - # print ("LF [$s], p=[$p], c=[$c]\n"); - return ($p) if ($c =~ /^[^\s]*xr-$s/); +# XR command according to a service name as one string +sub xr_command { + my $service = shift; + my @parts = xr_cmdarr($service); + msg ("Command: @parts\n"); + my $ret = find_bin('xr'); + for (my $i = 1; $i <= $#parts; $i++) { + my $sub = $parts[$i]; + $sub = "'$sub'" if ($sub =~ /\s/); + $ret .= ' ' . $sub; + } + return ($ret); +} + +# XR command according to a service name as an array, including ARGV[0] +# pseudo-name +sub xr_cmdarr { + my $service = shift; + + my @cmd; + push (@cmd, "xr-$service"); + + push (@cmd, '--pidfile', pid_file($service)) if (pid_file($service)); + push (@cmd, '--prefix-timestamp') + if (!istrue($sysconf{uselogger}) or !find_bin('logger')); + + # Fetch the <service> block for this service + my $sp = xml_serviceparser($service) + or die ("Failed to locate <service> block for service '$service'\n"); + + # Service descriptions + my $type = 'tcp'; + $type = $sp->data('type') if ($sp->data('type')); + my $addr = '0:10000'; + $addr = $sp->data('address') if ($sp->data('address')); + my $full = "$type:$addr"; + push (@cmd, '--server', $full) if ($full ne 'tcp:0:10000'); + + # Flags that should go on the command line if the bool-tag is true + my %boolflags = (closesocketsfast => '--close-sockets-fast', + verbose => '--verbose', + debug => '--debug'); + + # Handle general flags and boolflags + push (@cmd, + flag($sp, '--web-interface', 'webinterface', ''), + flag($sp, '--dispatch-mode', 'dispatchmode', + $default_dispatchmode), + flag($sp, '--max-connections', 'maxconnections', + $default_maxconnections), + flag($sp, '--client-timeout', 'clienttimeout', + $default_clienttimeout), + flag($sp, '--backend-timeout', 'backendtimeout', + $default_backendtimeout), + flag($sp, '--buffer-size', 'buffersize', + $default_buffersize), + flag($sp, '--wakeup-interval', 'wakeupinterval', + $default_wakeupinterval), + flag($sp, '--checkup-interval', 'checkupinterval', + $default_checkupinterval), + flag($sp, '--time-interval', 'timeinterval', + $default_timeinterval), + flag($sp, '--hard-maxconnrate', 'hardmaxconnrate', + $default_hardmaxconnrate), + flag($sp, '--soft-maxconnrate', 'softmaxconnrate', + $default_softmaxconnrate), + flag($sp, '--defer-time', 'defertime', + $default_defertime), + flag($sp, '--hard-maxconn-excess', 'hardmaxconnexcess', + $default_hardmaxconnexcess), + flag($sp, '--soft-maxconn-excess', 'softmaxconnexcess', + $default_softmaxconnexcess), + flag($sp, '--dns-cache-timeout', 'dnscachetimeout', + $default_dnscachetimeout), + flag($sp, '--log-traffic-dir', 'logtrafficdir', '')); + for my $k (sort (keys (%boolflags))) { + push (@cmd, $boolflags{$k}) if (istrue($sp->data($k))); + } + + # ACL's + for (my $i = 0; ; $i++) { + my $mask = $sp->data('allowfrom', $i) or last; + push (@cmd, '--allow-from', $mask); + } + for (my $i = 0; ; $i++) { + my $mask = $sp->data('denyfrom', $i) or last; + push (@cmd, '--deny-from', $mask); + } + + # HTTP goodies + push (@cmd, '--add-xr-version') + if ($sp->data('addxrversion') and + istrue($sp->data('addxrversion'))); + push (@cmd, '--add-x-forwarded-for') + if ($sp->data('addxforwardedfor') and + istrue($sp->data('addxforwardedfor'))); + push (@cmd, '--sticky-http') + if ($sp->data('stickyhttp') and + istrue($sp->data('stickyhttp'))); + for (my $i = 0; ; $i++) { + my $h = $sp->data('header', $i) or last; + push (@cmd, '--add-server-header', $h); + } + + # The <backend> blocks for this service + my $last_hostmatch = $default_hostmatch; + my $last_backendcheck = $default_backendcheck; + for (my $i = 0; ; $i++) { + my $bp = xml_backendparser($sp, $i) or last; + + # Handle host match + my $hm = $bp->data('hostmatch'); + if ($hm and $hm ne $last_hostmatch) { + push (@cmd, '--host-match', $hm); + } elsif ($hm eq '' and $last_hostmatch ne '') { + push (@cmd, '--host-match', $default_hostmatch); + } + $last_hostmatch = $hm; + + # Handle back end checks + my $bc = $bp->data('backendcheck'); + if ($bc and $bc ne $last_backendcheck) { + push (@cmd, '--backend-check', $bc); + } elsif ($bc eq '' and $last_backendcheck ne '') { + push (@cmd, '--backend-check', $default_backendcheck); + } + $last_backendcheck = $bc; + + # Get address, weight and max connections + my $wt = $bp->data('weight') or $default_weight; + my $mx = $bp->data('maxconnections') or $default_maxconnections; + my $ad = $bp->data('address') + or die ("Backend in service '$service' lacks <address>\n"); + + if ($mx and + ($wt ne $default_weight or $mx ne $default_maxconnections)) { + $ad .= ":$mx"; + } + if ($wt and ($wt ne $default_weight)) { + $ad .= ":$wt"; + } + push (@cmd, '--backend', $ad); + } + + # All done + my @ret; + for my $c (@cmd) { + push (@ret, $c) if ($c ne ''); + } + return (@ret); +} + +# Prepare a flag for the command line if it is defined and if it is +# not equal to the default +sub flag { + my ($parser, $longopt, $tag, $default) = @_; + msg ("Flag tag $tag: ", $parser->data($tag), " (default: '$default')\n"); + if ($parser->data($tag) ne '' && + $parser->data($tag) ne $default) { + msg ("Flag values meaningful: ", + $longopt, ' ', $parser->data($tag), "\n"); + return ($longopt, $parser->data($tag)); } return (undef); } -# Return the log file of a given service -# -------------------------------------- -sub logfile($) { - my $s = shift; - return ('logger') if ($use_logger and findbin('logger')); - return ("$logdir/xr-$s.log"); +# Is a boolean value true +sub istrue { + my $val = shift; + return (1) if ($val eq 'true' or $val eq 'on' or + $val eq 'yes' or $val != 0); + return (undef); } -# Find a binary along the path -# ----------------------------- -sub findbin ($) { - my $b = shift; - for my $d (@bindirs) { - return ("$d/$b") if (-x "$d/$b"); +# Fetch an XMLParser for a <service> block given a service name +sub xml_serviceparser { + my $service = shift; + + for (my $i = 0; ; $i++) { + $xml = $xp->data('service', $i) or return (undef); + msg ("XML service block: $xml\n"); + my $sub = new XMLParser($xml); + return ($sub) if ($sub->data('name') eq $service); } return (undef); } -# Run a command as a daemon -# ------------------------- -sub rundaemon { +# Fetch an XMLParser for a <backend> block given a service parser and +# an order number +sub xml_backendparser { + my ($serviceparser, $order) = @_; + $order = 0 unless ($order); + my $xml = $serviceparser->data('backend', $order) or return (undef); + return (new XMLParser($xml)); +} + +# Generate a service configuration from the running XR, if it has a +# web interface +sub generateconfig { my $s = shift; - my @args = @_; + msg ("Generating runtime configuration for service '$s'\n"); - my $logger = findbin('logger'); - my $outfile = logfile($s); - - my $pid = fork(); - return if ($pid > 0); + my $sp = xml_serviceparser($s) or die ("No service '$s' known.\n"); + my $webint = $sp->data('webinterface'); + $webint =~ s/^0:/localhost:/; - # Child branch - close (STDIN); - open (STDIN, "/dev/null") - or die ("xrctl (daemon): Can not reopen stdin to /dev/null: $!\n"); - if ($use_logger and $logger) { - open (STDOUT, "|$logger -t xr-$s") - or die ("xrctl (daemon): Cannot reopen stdout to logger: $!\n"); - open (STDERR, "|$logger -t xr-$s") - or die ("xrctl (daemon): Cannot reopen stdout to logger: $!\n"); + if ($webint eq '') { + print ("\n", + " <!-- Configuration for service $s not generated,\n", + " no web interface known -->\n"); + return; + } + + print ("\n", + " <!-- Configuration for service $s,\n", + " obtained at web interface $webint -->\n", + " <service>\n", + " <name>$s</name>\n"); + + # Get the configuration from a running XR. Try LWP::UserAgent or + # fall back to wget. + my $response_blob; + eval ("require LWP::UserAgent;"); + if ($@) { + msg ("LWP::UserAgent not present, trying wget\n"); + my $wget = find_bin('wget') + or die ("Neither LWP::UserAgent nor wget found.\n", + "Cannot contact service web interface $webint.\n"); + open (my $if, "wget --no-proxy -q -O- http://$webint/ |") + or die ("Cannot start wget: $!\n"); + while (my $line = <$if>) { + $response_blob .= $line; + } + close ($if) or die ("Wget indicates failure\n"); } else { - open (STDOUT, ">>$outfile") - or die ("xrctl (deamon): Cannot reopen stdout to $outfile: $!\n"); - open (STDERR, ">>$outfile") - or die ("xrctl (deamon): Cannot reopen stderr to $outfile: $!\n"); + my $ua = LWP::UserAgent->new(); + my $res = $ua->get("http://$webint/"); + die ("Failed to contact web interface at $webint:\n", + $res->status_line(), "\n") unless ($res->is_success()); + + $response_blob = $res->content(); } - my $truecmd = shift (@args); - exec ({ $truecmd } @args); - exit (1); + + # Print the config. + my $active = 0; + for my $l (split (/\n/, $response_blob)) { + if ($l =~ /<server>/) { + print ($l, "\n"); + $active = 1; + } elsif ($l =~ /<\/status>/) { + $active = 0; + } elsif ($active) { + print ($l, "\n"); + } + } + + print (" </service>\n"); +} + +# -------------------------------------------------------------------------- +# Idiotically simple XML parser. Used instead of a "real" parser so that +# xrctl isn't dependent on modules and can run anywhere. Safe for using +# with xr-style XML configs, but not with any XML in the free. + +package XMLParser; +sub new { + my ($proto, $doc) = @_; + my $self = {}; + + die ("Invalid or missing XML document\n") unless ($doc); + + my $docstr = ''; + for my $p (split (/\n/, $doc)) { + $docstr .= $p; + } + + # Whitespace between tags is trash + $docstr =~ s{>\s+<}{><}g; + + # Remove comments from the doc + FINDCOMM: + for (my $i = 0; $i <= length($docstr); $i++) { + next unless (substr($docstr, $i, 4) eq '<!--'); + for (my $end = $i + 4; $end <= length($docstr); $end++) { + if (substr($docstr, $end, 3) eq '-->') { + # print ("Comment: ", substr($docstr, $i, $end + 3 - $i), "\n"); + $docstr = substr($docstr, 0, $i) . substr($docstr, $end + 3); + $i--; + next FINDCOMM; + } + } + } + + # print $docstr, "\n"; + + $self->{xml} = $docstr; + bless ($self, $proto); + + return ($self); +} + +sub data { + my ($self, $tag, $order) = @_; + die ("XML::data: no tag to search for\n") unless ($tag); + $order = 0 unless ($order); + my $xml = $self->{xml}; + my $ret = undef; + for (0..$order) { + my $start = _findfirst($xml, "<$tag>"); + return (undef) unless (defined ($start)); + $xml = substr($xml, $start + length("<$tag>")); + # print ("start $start $xml\n"); + my $end = _findfirst($xml, "</$tag>"); + die ("Failed to match </$tag>, invalid XML\n") + unless (defined ($end)); + $ret = substr($xml, 0, $end); + $xml = substr($xml, $end + length("</tag>")); + # print ("end $end $xml\n"); + } + return ($ret); } + +sub _findfirst { + my ($stack, $needle) = @_; + # print ("needle: $needle, stack: $stack\n"); + for my $i (0..length($stack)) { + my $sub = substr($stack, $i, length($needle)); + # print ("sub: $sub\n"); + return ($i) if ($sub eq $needle); + } + return (undef); +} + +sub _findlast { + my ($stack, $needle) = @_; + for (my $i = length($stack); $i >= 0; $i--) { + return ($i) if (substr($stack, $i, length($needle)) eq $needle); + } + return (undef); +} + +1;