From f15af6fad4511ccafe7e94edde0a0d26264995a1 Mon Sep 17 00:00:00 2001 From: rwx Date: Tue, 27 Jan 2004 00:07:31 +0100 Subject: [PATCH] Initial revision darcs-hash:20040126230731-a3a09-53057b0ad2d5b93317730b13b1ed7621891e9add.gz --- AUTHORS | 4 + GNUmakefile | 78 ++++++++++ Halberd/__init__.py | 28 ++++ Halberd/clientlib.py | 227 +++++++++++++++++++++++++++++ Halberd/cluelib.py | 305 ++++++++++++++++++++++++++++++++++++++ Halberd/reportlib.py | 50 +++++++ Halberd/scanlib.py | 195 +++++++++++++++++++++++++ Halberd/version.py | 15 ++ LICENSE | 340 +++++++++++++++++++++++++++++++++++++++++++ MANIFEST.in | 6 + README | 24 +++ THANKS | 9 ++ scripts/halberd | 96 ++++++++++++ setup.py | 37 +++++ tests/test_clue.py | 40 +++++ tests/test_http.py | 105 +++++++++++++ 16 files changed, 1559 insertions(+) create mode 100644 AUTHORS create mode 100644 GNUmakefile create mode 100644 Halberd/__init__.py create mode 100644 Halberd/clientlib.py create mode 100644 Halberd/cluelib.py create mode 100644 Halberd/reportlib.py create mode 100644 Halberd/scanlib.py create mode 100644 Halberd/version.py create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 THANKS create mode 100755 scripts/halberd create mode 100755 setup.py create mode 100644 tests/test_clue.py create mode 100644 tests/test_http.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..4344b34 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +Authors of halberd. +See also the files THANKS and ChangeLog. + +Juan M. Bello Rivas designed and implemented halberd. diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..30f1b09 --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,78 @@ +# GNUmakefile +# $Id: GNUmakefile,v 1.1 2004/01/26 23:07:31 rwx Exp $ + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +srcdir := . +hlbddir := $(srcdir)/hlbd +docdir := $(srcdir)/doc/api +testdir := $(srcdir)/test + + +PYTHON := /usr/local/bin/python +EPYDOC := /usr/local/bin/epydoc +CTAGS := /usr/local/bin/ctags +SHTOOLIZE := /usr/local/bin/shtoolize +SHTOOL := $(srcdir)/shtool + +versionfile := $(hlbddir)/version.py + +SCRIPTS := halberd.py +MODULES := $(filter-out $(version-file), $(wildcard $(hlbddir)/*.py)) +SOURCES := $(SCRIPTS) $(MODULES) +TEST_SOURCES = $(wildcard $(testdir)/*.py) + + +build: $(SOURCES) + $(PYTHON) setup.py build + +clean: + rm -rf $(srcdir)/build + rm -f *.py[co] $(hlbddir)/*.py[co] $(testdir)/*.py[co] + +dist: distclean incversion doc + $(PYTHON) setup.py sdist + +distclean: clobber clean + rm -f $(srcdir)/{tags,MANIFEST} + rm -rf {$(docdir),$(srcdir)/dist} + +check: $(TEST_SOURCES) + PYTHONPATH=$$PYTHONPATH:$(srcdir)/:; export PYTHONPATH + @for x in $^; do \ + echo "Running "$$x; \ + $(PYTHON) $$x; \ + done + +doc: $(filter-out hlbd/version.py, $(MODULES)) + $(EPYDOC) -o $(docdir) $^ + +tags: $(SOURCES) + $(CTAGS) -R + +clobber: + rm -f $(srcdir)/*~ $(hlbddir)/*~ $(testdir)/*~ + +incversion: shtool + $(SHTOOL) version -l python -n halberd -i l $(versionfile) + +shtool: + $(SHTOOLIZE) -o $@ version + + +.PHONY: clean dist distclean clobber check incversion doc diff --git a/Halberd/__init__.py b/Halberd/__init__.py new file mode 100644 index 0000000..ab825b2 --- /dev/null +++ b/Halberd/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +"""http load balancer detector module (hlbd) +""" + +__revision__ = '$Id: __init__.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + +__all__ = ['version', 'scanlib', 'clientlib', 'cluelib', 'reportlib'] + + +# vim: ts=4 sw=4 et diff --git a/Halberd/clientlib.py b/Halberd/clientlib.py new file mode 100644 index 0000000..0a70c0b --- /dev/null +++ b/Halberd/clientlib.py @@ -0,0 +1,227 @@ +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +"""Minimalistic asynchronous HTTP/HTTPS clients for halberd. + +We need a custom client class because urllib2 and httplib don't really solve +our problems. + +L{HTTPClient} and L{HTTPSClient} connect asynchronously to the specified target +and record a timestamp B{after} the connection is completed. They also impose +a timeout so halberd doesn't wait forever if connected to a rogue HTTP server. +""" + +__revision__ = '$Id: clientlib.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import time +import socket +import urlparse +import asynchat + + +DEFAULT_HTTP_PORT = 80 + +DEFAULT_HDRS = """HEAD / HTTP/1.0\r\n\ +Connection: Keep-Alive\r\n\ +Accept-Encoding: gzip\r\n\ +Accept-Language: en\r\n\ +Accept-Charset: iso-8859-1,*,utf-8\r\n\r\n""" + + +class HTTPException(Exception): + """Unspecified HTTP error. + """ + pass + +class InvalidURL(HTTPException): + """Invalid URL. + """ + pass + +class UnknownProtocol(HTTPException): + """Protocol not supported. + """ + pass + + +class HTTPClient(asynchat.async_chat): + """Minimalistic asynchronous HTTP client. + """ + + def __init__(self, callback, errback, callbackarg=None, hdrs=DEFAULT_HDRS): + """Initializes the HTTPClient object. + """ + asynchat.async_chat.__init__(self) + + # Supported protocols. + self._schemes = ['http'] + + # We store the URL as (scheme, netloc, path, params, query, fragment) + self.__url = () + + # Remote host address, name and port number. + self.__address, self.__host, self.__port = '', '', 0 + + # Local time when the connection to the server is established. + self.__timestamp = 0 + + # String containing the MIME headers returned by the server. + self.__headers = '' + + self.__inbuf, self.__outbuf = '', hdrs + + # Function to call when a (time, headers) tuple is ready. + self.__cb = callback + # Callback argument. + self.__cbarg = callbackarg + # Function to call whenever an exception is captured. + self.__eb = errback + + self.set_terminator('\r\n\r\n') + + def open(self): + """Starts the HTTP transaction. + """ + assert self.__host and self.__port + + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((self.__address, self.__port)) + self.push(self.__outbuf) + + def setURL(self, address, url): + """URL attribute accessor. + + @param address: Target IP address. + @type address: str + @param url: A valid URL. + @type url: str + """ + # XXX Change the query string according to the URL (right now it only + # asks for /). + self.__url = urlparse.urlparse(url) + if self.__url[0] and self.__url[0] not in self._schemes: + raise UnknownProtocol, 'Protocol not supported' + + # Get a valid host and port number + if (self.__url[1].find(':') != -1): + try: + self.__host, port = self.__url[1].split(':') + except ValueError: + raise InvalidURL + + try: + self.__port = int(port) + except ValueError: + raise InvalidURL + else: + self.__address = address + self.__host = self.__url[1] + self.__port = DEFAULT_HTTP_PORT + + return self + + def getURL(self): + """URL attribute accessor. + + @return: Target URL. + @rtype: str + """ + return urlparse.urlunparse(self.__url) + + def getHost(self): + """Host accessor. + + @return: Host name + @rtype: str + """ + return self.__host + + def getPort(self): + """Port accessor. + + @return: Port number. + @rtype: int + """ + return self.__port + + def getTimestamp(self): + """Timestamp accessor. + + @return: Local time at connection establishment. + @rtype: int + """ + return self.__timestamp + + def getHeaders(self): + """Headers accessor. + + @return: Headers replied by the target server. It is the caller's + responsibility to convert this into a proper rfc822.Message. + @rtype: str + """ + return self.__headers + + + # ========================== + # Extensions for async_chat. + # ========================== + + def collect_incoming_data(self, data): + self.__inbuf += str(data) + + def found_terminator(self): + if self.__inbuf.startswith('HTTP/'): + self.__headers = self.__inbuf[self.__inbuf.find('\r\n'):] + self.close() + self.__cb(self.__cbarg, self.__timestamp, self.__headers) + + def handle_connect(self): + pass + + def handle_write(self): + try: + asynchat.async_chat.handle_write(self) + except: + self.__eb(self.__cbarg) + + self.__timestamp = time.time() + + def handle_read(self): + try: + asynchat.async_chat.handle_read(self) + except: + self.__eb(self.__cbarg) + + def handle_close(self): + self.close() + + +class HTTPSClient(HTTPClient): + """Minimalistic asynchronous HTTPS client. + """ + + def __init__(self): + """Initializes the HTTPSClient object. + """ + HTTPClient.__init__(self) + self._schemes.append('https') + + +# vim: ts=4 sw=4 et diff --git a/Halberd/cluelib.py b/Halberd/cluelib.py new file mode 100644 index 0000000..73def14 --- /dev/null +++ b/Halberd/cluelib.py @@ -0,0 +1,305 @@ +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +"""Clue management module. + +This module implements a few classes related to creation and and analysis of +pieces of information returned by a webserver which may help in locating load +balanced devices. +""" + +__revision__ = '$Id: cluelib.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import time +import rfc822 + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +try: + from sha import new as hashfn +except ImportError: + from md5 import new as hashfn + + +DELTA = 1 + + +class Clue: + """A clue is what we use to tell real servers from virtual ones. + + Clues are gathered during several connections to the target and they try to + identify clearly potential patterns in the HTTP responses. + """ + + def __init__(self): + """Initializes the clue object. + """ + + # Number of times this clue has been found. + self.__count = 1 + + # Server information. + self._server = '' + + # Local and remote time in seconds since the Epoch. + self._local, self._remote = 0, 0 + + # Content-Location field. In case the server is misconfigured and + # advertises IP addresses those will be shown here. + self._contloc = '' + + # Fingerprint for the reply. + self._fp = hashfn('') + # We store the headers we're interested in digesting in a string and + # calculate its hash _after_ the header processing takes place. This + # way we incur in less computational overhead. + self.__tmphdrs = '' + + # Cookie. Sometimes it helps pointing out real servers. + self._cookie = '' + + # Original MIME headers. They're useful during analysis and reporting. + self.headers = None + + + def processHdrs(self, headers): + """Extracts all relevant information from the MIME headers replied by + the target. + + @param headers: MIME headers replied by the target. + @type headers: str + """ + + hdrfp = StringIO(headers) + hdrs = rfc822.Message(hdrfp) + hdrs.readheaders() + hdrfp.close() + + self.headers = hdrs # Save a copy of the headers. + + normalize = lambda s: s.replace('-', '_') + + # We examine each MIME field and try to find an appropriate handler. If + # there is none we simply digest the info it provides. + for name, value in hdrs.items(): + try: + handlerfn = getattr(self, '_get_' + normalize(name)) + handlerfn(value) + except AttributeError: + self.__tmphdrs += '%s: %s ' % (name, value) + + self._fp.update(self.__tmphdrs) + + + def incCount(self, num=1): + """Increase the times this clue has been found. + + param num: Number of hits to add. + type num: int + """ + self.__count += num + + def getCount(self): + """Retrieve the number of times the clue has been found + + @return: Number of hits. + @rtype: integer. + """ + return self.__count + + + def setTimestamp(self, timestamp): + """Sets the local clock attribute. + + @param timestamp: The local time (expressed in seconds since the Epoch) + when the connection to the target was successfully completed. + @type timestamp: numeric. + """ + self._local = timestamp + + def calcDiff(self): + """Compute the time difference between the remote and local clocks. + """ + return (int(self._local) - int(self._remote)) + + + # =================== + # Comparison methods. + # =================== + + def __eq__(self, other): + """Rich comparison method implementing == + """ + if self._server != other._server: + return False + + # Important sanity check for the timestamps: + # Time can't (usually) go backwards. + local = (self._local, other._local) + remote = (self._remote, other._remote) + if ((local[0] < local[1]) and (remote[0] > remote[1]) \ + or (local[0] > local[1]) and (remote[0] < remote[1])): + return False + + if self.calcDiff() != other.calcDiff(): + return False + + if self._contloc != other._contloc: + return False + + if self._fp.digest() != other._fp.digest(): + return False + + return True + + def __ne__(self, other): + """Rich comparison method implementing != + """ + return not self == other + + # ========= + # Contains. + # ========= + + def __contains__(self, other): + onediff, otherdiff = self.calcDiff(), other.calcDiff() + + if abs(onediff - otherdiff) > DELTA: + return False + + # Important sanity check for the timestamps: + # Time can't (usually) go backwards. + local = (self._local, other._local) + remote = (self._remote, other._remote) + if ((local[0] < local[1]) and (remote[0] > remote[1]) \ + or (local[0] > local[1]) and (remote[0] < remote[1])): + return False + + if self._contloc != other._contloc: + return False + + if self._fp.digest() != other._fp.digest(): + return False + + return True + + + def __repr__(self): + return "" \ + % (self.calcDiff(), self.__count, self._fp.hexdigest()) + + # ================================================================== + # The following methods extract relevant data from the MIME headers. + # ================================================================== + + def _get_server(self, field): + """Server:""" + self._server = field + + def _get_date(self, field): + """Date:""" + self._remote = time.mktime(rfc822.parsedate(field)) + + def _get_content_location(self, field): + """Content-location.""" + self._contloc = field + self.__tmphdrs += field # Make sure this gets hashed too. + + def _get_set_cookie(self, field): + """Set-cookie:""" + self._cookie = field + + def _get_expires(self, field): + """Expires:""" + pass # By passing we prevent this header from being hashed. + + def _get_age(self, field): + """Age:""" + pass + + +def _comp_clues(one, other): + """Clue comparison for list sorting purposes. + + We take into account fingerprint and time differences. + """ + if one._fp.digest() < other._fp.digest(): + return -1 + elif one._fp.digest() > other._fp.digest(): + return 1 + + if one.calcDiff() < other.calcDiff(): + return -1 + elif one.calcDiff() > other.calcDiff(): + return 1 + + return 0 + +class Analyzer: + """Makes sens of the data gathered during the scanning stage. + """ + + def __init__(self, pending): + """Initializes the analyzer object + """ + assert pending is not None + self.__pending = pending + self.__analyzed = [] + + def analyze(self): + """Processes the list of pending clues checking for duplicated entries. + + Duplicated entries (differing in one second and equal in everything + else) are moved from the list of pending clues to the list of analyzed + ones. + """ + self.__pending.sort(_comp_clues) + + while self.__pending: + cur = self.__pending[0] + if len(self.__pending) >= 2: + next = self.__pending[1] + if cur in next: + self._consolidateClues(cur, next) + continue + + self._moveClue(cur) + + return self.__analyzed + + def _consolidateClues(self, one, two): + """Converts two or three clues into one. + + Note that the first one is the one which survives. + """ + one.incCount(two.getCount()) + self._moveClue(one) + self.__pending.remove(two) + + def _moveClue(self, clue): + """Moves a clue from pending to analyzed. + """ + self.__analyzed.append(clue) + self.__pending.remove(clue) + + +# vim: ts=4 sw=4 et diff --git a/Halberd/reportlib.py b/Halberd/reportlib.py new file mode 100644 index 0000000..7ce4086 --- /dev/null +++ b/Halberd/reportlib.py @@ -0,0 +1,50 @@ +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +"""Scanning engine for halberd. +""" + +__revision__ = '$Id: reportlib.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import sys +import time + + +def report(address, clues, hits, outfile=''): + """Displays detailed report information to the user. + """ + out = (outfile and open(outfile, 'w')) or sys.stdout + + out.write('\nfound %d possibly real server(s) at %s.\n' + % (len(clues), address)) + for num, clue in enumerate(clues): + out.write('\nserver %d: %s\n' % (num, clue._server)) + out.write(' received %.2f%% of the traffic\n' \ + % (clue.getCount() * 100 / float(hits))) + out.write(' time information:\n') + out.write(' remote clock: %s\n' % clue.headers['date']) + out.write(' difference: %d seconds\n' % (clue.calcDiff() - 3600)) + if clue._contloc: + out.write(' content-location: %s\n' % clue._contloc) + if clue._cookie: + out.write(' cookie: %s\n' % clue._cookie) + out.write(' header fingerprint: %s\n' % clue._fp.hexdigest()) + + +# vim: ts=4 sw=4 et diff --git a/Halberd/scanlib.py b/Halberd/scanlib.py new file mode 100644 index 0000000..300378b --- /dev/null +++ b/Halberd/scanlib.py @@ -0,0 +1,195 @@ +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +"""Scanning engine for halberd. +""" + +__revision__ = '$Id: scanlib.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import sys +import time +import asyncore + +import hlbd.cluelib as cluelib +import hlbd.clientlib as clientlib + + +__all__ = ["Scanner"] + + +class _State: + """Holds the state of the scanner at the current point in time. + """ + def __init__(self, sockets, verbose): + self.clues = [] + self.sockets = sockets + self.verbose = verbose + + self.replies = 0 + self.errorfound = False + self.cluesperreply = 0 + + self.address = '' + + def update(self, remaining): + """Updates certain statistics while the scan is happening. + """ + if len(self.clues) > 0: + self.cluesperreply = len(self.clues) / float(self.replies) + if self.cluesperreply >= 0.8: + # 80% or more of the replies create new clues... This means + # there's something wrong with the headers. + pass + + if self.verbose: + self._show(remaining) + + def _show(self, remaining): + """Displays progress information. + """ + sys.stdout.write('\r%3d seconds left, %3d clue(s) so far (out of ' \ + '%4d replies)' % (remaining, len(self.clues), self.replies)) + sys.stdout.flush() + + +# =============================== +# Callbacks passed to HTTPClient. +# =============================== + +def _get_clues_cb(state, timestamp, headers): + """Transforms timestamp-header pairs into clues. + """ + state.replies += 1 + + clue = cluelib.Clue() + clue.setTimestamp(timestamp) + clue.processHdrs(headers) + + try: + i = state.clues.index(clue) + state.clues[i].incCount() + except ValueError: + state.clues.append(clue) + +def _error_cb(state): + """Handles exceptions in a (somewhat) graceful way. + """ + state.errorfound = True + + if sys.exc_type is KeyboardInterrupt: + pass + else: + sys.stderr.write('Caught exception: ' + `sys.exc_type` + '\n') + + +class Scanner: + """Load-balancer scanner. + """ + + def __init__(self, scantime, sockets, verbose): + """Initializes scanner object. + + @param scantime: Time (in seconds) to spend peforming the analysis. + @type scantime: int + @param sockets: Number of sockets to use in parallel to probe the target. + @type sockets: int + @param verbose: Specifies whether progress information should be printed or + not. + @type verbose: bool + """ + self.__scantime = scantime + self.__state = _State(sockets, verbose) + self.__clients = self._setupClientPool() + + def _setupClientPool(self): + """Initializes the HTTP client pool before the scan starts. + """ + clients = [] + + if self.__state.verbose: + print 'setting up client pool...', + + for client in xrange(self.__state.sockets): + client = clientlib.HTTPClient(_get_clues_cb, _error_cb, + self.__state) + clients.append(client) + + if self.__state.verbose: + print 'done.', + + return clients + + def _getAddress(self, url): + """Extract an IP address to scan. + """ + import urlparse + import socket + + netloc = urlparse.urlparse(url)[1] + hostname = netloc.split(':')[0] + name, aliases, addresses = socket.gethostbyname_ex(hostname) + + if len(addresses) > 1: + sys.stdout.write('\nwarning: the server at %s resolves to the ' + 'following addresses:\n' % hostname) + sys.stdout.write(' %s <-- using this one.\n' % addresses[0]) + for address in addresses[1:]: + sys.stdout.write(' %s\n' % address) + + return addresses[0] + + def scan(self, url): + """Looks for load balanced servers. + + @param url: URL to scan. + @type url: str + @return: list of clues found and number of replies received from the + target. + @rtype: tuple + """ + remaining = lambda end: int(end - time.time()) + hasexpired = lambda end: (remaining(end) <= 0) + + state = self.__state + state.address = self._getAddress(url) + + # Start with the scanning loop + state.round = 0 + stop = time.time() + self.__scantime # Expiration time for the scan. + while 1: + for client in self.__clients: + client.setURL(state.address, url).open() + if state.errorfound: + break + + # Check if the timer expired. + if hasexpired(stop) or state.errorfound: + break + + asyncore.loop(remaining(stop)) + + state.update(remaining(stop)) + + if state.verbose: + print + + return state.address, state.clues, state.replies + + +# vim: ts=4 sw=4 et diff --git a/Halberd/version.py b/Halberd/version.py new file mode 100644 index 0000000..9d6e819 --- /dev/null +++ b/Halberd/version.py @@ -0,0 +1,15 @@ +## +## ./hlbd/version.py -- Version Information for halberd (syntax: Python) +## [automatically generated and maintained by GNU shtool] +## + +class version: + v_hex = 0x001201 + v_short = "0.1.1" + v_long = "0.1.1 (26-Jan-2004)" + v_tex = "This is halberd, Version 0.1.1 (26-Jan-2004)" + v_gnu = "halberd 0.1.1 (26-Jan-2004)" + v_web = "halberd/0.1.1" + v_sccs = "@(#)halberd 0.1.1 (26-Jan-2004)" + v_rcs = "$Id: version.py,v 1.1 2004/01/26 23:07:31 rwx Exp $" + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b8cae99 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include setup.py +include halberd.py +include AUTHORS LICENSE README THANKS +include hlbd/*.py +include test/test_*.py +recursive-include doc * diff --git a/README b/README new file mode 100644 index 0000000..e91b554 --- /dev/null +++ b/README @@ -0,0 +1,24 @@ +_h_a_l_b_e_r_d -- HTTP load balancer detection tool +================================================================================ + +"The halberd, a weapon based on the spear, was developed by combining the +merits of the spear and dagger-axe in the Yin and Shang period (1600-1100 +B.C.). It can be used to hook-cut, peck and pierce the opponent making it a +more powerful weapon than the dagger-axe and spear." + Essentials of Chinese Wushu by Wu Bin, Li Xingdong and Yu Gongbao + +Overview +-------------------------------------------------------------------------------- + + Halberd is a tool aimed at discovering web server load balancing +devices. + +Installation +-------------------------------------------------------------------------------- + + Installing halberd is a very simple task. It suffices to write:: + + # python setup.py install + + There are no special requirements other than having Python version 2.3 +or above. diff --git a/THANKS b/THANKS new file mode 100644 index 0000000..03d400d --- /dev/null +++ b/THANKS @@ -0,0 +1,9 @@ +halberd THANKS file + +halberd has originally been written by Juan M. Bello Rivas. +Many people have further contributed to halberd by reporting problems, +suggesting various improvements, or submitting actual code. Here is +a list of these people. Help me keep it complete and exempt of errors. + +Daniel SolĂ­s Agea +Dethy diff --git a/scripts/halberd b/scripts/halberd new file mode 100755 index 0000000..b26e464 --- /dev/null +++ b/scripts/halberd @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +"""Command line interface and main driving module. +""" + +__revision__ = '$Id: halberd,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import sys + + +# Time to spend probing the target expressed in seconds. +_DEFAULT_SCANTIME = 30 +# Number of sockets to create at each scanning round. +_DEFAULT_SOCKETS = 10 + + +def main(argv): + """http load balancer detector's main routine. + """ + import optparse + from hlbd import version + + parser = optparse.OptionParser(usage='%prog [options] url', + version=version.version.v_gnu) + parser.add_option('-t', '--time', + action='store', type='int', dest='scantime', + help='time (in seconds) to spend analyzing the target', + metavar='NUM', default=_DEFAULT_SCANTIME) + parser.add_option('-s', '--sockets', + action='store', type='int', dest='sockets', + help='number of sockets to use in parallel', + metavar='NUM', default=_DEFAULT_SOCKETS) + parser.add_option('-v', '--verbose', action='store_true', dest='verbose', + help='explain what is being done', default=False) + parser.add_option('-o', '--output', action='store', dest='output', + help='write output to the specified file', + metavar='FILE', default='') + group = optparse.OptionGroup(parser, 'Example', + 'halberd.py -vt 10 -s 5 http://example.net # would scan ' + 'example.net during 10 seconds using 5 sockets in parallel ' + 'while printing progress information.') + parser.add_option_group(group) + + (opts, args) = parser.parse_args(argv) + + if len(args) < 1: + # We must have an URL to scan. + parser.error('incorrect number of arguments') + + if opts.verbose: + print '%s\nhttp load balancer detector\n' % version.version.v_gnu + + # Ensures the URL is a valid one. + make_url = lambda url, base: (url.startswith(base) and url) or (base + url) + + # Go through the reconnaissance stage. + from hlbd import scanlib + + address, clues, hits = ('', [], 0) + scanner = scanlib.Scanner(opts.scantime, opts.sockets, opts.verbose) + try: + address, clues, hits = scanner.scan(make_url(args[0], 'http://')) + except KeyboardInterrupt: + print '\n*** interrupted by the user ***' + + if len(clues) > 0: + # Analyze results and print report. + from hlbd import cluelib, reportlib + + analyzer = cluelib.Analyzer(clues) + reportlib.report(address, analyzer.analyze(), hits, opts.output) + +if __name__ == '__main__': + main(sys.argv[1:]) + + +# vim: ts=4 sw=4 et diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..b7353d0 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from distutils.core import setup + +from hlbd.version import version + + +setup(name = 'halberd', version = version.v_short, + description = 'HTTP load balancer detector', + author = 'Juan M. Bello Rivas', + author_email = 'rwx+halberd@synnergy.net', + url = 'http://www.synnergy.net/~rwx/halberd', + license = 'GNU GENERAL PUBLIC LICENSE', + package_dir = {'hlbd': 'hlbd'}, + scripts = ['halberd.py'], +) + + +# vim: ts=4 sw=4 et diff --git a/tests/test_clue.py b/tests/test_clue.py new file mode 100644 index 0000000..b728bdb --- /dev/null +++ b/tests/test_clue.py @@ -0,0 +1,40 @@ +#-*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +__revision__ = '$Id: test_clue.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import unittest +from hlbd.Clue import * + + +class TestClue(unittest.TestCase): + + def setUp(self): + self.clue = Clue() + + def testCount(self): + self.assertEquals(self.clue.getCount(), 1) + self.clue.incCount() + self.assertEquals(self.clue.getCount(), 2) + +if __name__ == '__main__': + unittest.main() + + +# vim: ts=4 sw=4 et diff --git a/tests/test_http.py b/tests/test_http.py new file mode 100644 index 0000000..338da19 --- /dev/null +++ b/tests/test_http.py @@ -0,0 +1,105 @@ +#-*- coding: iso-8859-1 -*- + +# Copyright (C) 2004 Juan M. Bello Rivas +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +__revision__ = '$Id: test_http.py,v 1.1 2004/01/26 23:07:31 rwx Exp $' + + +import time +import unittest +import asyncore +from test.test_support import run_unittest + +from hlbd.http import * + + +class TestHTTPClient(unittest.TestCase): + + def setUp(self): + none = lambda: None + self.client = HTTPClient(none, none) + + def tearDown(self): + pass + + def testURLAccessors(self): + url = 'www.example.net/test?param=fnord' + + self.assertRaises(UnknownProtocol, self.client.setURL, + 'gopher://example.net') + + self.client.setURL(url) + self.assertEquals(url, self.client.getURL()) + self.client.setURL('http' + url) + self.assertEquals('http' + url, self.client.getURL()) + + def testHostAndPort(self): + self.client.setURL('www.example.com') + self.failUnless(self.client.getPort() == 80) + + self.client.setURL('http://www.example.net:8080') + self.failUnless(self.client.getPort() == 8080) + + self.assertRaises(InvalidURL, self.client.setURL, + 'http://www.example.net:abc') + + self.assertRaises(InvalidURL, self.client.setURL, + 'http://www.example.org:777:8080') + + def testConnect(self): + url = 'http://agartha:8080' + + self.failUnless(self.client.setURL(url) == self.client) + t = time.time() + self.client.open() + asyncore.loop() + + self.failUnless(int(self.client.getTimestamp()) == int(t)) + #print self.client.getHeaders() + +# def testMultipleConnections(self): +# url = 'http://www.telefonica.net:80' +# +# clients = [] +# for x in xrange(10): +# client = HTTPClient() +# client.setURL(url).open() +# clients.append(client) +# +# asyncore.loop() +# +# count = 1 +# endtime = time.time() + 10 +# for timestamp, headers in collect_tuples(clients): +# if time.time() > endtime: +# break +# +# print timestamp +# count += 1 +# +# print count +# +#def collect_tuples(clients): +# for client in clients: +# yield (client.getTimestamp(), client.getHeaders()) + + +if __name__ == '__main__': + unittest.main() + + +# vim: ts=4 sw=4 et