crossroads

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

commit 74a71dbeeea3a2ce4cd28f0a21038dbac15756f6
parent a4c87a53fff698e749a3f5354277decf9a1e2029
Author: finwo <finwo@pm.me>
Date:   Sat,  3 Jan 2026 19:38:46 +0100

2.70

Diffstat:
MChangeLog | 15+++++++++++++++
MMakefile | 2+-
Mdoc/xr.odt | 0
Atest/backendmon | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/downer | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/externalchecker | 10++++++++++
Atest/externaldispatcher | 8++++++++
Mtest/onfail | 3+--
Mtest/onstart | 3+--
Mxr/Dispatchers/tcpdispatcher/execute.cc | 4++--
Mxr/backend/check.cc | 27+++++++++++++++++++++++----
Mxr/backend/connect.cc | 7+++++--
Mxr/backend/live.cc | 4----
Mxr/config/config | 4++++
Mxr/config/config1.cc | 1+
Mxr/config/parsecmdline.cc | 6+++++-
Axr/config/webinterface_auth.cc | 11+++++++++++
Mxr/etc/Makefile.class | 15++++++++++++++-
Mxr/etc/status-nosavebutton.xslt | 10++++++++++
Mxr/etc/usage.txt | 4+++-
Axr/sys/base64decode.cc | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mxr/sys/sys | 1+
Mxr/webinterface/answer.cc | 40++++++++++++++++++++++++++++++++++++++++
Mxr/webinterface/answerstatus.cc | 1+
Mxr/webinterface/execute.cc | 28++++++++++++++--------------
Mxr/webinterface/serve.cc | 2+-
Mxrctl/xrctl | 1+
27 files changed, 391 insertions(+), 35 deletions(-)

