Compare commits
No commits in common. "master" and "master" have entirely different histories.
5 changed files with 115 additions and 213 deletions
|
@ -1,34 +0,0 @@
|
|||
# .editorconfig
|
||||
# EditorConfig settings for this code base.
|
||||
# See documentation at <URL:http://editorconfig.org/>.
|
||||
|
||||
# Is this the top-most EditorConfig config file in the code base?
|
||||
root = true
|
||||
|
||||
# Match all file names, unless more specific match later.
|
||||
[*]
|
||||
|
||||
# Text encoding name.
|
||||
charset = utf-8
|
||||
|
||||
# Remove trailing whitespace on lines?
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# End-of-line style (“lf”, “cr”, “crlf”).
|
||||
end_of_line = lf
|
||||
|
||||
# Ensure file ends with a line break?
|
||||
insert_final_newline = true
|
||||
|
||||
# Character to use for indentation (“tab“ for U+0009, “space“ for U+0020).
|
||||
indent_style = space
|
||||
|
||||
# Number of columns for each indentation level.
|
||||
indent_size = 4
|
||||
|
||||
|
||||
# Local variables:
|
||||
# coding: utf-8
|
||||
# mode: conf
|
||||
# End:
|
||||
# vim: fileencoding=utf-8 filetype=dosini :
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1 @@
|
|||
.vscode/*
|
||||
.venv
|
||||
log/*
|
||||
auth-mastodon.ini
|
||||
|
|
22
README.md
22
README.md
|
@ -3,19 +3,16 @@ This is a Python script designed to run as an [ejabberd external auth](https://d
|
|||
|
||||
The code is derived from [ejabberd-auth-mysql](https://github.com/rankenstein/ejabberd-auth-mysql) and is licensed under the GNU GPLv3.
|
||||
|
||||
It is used on [Chinwag Social](https://social.chinwag.org) to provide XMPP messaging functionality to every account.
|
||||
It is used on [Chinwag Social](https://social.chinwag.org) to provide XMPP messaging functionality to every account.
|
||||
|
||||
Discussion, questions and/or just saying hi in the [Chinwag Social Beergarden](xmpp:beergarden@rooms.chinwag.org?join) MUC is welcomed!
|
||||
Discussion, questions and/or just saying hi in the [Chinwag Social Beergarden](xmpp:beergarden@rooms.chinwag.org?join) MUC (beergarden@rooms.chinwag.org) is welcomed!
|
||||
|
||||
## Setup
|
||||
Edit the `auth-mastodon.ini.default` file and add database connection credentials, and a location for the log files if desired. Rename it and place it somewhere the ejabberd user can read it.
|
||||
Edit the `auth-mastodon.py` file and add database connection credentials at the top. Moving this to a config file is next on the TODO list, as this makes updates an awful process. Sorry.
|
||||
|
||||
I recommend not using your main Mastodon database user account for this, and instead granting SELECT privileges on the Mastodon *accounts* and *users* tables to your ejabberd user instead. The code here does not attempt any modification to the Mastodon tables at any point, so there's no reason to give it more than read-only rights.
|
||||
I recommend not using your main Mastodon database user account for this, and instead granting SELECT privileges on the Mastodon *accounts* and *users* tables to your ejabberd user instead.
|
||||
|
||||
The default locaction for the ini file is `/etc/ejabberd/auth-mastodon.ini` and can be changed with a command line option if desired:
|
||||
```bash
|
||||
$ auth-mastodon.py -c /usr/local/etc/auth-mastodon.ini
|
||||
```
|
||||
The code here does not attempt any modification to the Mastodon tables at any point, so there's no reason to give it more than read-only rights.
|
||||
|
||||
Then configure ejabberd to use `auth-mastodon.py` as an external authentication provider, as described in the [ejabberd docs](https://docs.ejabberd.im/admin/configuration/#external-script):
|
||||
|
||||
|
@ -23,10 +20,9 @@ Then configure ejabberd to use `auth-mastodon.py` as an external authentication
|
|||
auth_method: external
|
||||
extauth_program: "/path/to/auth-mastodon.py"
|
||||
```
|
||||
Startup and shutdown is handled by the ejabberd process, there's no need to handle this separately via systemd or similar. If the process is killed, ejabberd will restart it.
|
||||
|
||||
## To Do
|
||||
1. Verify domain part of request somehow. Maybe define a canonical domain to be used in config? Does this gain us anything at all?
|
||||
2. Better error handling. Would be good to be more descriptive in the logs, perhaps.
|
||||
3. Setup documentation is very brief, maybe include how to grant minimal permissions via pgsql.
|
||||
4. Reconnection handling, exiting if a query fails would be a simple way to reload if a DB upgrade occurs but we should check if we're in a restart loop or something.
|
||||
1. Move all database and config elements to a simple file to be stored in /etc/ejabberd or similar
|
||||
2. Verify domain part of request somehow. Maybe define a canonical domain to be used in config? Does this gain us anything at all?
|
||||
3. Better error handling. Would be good to be more descriptive in the logs, perhaps.
|
||||
4. Setup documentation is very brief, maybe include how to grant minimal permissions via pgsql.
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
[database]
|
||||
db_host = localhost
|
||||
db_port = 5432
|
||||
db_user = ejabberd
|
||||
db_pass =
|
||||
db_name = mastodon
|
||||
|
||||
[log]
|
||||
log_dir = /var/log/ejabberd/
|
260
auth-mastodon.py
260
auth-mastodon.py
|
@ -1,213 +1,165 @@
|
|||
#!/usr/bin/env python3
|
||||
import os, sys, logging, struct, psycopg2, bcrypt, random, atexit, time
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
# Database connection details. The credentials here need access to the Mastodon database, which "ejabberd" is unlikely
|
||||
# to have on your system by default. You shoud grant SELECT privileges to ejabberd on the "accounts" and "users" tables,
|
||||
# to play it safe, or include the Mastodon DB user credentials here (don't).
|
||||
db_host="localhost"
|
||||
db_port=5432
|
||||
db_user="ejabberd"
|
||||
db_pass=""
|
||||
db_name="mastodon"
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import bcrypt
|
||||
import psycopg2
|
||||
|
||||
# Get config file location from command line, or default to /etc/ejabberd
|
||||
config_args = argparse.ArgumentParser()
|
||||
config_args.add_argument("-c", "--config_file", help="Config file location", type=str, default="/etc/ejabberd/auth-mastodon.ini")
|
||||
args = config_args.parse_args()
|
||||
|
||||
# Load database config from file, this is currently the only configurable item
|
||||
config_ini = configparser.ConfigParser()
|
||||
config_ini.read(args.config_file)
|
||||
|
||||
# Database connection details. The credentials here need access to the
|
||||
# Mastodon database, which "ejabberd" is unlikely to have on your system
|
||||
# by default. You shoud grant SELECT privileges to ejabberd on the
|
||||
# "accounts" and "users" tables, to play it safe, or include the
|
||||
# Mastodon DB user credentials here (don't).
|
||||
db_host = config_ini.get('database', 'db_host', fallback='localhost')
|
||||
db_port = config_ini.get('database', 'db_port', fallback=5432)
|
||||
db_user = config_ini.get('database', 'db_user', fallback='ejabberd')
|
||||
db_pass = config_ini.get('database', 'db_pass', fallback='')
|
||||
db_name = config_ini.get('database', 'db_name', fallback='mastodon_production')
|
||||
|
||||
# This is the query that pulls the password hash for the given user.
|
||||
# Mastodon doesn't store the domain for local accounts in the database,
|
||||
# so we ignore the host component and try to match username where the
|
||||
# domain is NULL.
|
||||
db_query_getpass = """
|
||||
SELECT users.encrypted_password AS password
|
||||
FROM accounts
|
||||
INNER JOIN users
|
||||
ON accounts.id = users.account_id
|
||||
WHERE
|
||||
lower(accounts.username) = %(user)s
|
||||
AND accounts.domain IS NULL
|
||||
"""
|
||||
# This is the query that pulls the password hash for the given user. Mastodon doesn't store the domain for local accounts in
|
||||
# the database, so we ignore the host component and try to match username where the domain is NULL.
|
||||
db_query_getpass="select users.encrypted_password as password from accounts inner join users on accounts.id=users.account_id where lower(accounts.username) = %(user)s and accounts.domain is null"
|
||||
|
||||
########################################################################
|
||||
# Setup
|
||||
#Setup
|
||||
########################################################################
|
||||
|
||||
log_dir = config_ini.get('log', 'log_dir', fallback='/var/log/ejabberd')
|
||||
sys.stderr = open('%s/extauth_err.log' % log_dir, 'a')
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(message)s',
|
||||
filename='%s/extauth.log' % log_dir,
|
||||
filemode='a',
|
||||
)
|
||||
sys.stderr = open('/var/log/ejabberd/extauth_err.log', 'a')
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s %(message)s',
|
||||
filename='/var/log/ejabberd/extauth.log',
|
||||
filemode='a')
|
||||
|
||||
try:
|
||||
# Connect to the DB, set autocommit and readonly otherwise
|
||||
# postgresql seems to have a tendency to keep things "idle in
|
||||
# transaction" and table locks eventually grind Mastodon to a halt.
|
||||
# We don't make any changes anyway.
|
||||
database = psycopg2.connect(
|
||||
host=db_host,
|
||||
user=db_user,
|
||||
password=db_pass,
|
||||
database=db_name,
|
||||
port=db_port,
|
||||
)
|
||||
database.set_session(readonly=True, autocommit=True)
|
||||
logging.debug(database.get_dsn_parameters())
|
||||
except Exception:
|
||||
logging.error("Unable to connect to %s as %s, check configuration" % (db_name, db_user))
|
||||
time.sleep(10)
|
||||
sys.exit(1)
|
||||
|
||||
# Connect to the DB, set autocommit and readonly otherwise postgresql seems to have a
|
||||
# tendency to keep things "idle in transaction" and table locks eventually grind
|
||||
# Mastodon to a halt. We don't make any changes anyway.
|
||||
database=psycopg2.connect(host = db_host, user = db_user, password = db_pass, database = db_name, port = db_port)
|
||||
database.set_session(readonly=True, autocommit=True)
|
||||
logging.debug(database.get_dsn_parameters())
|
||||
except:
|
||||
logging.error("Unable to initialize database, check settings!")
|
||||
time.sleep(10)
|
||||
sys.exit(1)
|
||||
|
||||
@atexit.register
|
||||
def close_db():
|
||||
database.close()
|
||||
|
||||
cursor.close()
|
||||
database.close()
|
||||
|
||||
logging.info('auth-mastodon script started, waiting for ejabberd requests')
|
||||
|
||||
|
||||
class EjabberdInputError(Exception):
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
def __str__(self):
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
########################################################################
|
||||
# Declarations
|
||||
#Declarations
|
||||
########################################################################
|
||||
|
||||
def ejabberd_in():
|
||||
logging.debug("trying to read 2 bytes from ejabberd:")
|
||||
logging.debug("trying to read 2 bytes from ejabberd:")
|
||||
|
||||
input_length = sys.stdin.buffer.read(2)
|
||||
input_length = sys.stdin.buffer.read(2)
|
||||
|
||||
if len(input_length) != 2:
|
||||
logging.debug("ejabberd sent us wrong things!")
|
||||
raise EjabberdInputError('Wrong input from ejabberd!')
|
||||
if len(input_length) is not 2:
|
||||
logging.debug("ejabberd sent us wrong things!")
|
||||
raise EjabberdInputError('Wrong input from ejabberd!')
|
||||
|
||||
logging.debug('got 2 bytes via stdin: %s' % input_length)
|
||||
logging.debug('got 2 bytes via stdin: %s'%input_length)
|
||||
|
||||
(size,) = struct.unpack('>h', input_length)
|
||||
logging.debug('size of data: %i' % size)
|
||||
(size,) = struct.unpack('>h', input_length)
|
||||
logging.debug('size of data: %i'%size)
|
||||
|
||||
income = sys.stdin.read(size)
|
||||
logging.debug("incoming data: %s" % income)
|
||||
income=sys.stdin.read(size)
|
||||
logging.debug("incoming data: %s"%income)
|
||||
|
||||
return income
|
||||
return income
|
||||
|
||||
|
||||
def ejabberd_out(bool):
|
||||
logging.debug("Ejabberd gets: %s" % bool)
|
||||
logging.debug("Ejabberd gets: %s" % bool)
|
||||
|
||||
token = genanswer(bool)
|
||||
token = genanswer(bool)
|
||||
|
||||
logging.debug(
|
||||
"sent bytes: %#x %#x %#x %#x"
|
||||
% (token[0], token[1], token[2], token[3]))
|
||||
logging.debug("sent bytes: %#x %#x %#x %#x" % (token[0], token[1], token[2], token[3]))
|
||||
|
||||
sys.stdout.buffer.write(token)
|
||||
sys.stdout.buffer.flush()
|
||||
sys.stdout.buffer.write(token)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
|
||||
def genanswer(bool):
|
||||
answer = 0
|
||||
if bool:
|
||||
answer = 1
|
||||
token = struct.pack('>hh', 2, answer)
|
||||
return token
|
||||
answer = 0
|
||||
if bool:
|
||||
answer = 1
|
||||
token = struct.pack('>hh', 2, answer)
|
||||
return token
|
||||
|
||||
|
||||
def get_password(user, host):
|
||||
# Right now we ignore the host component, as Mastodon doesn't store
|
||||
# it for local accounts. It may be required one day, so the code to
|
||||
# handle passing it to the query is left in for now.
|
||||
cursor = database.cursor()
|
||||
cursor.execute(db_query_getpass, {"user": user.lower(), "host": host})
|
||||
data = cursor.fetchone()
|
||||
cursor.close()
|
||||
return data[0] if data is not None else None
|
||||
# Right now we ignore the host component, as Mastodon doesn't store it for local accounts.
|
||||
# It may be required one day, so the code to handle passing it to the query is left in for now.
|
||||
cursor = database.cursor()
|
||||
cursor.execute(db_query_getpass, {"user": user.lower(), "host": host})
|
||||
data = cursor.fetchone()
|
||||
cursor.close()
|
||||
return data[0] if data != None else None
|
||||
|
||||
|
||||
def isuser(user, host):
|
||||
return get_password(user, host) is not None
|
||||
return get_password(user, host) != None
|
||||
|
||||
|
||||
def auth(user, host, password):
|
||||
db_password = get_password(user, host)
|
||||
if db_password is None:
|
||||
logging.debug("Wrong username: %s@%s" % (user, host))
|
||||
return False
|
||||
else:
|
||||
if bcrypt.checkpw(password.encode('utf8'), db_password.encode('utf8')):
|
||||
logging.debug("Validated %s against hash %s" % (user, db_password))
|
||||
return True
|
||||
else:
|
||||
logging.debug("Wrong password for user: %s@%s" % (user, host))
|
||||
return False
|
||||
db_password = get_password(user, host)
|
||||
if db_password == None:
|
||||
logging.debug("Wrong username: %s@%s" % (user, host))
|
||||
return False
|
||||
else:
|
||||
if bcrypt.checkpw(password.encode('utf8'), db_password.encode('utf8')):
|
||||
logging.debug("Validated %s against hash %s" % (user, db_password))
|
||||
return True
|
||||
else:
|
||||
logging.debug("Wrong password for user: %s@%s" % (user, host))
|
||||
return False
|
||||
|
||||
|
||||
########################################################################
|
||||
# Main Loop
|
||||
#Main Loop
|
||||
########################################################################
|
||||
|
||||
exitcode = 0
|
||||
exitcode=0
|
||||
|
||||
while True:
|
||||
logging.debug("start of infinite loop")
|
||||
logging.debug("start of infinite loop")
|
||||
|
||||
try:
|
||||
ejab_request = ejabberd_in().split(':', 3)
|
||||
except EOFError:
|
||||
break
|
||||
except Exception:
|
||||
logging.exception("Exception occured while reading stdin")
|
||||
raise
|
||||
try:
|
||||
ejab_request = ejabberd_in().split(':', 3)
|
||||
except EOFError:
|
||||
break
|
||||
except Exception as e:
|
||||
logging.exception("Exception occured while reading stdin")
|
||||
raise
|
||||
|
||||
op_result = False
|
||||
try:
|
||||
# Only 'auth' and 'isuser' implemented, placeholders left to
|
||||
# maybe expose other functions later but for now let's not even
|
||||
# think about modifying the Mastodon DB
|
||||
if ejab_request[0] == "auth":
|
||||
op_result = auth(ejab_request[1], ejab_request[2], ejab_request[3])
|
||||
elif ejab_request[0] == "isuser":
|
||||
op_result = isuser(ejab_request[1], ejab_request[2])
|
||||
elif ejab_request[0] == "setpass":
|
||||
op_result = False
|
||||
elif ejab_request[0] == "tryregister":
|
||||
op_result = False
|
||||
elif ejab_request[0] == "removeuser":
|
||||
op_result = False
|
||||
elif ejab_request[0] == "removeuser3":
|
||||
op_result = False
|
||||
except Exception:
|
||||
logging.exception("Exception occured")
|
||||
op_result = False
|
||||
try:
|
||||
# Only 'auth' and 'isuser' implemented, placeholders left to maybe
|
||||
# expose other functions later but for now let's not even think about
|
||||
# modifying the Mastodon DB
|
||||
if ejab_request[0] == "auth":
|
||||
op_result = auth(ejab_request[1], ejab_request[2], ejab_request[3])
|
||||
elif ejab_request[0] == "isuser":
|
||||
op_result = isuser(ejab_request[1], ejab_request[2])
|
||||
elif ejab_request[0] == "setpass":
|
||||
op_result = False
|
||||
elif ejab_request[0] == "tryregister":
|
||||
op_result = False
|
||||
elif ejab_request[0] == "removeuser":
|
||||
op_result = False
|
||||
elif ejab_request[0] == "removeuser3":
|
||||
op_result = False
|
||||
except Exception:
|
||||
logging.exception("Exception occured")
|
||||
|
||||
ejabberd_out(op_result)
|
||||
logging.info("successful" if op_result else "unsuccessful")
|
||||
ejabberd_out(op_result)
|
||||
logging.info("successful" if op_result else "unsuccessful")
|
||||
|
||||
logging.debug("end of infinite loop")
|
||||
logging.info('extauth script terminating')
|
||||
|
|
Loading…
Add table
Reference in a new issue