commit 9cd7d67d498bd3e2093eb453e1c58f307ad42136
parent 778f28485845813047ba9b1745ea965e85dc71bd
Author: finwo <finwo@pm.me>
Date: Sat, 3 Jan 2026 19:36:21 +0100
2.31
Diffstat:
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;