From 31d56acd65724f6bd8864aa8192160b2a7e8978f Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Wed, 5 Apr 2023 23:17:15 +0200 Subject: [PATCH] Initial commit. --- README.md | 40 ++++++++++++++++ http_to_xmpp.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 35 ++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 README.md create mode 100644 http_to_xmpp.py create mode 100644 pyproject.toml diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce5ae6f --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# HTTP to XMPP gateway + +This is a dumb dumb gateay between HTTP and XMPP: + +You POST some text over HTTP, text gets sent over XMPP. + +Only one XMPP jid can receive message. + + +## Installation + + pip install http-to-xmpp + + +## Setup + +It can be configured either via environment variables: + +```bash +$ export XMPP_DEST_JID=the_human_receiving_messages@the_server.org +$ export XMPP_JID=the_bot_account@the_server.org +$ export XMPP_PASSWORD=the_bot_password +$ http-to-xmpp +``` + +or via arguments, but beware of `ps` showing your password! + +```bash +$ http-to-xmpp --xmpp-jid the_bot_account@the_server.org --xmpp-password the_bot_password --xmpp-dest-jid the_human_receiving_messages@the_server.org +``` + +HTTP host and port to listen to can be changed using `--http-host` and `--http-port`, they default to `localhost:1985`. + + +## Usage + +You just have to send POST requests, to `/` on the given host:port +pair so by default, using curl, one can post messages using: + + $ curl -XPOST -d Coucou localhost:1985 diff --git a/http_to_xmpp.py b/http_to_xmpp.py new file mode 100644 index 0000000..5e1f60c --- /dev/null +++ b/http_to_xmpp.py @@ -0,0 +1,119 @@ +"""HTTP to XMPP gateway.""" + +import argparse +import asyncio +import os +import signal +import socket +import sys + +import aioxmpp +import aioxmpp.dispatcher +from aiohttp import web + +__version__ = "0.1" + + +class XMPPClient: + def __init__(self, jid, password): + self.jid = jid + self.password = password + + async def setup(self): + self.hostname = socket.gethostname() + self.client = aioxmpp.Client( + aioxmpp.JID.fromstr(self.jid), aioxmpp.make_security_layer(self.password) + ) + self.client.start() + + def send(self, msg: str, to: str): + message = aioxmpp.Message( + aioxmpp.MessageType.CHAT, + to=aioxmpp.JID.fromstr(to), + ) + message.body[None] = msg + self.client.enqueue(message) + + +class HTTPServer: + def __init__(self, host, port): + self.host = host + self.port = port + + async def setup(self): + self.app = web.Application() + self.app.add_routes([web.post("/", self.on_post)]) + self.runner = web.AppRunner(self.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, self.host, self.port) + await self.site.start() + + def forward_to(self, to_jid, xmpp_client): + self.to_jid = to_jid + self.xmpp_client = xmpp_client + + async def on_post(self, request): + if not self.xmpp_client: + return + self.xmpp_client.send(await request.text(), self.to_jid) + return web.Response(text="") + + +def parse_args(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--http-host", default="localhost") + parser.add_argument("--http-port", default=1985, type=int) + parser.add_argument( + "--xmpp-jid", + default=os.environ.get("XMPP_JID"), + help="XMPP account of the bot." + "if not given it's read from XMPP_JID environment variable.", + ) + parser.add_argument( + "--xmpp-password", + default=os.environ.get("XMPP_PASSWORD"), + help="XMPP password of the bot, " + "if not given it's read from XMPP_PASSWORD environment variable.", + ) + parser.add_argument( + "--xmpp-dest-jid", + default=os.environ.get("XMPP_DEST_JID"), + help="XMPP account for the human to receive messages." + "if not given it's read from XMPP_DEST_JID environment variable.", + ) + args = parser.parse_args() + for mandatory in "xmpp_password", "xmpp_jid", "xmpp_dest_jid": + if not getattr(args, mandatory): + print( + f"No {mandatory.replace('_', ' ')} found neither via " + f"--{mandatory.replace('_', '-')} neither via " + f"{mandatory.upper()} env var.", + file=sys.stderr, + ) + sys.exit(1) + return args + + +async def amain(): + args = parse_args() + + http_server = HTTPServer(args.http_host, args.http_port) + await http_server.setup() + + xmpp_client = XMPPClient(args.xmpp_jid, args.xmpp_password) + await xmpp_client.setup() + + http_server.forward_to(args.xmpp_dest_jid, xmpp_client) + + event = asyncio.Event() + loop = asyncio.get_event_loop() + loop.add_signal_handler(signal.SIGINT, event.set) + await event.wait() + + +def main(): + asyncio.run(amain()) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2b7c9a2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "http-to-xmpp" +description = "Basic one way gate between HTTP and XMPP." +readme = "README.md" +license = {text = "MIT License"} +authors = [ + {name = "Julien Palard", email = "julien@palard.fr"}, +] +requires-python = ">= 3.7" +dependencies = [ + "aiohttp", + "aioxmpp", +] +dynamic = ["version"] + +[project.urls] +homepage = "https://git.afpy.org/mdk/http-to-xmpp" + +[project.scripts] +http-to-xmpp = "http_to_xmpp:main" + +[tool.setuptools] +py-modules = [ + "http_to_xmpp", +] +include-package-data = false + +[tool.setuptools.dynamic.version] +attr = "http_to_xmpp.__version__" + +[tool.black]