From d02d7eabb65a60d581287827617cfffa6cc050f5 Mon Sep 17 00:00:00 2001 From: Daniel Kraft Date: Mon, 13 Jun 2022 10:15:12 +0200 Subject: [PATCH] Support multiple servers in ejabberd auth. This is an update to the xidauth.py ejabberd authentication script, such that is can support multiple server names and each with its own XID application name and endpoint. With this, it is possible to support multiple domains on ejabberd, e.g. associated to XID on different blockchains / networks. --- ejabberd/README.md | 8 -- ejabberd/docker/xidauth.sh | 6 +- ejabberd/tests/integration.py | 9 +- ejabberd/tests/namecoding.py | 5 +- ejabberd/xidauth.py | 153 +++++++++++++++++++++------------- 5 files changed, 102 insertions(+), 79 deletions(-) diff --git a/ejabberd/README.md b/ejabberd/README.md index 49b3da8..001c685 100644 --- a/ejabberd/README.md +++ b/ejabberd/README.md @@ -58,11 +58,3 @@ Then, the following configuration options should be set in `ejabberd.yml`: auth_method: external extauth_program: "/etc/ejabberd/xidauth.py ...arguments..." - -**Note: When Xid is running its JSON-RPC server over HTTP, it -[allows connections to the specified port -*from everywhere*](https://github.com/xaya/libxayagame/issues/41). -In particular, this -allows anyone to e.g. shut down the server. Thus, for Xid running on a -public server, it is important to block the RPC port from incoming -external connections through a suitable firewall (e.g. `iptables`)!** diff --git a/ejabberd/docker/xidauth.sh b/ejabberd/docker/xidauth.sh index 6b105fb..ceb0d2b 100755 --- a/ejabberd/docker/xidauth.sh +++ b/ejabberd/docker/xidauth.sh @@ -1,6 +1,6 @@ #!/bin/sh -e -# Copyright (C) 2020 The Xaya developers +# Copyright (C) 2020-2022 The Xaya developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -8,7 +8,5 @@ # variables from the Docker context to set the arguments. exec ${HOME}/bin/xidauth.py \ - --xid_rpc_url "${XID_RPC_URL}" \ - --application "${XID_APPLICATION}" \ - --servername "${XMPP_DOMAIN}" \ + --servers ${XIDAUTH_SERVERS} \ --logfile "${HOME}/logs/xidauth.log" diff --git a/ejabberd/tests/integration.py b/ejabberd/tests/integration.py index 71fc62d..844aedf 100755 --- a/ejabberd/tests/integration.py +++ b/ejabberd/tests/integration.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # coding=utf8 -# Copyright (C) 2019-2020 The Xaya developers +# Copyright (C) 2019-2022 The Xaya developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -123,7 +123,7 @@ def run (self): # We need an instance of EjabberdXidAuth for decoding names. Create it # and then only use it for that purpose. (This is not the instance # that will be used in the real test.) - self.auth = EjabberdXidAuth ({}, "http://localhost", None) + self.auth = EjabberdXidAuth ({}, None) self.auth.log = logging.getLogger ("xidauth") # Define some test names (of various types) and set up signer addresses @@ -173,9 +173,8 @@ def run (self): srcdir = "." binary = os.path.join (srcdir, "xidauth.py") cmd = [binary] - cmd.append ("--xid_rpc_url=http://localhost:%s" % self.gamenode.port) - cmd.append ("--servername=%s" % self.server) - cmd.append ("--application=%s" % self.app) + rpcUrl = "http://localhost:%s" % self.gamenode.port + cmd.extend (["--servers", "%s,%s,%s" % (self.server, self.app, rpcUrl)]) cmd.append ("--logfile=%s" % os.path.join (self.basedir, "xidauth.log")) try: self.log.info ("Starting process: %s" % " ".join (cmd)) diff --git a/ejabberd/tests/namecoding.py b/ejabberd/tests/namecoding.py index 7cd77f5..7147c6c 100755 --- a/ejabberd/tests/namecoding.py +++ b/ejabberd/tests/namecoding.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # coding=utf-8 -# Copyright (C) 2019-2020 The Xaya developers +# Copyright (C) 2019-2022 The Xaya developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -15,8 +15,7 @@ class NameCodingTest (unittest.TestCase): def setUp (self): - self.auth = EjabberdXidAuth ({}, "http://localhost", - logging.StreamHandler (sys.stderr)) + self.auth = EjabberdXidAuth ({}, logging.StreamHandler (sys.stderr)) def testSimpleNames (self): simpleNames = ["domob", "0", "foo42bar", "xxx"] diff --git a/ejabberd/xidauth.py b/ejabberd/xidauth.py index ad892e7..7b89eef 100755 --- a/ejabberd/xidauth.py +++ b/ejabberd/xidauth.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (C) 2019-2020 The Xaya developers +# Copyright (C) 2019-2022 The Xaya developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -20,10 +20,81 @@ HEX_CHARS = string.digits + "abcdef" -class EjabberdXidAuth (object): +class EjabberdServer: + """ + The authentication service for a single XMPP server, which is + tied to a particular Xid application string and also + JSON-RPC endpoint (e.g. Xid on a particular network). + """ + + def __init__ (self, application, xidRpcUrl): + self.application = application + self.xidRpc = jsonrpclib.ServerProxy (xidRpcUrl) + self.chain = self.xidRpc.getnullstate ()["chain"] + + # The log is filled in when the service gets added to an instance + # of EjabberdXidAuth. + self.log = None + + def unwrapGameState (self, data): + """ + Verifies that the game-state JSON in data represents an up-to-date + state (if not, an exception is raised). Then unwraps the contained + actual state from the "data" field and returns that. + """ + + if data["state"] != "up-to-date": + self.log.critical ("xid is not up-to-date: %s" % data["state"]) + raise RuntimeError ("xid is not up-to-date") + + return data["data"] + + def isUser (self, xayaName): + """ + Checks if the given username (already decoded to Xaya) is + a registered user of Xid on this service. + """ + + data = self.xidRpc.getnamestate (name=xayaName) + state = self.unwrapGameState (data) + + for entry in state["signers"]: + if len (entry["addresses"]) == 0: + continue + + if not "application" in entry: + self.log.debug ("Found global signer key for %s" % xayaName) + return True + + if entry["application"] == self.application: + self.log.debug ("Found signer key for %s and application %s" + % (xayaName, app)) + return True + + self.log.debug ("No valid signer keys for %s and application %s" + % (xayaName, self.application)) + return False + + def authenticate (self, xayaName, pwd): + """ + Checks if the given user (as decoded Xaya name) can be authenticated + with the given password on this Xid service. + """ + + data = self.xidRpc.verifyauth (name=xayaName, application=self.application, + password=pwd) + state = self.unwrapGameState (data) + + self.log.debug ("Authentication state from xid: %s" % state["state"]) + return state["valid"] + + +class EjabberdXidAuth: """ The main class for an external authentication script for ejabberd that - uses xid to authenticate users on the XMPP server. + uses xid to authenticate users on the XMPP server. It has a mapping of + potentially multiple XMPP server names / domains to the associated + Xid authentication services, e.g. for different networks. Note that XMPP has certain restrictions on the usernames; in particular, names are treated in a case-insensitive way (other than Xaya). Thus, @@ -32,13 +103,14 @@ class EjabberdXidAuth (object): UTF-8. """ - def __init__ (self, serverNames, xidRpcUrl, logHandler): - self.serverNames = serverNames - self.xidRpc = jsonrpclib.ServerProxy (xidRpcUrl) - + def __init__ (self, services, logHandler): if logHandler is not None: self.setupLogging (logHandler) + self.serverNames = services + for s in self.serverNames.values (): + s.log = self.log + def setupLogging (self, handler): logFmt = "%(asctime)s %(name)s (%(levelname)s): %(message)s" handler.setFormatter (logging.Formatter (logFmt)) @@ -123,19 +195,6 @@ def decodeXmppName (self, name): self.log.warning ("Simple name was hex-encoded: %s" % name) return None - def unwrapGameState (self, data): - """ - Verifies that the game-state JSON in data represents an up-to-date - state (if not, an exception is raised). Then unwraps the contained - actual state from the "data" field and returns that. - """ - - if data["state"] != "up-to-date": - self.log.critical ("xid is not up-to-date: %s" % data["state"]) - raise RuntimeError ("xid is not up-to-date") - - return data["data"] - def isUser (self, name, server): """ Checks whether the given XMPP name is a valid user for the given @@ -146,31 +205,12 @@ def isUser (self, name, server): if server not in self.serverNames: self.log.warning ("Server %s is not configured for xidauth" % server) return False - app = self.serverNames[server] xayaName = self.decodeXmppName (name) if xayaName is None: return False - data = self.xidRpc.getnamestate (name=xayaName) - state = self.unwrapGameState (data) - - for entry in state["signers"]: - if len (entry["addresses"]) == 0: - continue - - if not "application" in entry: - self.log.debug ("Found global signer key for %s" % xayaName) - return True - - if entry["application"] == app: - self.log.debug ("Found signer key for %s and application %s" - % (xayaName, app)) - return True - - self.log.debug ("No valid signer keys for %s and application %s" - % (xayaName, app)) - return False + return self.serverNames[server].isUser (xayaName) def authenticate (self, name, server, pwd): """ @@ -181,17 +221,12 @@ def authenticate (self, name, server, pwd): if server not in self.serverNames: self.log.warning ("Server %s is not configured for xidauth" % server) return False - app = self.serverNames[server] xayaName = self.decodeXmppName (name) if xayaName is None: return False - data = self.xidRpc.verifyauth (name=xayaName, application=app, password=pwd) - state = self.unwrapGameState (data) - - self.log.debug ("Authentication state from xid: %s" % state["state"]) - return state["valid"] + return self.serverNames[server].authenticate (xayaName, pwd) def run (self, inp, outp): """ @@ -201,13 +236,10 @@ def run (self, inp, outp): """ servers = "" - for s, a in self.serverNames.items (): - servers += "\n %s: %s" % (s, a) + for n, s in self.serverNames.items (): + servers += "\n %s: %s (%s)" % (n, s.application, s.chain) self.log.info ("Running xid authentication script for servers:" + servers) - data = self.xidRpc.getnamestate (name="xaya") - self.log.info ("Xid is %s at height %d" % (data["state"], data["height"])) - self.log.info ("Starting main loop...") while True: cmd = self.readCommand (inp) @@ -230,16 +262,19 @@ def run (self, inp, outp): if __name__ == "__main__": desc = "ejabberd extauth script for authentication with xid" parser = argparse.ArgumentParser (description=desc) - parser.add_argument ("--xid_rpc_url", required=True, - help="JSON-RPC URL for xid's RPC interface") - parser.add_argument ("--servername", required=True, - help="name of the XMPP server") - parser.add_argument ("--application", required=True, - help="application name for xid") + parser.add_argument ("--servers", required=True, nargs="+", + help="Server names to handle as server,application,rpcurl triplets") parser.add_argument ("--logfile", default="/var/log/ejabberd/xidauth.log", help="filename for writing logs to") args = parser.parse_args () - auth = EjabberdXidAuth ({args.servername: args.application}, args.xid_rpc_url, - logging.FileHandler (args.logfile)) + services = {} + for s in args.servers: + parts = s.split (",") + if len (parts) != 3: + sys.exit ("Invalid server triplet: %s" % s) + [srv, app, rpcUrl] = parts + services[srv] = EjabberdServer (app, rpcUrl) + + auth = EjabberdXidAuth (services, logging.FileHandler (args.logfile)) auth.run (sys.stdin.buffer, sys.stdout.buffer)