diff --git a/ChangeLog b/ChangeLog @@ -1,3 +1,18 @@ +2.70 [KK 2010-10-28] +- Bugfix in 3-secs delay during out of file handles conditions (see + xr/balancer/serve.cc) +- Potential bug fixed in web interface responder (check for validity + of client socket added). +- Browser requests for /favicon.ico are silently dropped (404 return), + instead of showing up in the error log as unknown url. +- Small codechanges in back end testing via TCP. E.g, when tcp-connect + checking a back end, the connection is attempted twice by default. + Just once appears to fail on some systems, twice fixes the problem. +- Basic authentication for the web interface implemented. This can be + given on the commandline using --webinterface-auth, in xrctl.xml + using <webinterfaceauth>, or injected later on the url + /server/webinterfaceauth/USER:PASS + 2.69 [KK 2010-10-08] - Installation paths in the make install process can be tweaked, thanks Felix A. W. O. for suggesting this. diff --git a/Makefile b/Makefile @@ -1,7 +1,7 @@ # Top-level Makefile for XR # ------------------------- -VER ?= 2.69 +VER ?= 2.70 PREFIX ?= $(DESTDIR)/usr BINDIR ?= $(PREFIX)/sbin MANDIR ?= $(PREFIX)/share/man diff --git a/doc/xr.odt b/doc/xr.odt Binary files differ. diff --git a/test/backendmon b/test/backendmon @@ -0,0 +1,61 @@ +#!/usr/bin/env perl + +use strict; +use XML::Simple; +use LWP::UserAgent; + +# Check arguments +if ($#ARGV != 1) { + die << "ENDUSAGE"; + +Usage: $0 webinterface-url period +Example: $0 http://localhost:20001 5 + +The XR web interface at the stated URL is checked and the back end states +are reported. + +ENDUSAGE +} + +# Process +while (1) { + check($ARGV[0]); + sleep($ARGV[1]); +} + +# Check the web interface. Take unavailable back ends offline. +sub check($) { + my $url = shift; + + # Access web interface + my $ua = LWP::UserAgent->new(); + my $resp = $ua->get($url); + if (! $resp->is_success()) { + warn("Failed to access the XR web interface on '$url': ", + $resp->status_line(), "\n"); + return; + } + + # Parse the XML + my $xml; + eval { + $xml = XMLin($resp->content()); + }; + if ($@) { + warn("Failed to parse web interface response: $@\n"); + return; + } + + # print Dumper $xml; + + my @backends = @{ $xml->{backend} }; + print("\n", scalar(localtime()), "\n"); + for my $b (@backends) { + print(" Back end ", $b->{nr}, " at ", $b->{address}, + ": available=", $b->{available}, " up=", $b->{up}); + print(" DEAD") if ($b->{live} ne 'alive'); + print("\n"); + } +} + + diff --git a/test/downer b/test/downer @@ -0,0 +1,65 @@ +#!/usr/bin/env perl + +use strict; +use XML::Simple; +use LWP::UserAgent; +use Data::Dumper; + +# Check arguments +if ($#ARGV != 1) { + die << "ENDUSAGE"; + +Usage: $0 webinterface-url period +Example: $0 http://localhost:20001 5 + +The XR web interface at the stated URL is checked for unavailable back ends. +Such back ends are taken offline. This is repeated each 'period' seconds. + +ENDUSAGE +} + +# Process +while (1) { + check($ARGV[0]); + sleep($ARGV[1]); +} + +# Check the web interface. Take unavailable back ends offline. +sub check($) { + my $url = shift; + + # Access web interface + my $ua = LWP::UserAgent->new(); + my $resp = $ua->get($url); + if (! $resp->is_success()) { + warn("Failed to access the XR web interface on '$url': ", + $resp->status_line(), "\n"); + return; + } + + # Parse the XML + my $xml; + eval { + $xml = XMLin($resp->content()); + }; + if ($@) { + warn("Failed to parse web interface response: $@\n"); + return; + } + + # print Dumper $xml; + + my @backends = @{ $xml->{backend} }; + for my $b (@backends) { + print("Back end ", $b->{nr}, " at ", $b->{address}, + ": available=", $b->{available}, " up=", $b->{up}, "\n"); + if ($b->{available} ne 'available' and $b->{up} eq 'up') { + print(" Marking back end as 'down'.\n"); + my $resp = $ua->get($url . '/backend/' . $b->{nr} . '/up/0'); + warn("Failed to mark back end down: ", $resp->status_line(), "\n") + unless ($resp->is_success()); + } + } +} + + diff --git a/test/externalchecker b/test/externalchecker @@ -0,0 +1,10 @@ +#!/usr/bin/env perl + +# Example of an external checker + +print STDERR (">>>> External checker: ", scalar(localtime()), " @ARGV\n"); +if ($ARGV[0] eq 'localhost:8000') { + print("1\n"); +} else { + print("0\n"); +} diff --git a/test/externaldispatcher b/test/externaldispatcher @@ -0,0 +1,8 @@ +#!/usr/bin/env perl + +# Example of an external dispatcher. +# ---------------------------------- + +print STDERR (">>>> External dispatcher called on ", scalar(localtime()), + " with arguments @ARGV\n"); +print("0\n"); diff --git a/test/onfail b/test/onfail @@ -1,4 +1,3 @@ #!/bin/sh -echo Back end $2 with $3 connections failed for client $1 \ - >> /tmp/activity.log +echo `date` Back end $2 with $3 connections failed for client $1 1>&2 diff --git a/test/onstart b/test/onstart @@ -1,6 +1,5 @@ #!/bin/sh -echo Client $1 will be handled by $2, $3 connections so far \ - >> /tmp/activity.log +echo `date` Client $1 will be handled by $2, $3 connections so far 1>&2 diff --git a/xr/Dispatchers/tcpdispatcher/execute.cc b/xr/Dispatchers/tcpdispatcher/execute.cc @@ -8,7 +8,7 @@ void TcpDispatcher::execute() { !check_acl()) return; - msg("Dispatch request for client fd " << clientfd() << '\n'); + debugmsg("Dispatch request for client fd " << clientfd() << '\n'); // Try to determine the back end. try { @@ -33,7 +33,7 @@ void TcpDispatcher::execute() { } // Dispatch! - msg ("Dispatching client fd " << clientfd() << " to " << + msg("Dispatching client fd " << clientfd() << " to " << balancer.backend(targetbackend()).description() << ", fd " << backendfd() << '\n'); diff --git a/xr/backend/check.cc b/xr/backend/check.cc @@ -12,8 +12,27 @@ void Backend::check() { case BackendCheck::c_connect: if (backendcheck().server() == "" && backendcheck().port() == 0) { // Most common: TCP connect to the actual back end - connect(); - socketclose(sock()); + tester = *this; + // Retry CONNCHECK times (see etc/Makefile.class). + for (int i = 0; i < CONNCHECKS; i++) { + tester.connect(); + socketclose(tester.sock()); + debugmsg("TCP-connect testing back end " << tester.description() << + " try " << i << ": " << + (tester.live() ? "alive" : "not-alive") + << '\n'); + } + if ( (tester.live() && !live()) || + (!tester.live() && live()) ) { + debugmsg("State of back end " << tester.description() << + " is now " << (tester.live() ? "alive" : "not-alive") << + '\n'); + live(tester.live()); + } + /* This was: + connect(); + socketclose(sock()); + */ } else { // TCP connects to an alternative server or port. // We instantiate a dummy backend and let it connect to the "other" @@ -26,8 +45,8 @@ void Backend::check() { tester.connect(); socketclose (tester.sock()); live(tester.live()); - msg ("Alternative back end for testing " << - tester.description() << " is " << livestr() << '\n'); + debugmsg("Alternative back end for testing " << + tester.description() << " is " << livestr() << '\n'); } break; diff --git a/xr/backend/connect.cc b/xr/backend/connect.cc @@ -54,8 +54,6 @@ bool Backend::connect() { fdset.add (clsocket); fdset.wait_rw(); - debugmsg("Connecting to " << description() << '\n'); - # ifdef CONNECTCHECK_ONLY_WRITABLE if (fdset.writeable(clsocket)) islive = true; @@ -67,6 +65,11 @@ bool Backend::connect() { if (fdset.writeable(clsocket) && !fdset.readable(clsocket)) islive = true; else { + debugmsg("Connect socket writable: " << + (fdset.writeable(clsocket) ? "yes" : "no") << + ", readable: " << + (fdset.readable(clsocket) ? "yes" : "no") << + '\n'); socketclose(clsocket); markconnecterror(); } diff --git a/xr/backend/live.cc b/xr/backend/live.cc @@ -4,10 +4,6 @@ void Backend::live (bool state) { PROFILE("Backend::live"); mutex_lock(&islive); - bool oldstate = islive; islive = state; mutex_unlock(&islive); - - if (oldstate != state) - msg ("Marking back end " + description() + " as " + livestr() + "\n"); } diff --git a/xr/config/config b/xr/config/config @@ -111,6 +111,9 @@ public: } int webinterfaceport() const { return webinterface_port; } + void webinterface_auth(string const &s); + string webinterface_auth() const { return web_auth; } + unsigned nserverheaders() const { return (serverheaders.size()); } string const &serverheader (unsigned n) { return (serverheaders[n]); @@ -229,6 +232,7 @@ private: static bool use_webinterface; static string webinterface_ip; static int webinterface_port; + static string web_auth; static string dump_dir; static unsigned soft_maxconnrate; static unsigned hard_maxconnrate; diff --git a/xr/config/config1.cc b/xr/config/config1.cc @@ -31,6 +31,7 @@ int Config::ipstore_timeout; bool Config::use_webinterface = false; string Config::webinterface_ip; int Config::webinterface_port; +string Config::web_auth; string Config::dump_dir; unsigned Config::soft_maxconnrate = 0; unsigned Config::hard_maxconnrate = 0; diff --git a/xr/config/parsecmdline.cc b/xr/config/parsecmdline.cc @@ -19,7 +19,7 @@ void Config::parsecmdline (int ac, char **av) { throw Error("Bad command line '" + cmdline + "'\n" + USAGE); # define OPTSTRING "?a:A:B:b:c:CDd:E:e:fF:Gg:hH:Ij:l:" \ - "m:M:nPQ:r:R:Ss:t:T:u:U:vVW:w:xXy:z:Z:" + "m:M:nPQ:r:R:Ss:t:T:u:U:vVW:w:xXy:Y:z:Z:" # ifdef HAVE_GETOPT_LONG static struct option longopts[] = { { "allow-from", required_argument, 0, 'a' }, @@ -59,6 +59,7 @@ void Config::parsecmdline (int ac, char **av) { { "version", no_argument, 0, 'V' }, { "wakeup-interval", required_argument, 0, 'w' }, { "web-interface", required_argument, 0, 'W' }, + { "web-interface-auth", required_argument, 0, 'Y' }, { "add-xr-version", no_argument, 0, 'X' }, { "add-x-forwarded-for", no_argument, 0, 'x' }, { "onfail", required_argument, 0, 'y' }, @@ -262,6 +263,9 @@ void Config::parsecmdline (int ac, char **av) { case 'y': onfail(optarg); break; + case 'Y': + webinterface_auth(optarg); + break; case 'z': onstart(optarg); break; diff --git a/xr/config/webinterface_auth.cc b/xr/config/webinterface_auth.cc @@ -0,0 +1,11 @@ +#include "config" + +void Config::webinterface_auth(string const &s) { + // Make sure there is a ':' delimiter in the parameter + if (s.length() > 0 && s.find_first_of(':') == string::npos) + throw Error("Bad web interface authentication parameter, " + "format must be username:password"); + + web_auth = s; +} + diff --git a/xr/etc/Makefile.class b/xr/etc/Makefile.class @@ -3,9 +3,21 @@ OBJ = $(patsubst %.cc, $(BASE)/xr/$(BUILDDIR)/$(DIR)_%.o, $(SRC)) DIR = $(shell pwd | sed 's:.*/::') SYS = $(shell uname) HST = $(shell hostname) + +# How many times should XR check a back end using plain TCP connects? +# Just once fails on some systems. Twice seems to do the job. +CONNCHECKS = 2 + +# When connecting to a back end with the socket in nonblocking mode, is +# the connection alive when the socket is just writable? When commented out, +# the socket must also be non-readable (which should be the standard, but on +# some systems appears not to work). Leave this commented out unless you are +# experimenting or really know what you are doing. # CCC = -DCONNECTCHECK_ONLY_WRITABLE -ifeq ($(HST), Thera.local) +# On my dev system, I want warnings to stop the compilation. Does not apply to +# others where warnings won't stop the build. +ifeq ($(HST), Kostunrix.local) ERRFLAG = -Werror endif @@ -19,6 +31,7 @@ $(BASE)/xr/$(BUILDDIR)/$(DIR)_%.o: %.cc -DSYS='"$(SYS)"' -D$(SYS) $(MEMDEBUG) $(CCC) \ -DCONF_CC='"$(CONF_CC)"' -DCONF_LIB='"$(CONF_LIB)"' \ -DCONF_OPTFLAGS='"$(CONF_OPTFLAGS)"' $(CONF_STRNSTR) \ + -DCONNCHECKS=$(CONNCHECKS) \ $(CONF_GETOPT) $(CONF_GETOPT_LONG) $(CONF_INET_ATON) \ -I$(BASE)/xr \ -c -g -Wall $(ERRFLAG) -o $@ $< diff --git a/xr/etc/status-nosavebutton.xslt b/xr/etc/status-nosavebutton.xslt @@ -608,6 +608,16 @@ </td> </tr> <xsl:apply-templates select="/status/server/acl/deny"/> + + <tr> + <td>Web interface credentials</td> + <td>Format username:password</td> + <td colspan="2" align="right"> + <input type="text" size="30" name="webinterfaceauth" class="input" + id="webinterfaceauth" value="{webinterfaceauth}" + onchange="goto('/server/webinterfaceauth/', 'webinterfaceauth');"/> + </td> + </tr> <xsl:if test="/status/server/type = 'http'"> <xsl:apply-templates select="/status/server/http"/> diff --git a/xr/etc/usage.txt b/xr/etc/usage.txt @@ -122,7 +122,7 @@ may not exist on your platform): to the command are: the client's IP address, and the back end address. --onstart CMD, -z CMD Runs CMD just before letting a back end handle a client's connection. - For the arguments of CMD see -y. + For the arguments of CMD see --onfail, -y. --pidfile FILE, -p FILE FILE is written with the process id of XR upon startup, and removed upon exit. @@ -167,6 +167,8 @@ may not exist on your platform): matching URL. Only available when the server is in http mode. --web-interface IP:PORT, -W IP:PORT Starts a web interface on specified IP address and port. + --web-interface-auth USER:PASS, -Y USER:PASS + Access to the web interface will be protected by basic authentication. --verbose, -v Increases verbosity, default is silent operation. --version, -V diff --git a/xr/sys/base64decode.cc b/xr/sys/base64decode.cc @@ -0,0 +1,93 @@ + +#include "sys" + +/* ========================================================================= + * The remainder of the code originates from http://base64.sourceforge.net/. + * The original version was written by Bob Trower. + * Adapted for C++ by me [kk] + * ========================================================================= + */ + +/* +** Translation Table as described in RFC1113 +** Not used during decode phase +static +const char *xx_cb64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; +*/ + +/* +** Translation Table to decode (created by author) +*/ +static +const char *xx_cd64 = "|$$$}rstuvwxyz{$$$$$$$>?@" + "ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ" + "[\\]^_`abcdefghijklmnopq"; + +/* +** decodeblock +** +** decode 4 '6-bit' characters into 3 8-bit binary bytes +*/ +static void decodeblock(char in[4], char out[4]) +{ + out[0] = (unsigned char ) (in[0] << 2 | in[1] >> 4); + out[1] = (unsigned char ) (in[1] << 4 | in[2] >> 2); + out[2] = (unsigned char ) (((in[2] << 6) & 0xc0) | in[3]); +} + +/* +** decode +** +** decode a base64 encoded stream discarding padding, line breaks and noise +*/ +static +string base64_decode(char const *srcstr, int srclen) { + char in[4], out[4], v; + int i, j, len, morechars; + string decbuf; + int declen = 0; + + /* Initialize. Only the first 3 chars of out will get overwritten. */ + out[3] = 0; + + /* Examine all characters. */ + j = 0; + morechars = 1; + while (morechars) { + for (len = 0, i = 0; i < 4 && morechars; i++) { + v = 0; + + while (morechars && !v) { + v = (unsigned char) srcstr[j++]; + if (j > srclen) + morechars = 0; + v = (unsigned char) ((v < 43 || v > 122) ? + 0 : + xx_cd64[v - 43]); + if (v) + v = (unsigned char) ((v == '$') ? 0 : v - 61); + } + if (morechars) { + len++; + if (v) + in[i] = (unsigned char) (v - 1); + } + else + in[i] = 0; + } + if (len) { + decodeblock (in, out); + decbuf += string(out); + declen += len - 1; + } + } + + /* All done. */ + return (decbuf); +} + +string base64_decode(string const &s) { + return base64_decode(s.c_str(), s.length()); +} diff --git a/xr/sys/sys b/xr/sys/sys @@ -88,6 +88,7 @@ unsigned long mt_rand(void); bool check_acl(string const &ipstr, struct in_addr ipaddr); int sysrun (string const &s); int maxtimeout(int a, int b); +string base64_decode(string const &s); void mutex_lock(void *obj); void mutex_unlock(void *obj); diff --git a/xr/webinterface/answer.cc b/xr/webinterface/answer.cc @@ -106,6 +106,26 @@ void Webinterface::answer(Httpbuffer req) { if (req.requestmethod() != Httpbuffer::m_get) throw Error("Only request method GET supported"); + // If web interface authentication is in effect, then we need the creds + // for all requests. + if (config.webinterface_auth() != "") { + // Incoming auth headers have the format: + // Authorization: Basic BASE64-ENCODED-UN:PW + string given_auth = req.headerval("Authorization:"); + if (given_auth.length() > 6) + given_auth = given_auth.substr(6); + if (base64_decode(given_auth) != config.webinterface_auth()) { + string resp = + "HTTP/1.0 401 Authorization Required\r\n" + "WWW-Authenticate: Basic realm=\"Crossroads Web Interface\"\r\n" + "Content-Length: 0\r\n" + "\r\n"; + Netbuffer buf(resp); + buf.netwrite(cfd, config.client_write_timeout()); + return; + } + } + string uri = req.requesturi(); // Status overview @@ -120,6 +140,17 @@ void Webinterface::answer(Httpbuffer req) { return; } + // /favicon.ico requests (those pesky browsers) + if (uri == "/favicon.ico") { + string resp = + "HTTP/1.0 404 Not Found\r\n" + "Content-Length: 0\r\n" + "\r\n"; + Netbuffer buf(resp); + buf.netwrite(cfd, config.client_write_timeout()); + return; + } + if (uri[0] == '/') uri = uri.substr(1); vector<string> parts = str2parts (uri, '/'); @@ -137,6 +168,15 @@ void Webinterface::answer(Httpbuffer req) { return; } + // /server/webinterfaceauth/ + // /server/webinterfaceauth/USER:PASS + if (parts.size() == 3 && + parts[0] == "server" && parts[1] == "webinterfaceauth") { + config.webinterface_auth(parts[2]); + answer_status(); + return; + } + // /server/maxconnections/ // /server/maxconnections/NUMBER if (parts.size() == 3 && diff --git a/xr/webinterface/answerstatus.cc b/xr/webinterface/answerstatus.cc @@ -26,6 +26,7 @@ void Webinterface::answer_status() { " <dispatchmode>" << config.dispatchmodestr() << "</dispatchmode>\n" " <removereservations>" << config.removereservations() << "</removereservations>\n" " <webinterface>" << config.webinterfaceip() << ':' << config.webinterfaceport() << "</webinterface>\n" + " <webinterfaceauth>" << config.webinterface_auth() << "</webinterfaceauth>\n" " <dnscachetimeout>" << config.dnscachetimeout() << "</dnscachetimeout>\n" " <buffersize>" << config.buffersize() << "</buffersize>\n" " <closesocketsfast>" << config.fastclose() << "</closesocketsfast>\n" diff --git a/xr/webinterface/execute.cc b/xr/webinterface/execute.cc @@ -16,7 +16,7 @@ void Webinterface::execute() { config.webinterfaceport(), "web interface", Servertype::t_tcp); } catch (Error const &e) { - cerr << e.what() << "(webinterface, retrying in a sec)\n"; + cerr << e.what() << " (webinterface, retrying in a sec)\n"; sleep (1); continue; } @@ -37,26 +37,26 @@ void Webinterface::execute() { warnmsg("Web interface: failed to accept network " "connection: " << strerror(errno) << '\n'); } else { - serve (); + serve(); socketclose(cfd); } } } catch (Error const &e) { cerr << e.what() << " (webinterface)\n"; - ostringstream m; - m << - "<h1>Web interface error</h1>\n" - "XR's web interface could not handle your request.<p/>\n" - "<i>" << e.what() << "</i>\n"; - ostringstream o; - o << - "HTTP/1.0 500 Server Error\r\n" - "X-Reason: " << e.what() << "\r\n" - "Content-Length: " << m.str().length() << "\r\n" - "\r\n" << - m.str(); + if (cfd >= 0) { + ostringstream m; + m << "<h1>Web interface error</h1>\n" + "XR's web interface could not handle your request.<p/>\n" + "<i>" << e.what() << "</i>\n"; + ostringstream o; + o << "HTTP/1.0 500 Server Error\r\n" + "X-Reason: " << e.what() << "\r\n" + "Content-Length: " << m.str().length() << "\r\n" + "\r\n" << + m.str(); Netbuffer buf(o.str()); buf.netwrite(cfd, config.client_write_timeout()); + } socketclose(cfd); } } diff --git a/xr/webinterface/serve.cc b/xr/webinterface/serve.cc @@ -1,7 +1,7 @@ #include "webinterface" void Webinterface::serve () { - msg("Webinterface serving request on client fd " << cfd << '\n'); + debugmsg("Webinterface serving request on client fd " << cfd << '\n'); Httpbuffer clientrequest; clientrequest.netread(cfd, config.client_read_timeout()); diff --git a/xrctl/xrctl b/xrctl/xrctl @@ -513,6 +513,7 @@ sub xr_cmdarr { # Handle general flags and boolflags push (@cmd, flag($ss, '--web-interface', 'webinterface', ''), + flag($ss, '--web-interface-auth', 'webinterfaceauth', ''), flag($ss, '--dispatch-mode', 'dispatchmode', $default_dispatchmode), flag($ss, '--max-connections', 'maxconnections',