Added embedded minecraft authentication server to map minecraft

accounts to discord accounts.
feature/embed-uploads
Tristan Gosselin-Hane 7 years ago
parent c225482452
commit 46277944aa
  1. 1
      Pipfile
  2. 135
      Pipfile.lock
  3. 77
      auth_server.py
  4. 5
      config.example.json
  5. 3
      config.py
  6. 49
      database.py
  7. 2
      docker-compose.yml
  8. 61
      webhook-bridge.py

@ -14,6 +14,7 @@ future = "*"
"discord.py" = {git = "https://github.com/Rapptz/discord.py.git"} "discord.py" = {git = "https://github.com/Rapptz/discord.py.git"}
sqlalchemy = "*" sqlalchemy = "*"
mcstatus = "*" mcstatus = "*"
quarry = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

135
Pipfile.lock generated

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "b74ed57bc94dedc5e0cb39283d2d16b952c17d6db29c6a0c51956b5607f54017" "sha256": "45a63e42d93560ed412ddaeed6b5962c849eeaf9424c12f63e4a6c3aea903377"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -23,6 +23,20 @@
], ],
"version": "==0.24.0" "version": "==0.24.0"
}, },
"attrs": {
"hashes": [
"sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
"sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
],
"version": "==18.2.0"
},
"automat": {
"hashes": [
"sha256:cbd78b83fa2d81fe2a4d23d258e1661dd7493c9a50ee2f1a5b2cac61c1793b0e",
"sha256:fdccab66b68498af9ecfa1fa43693abe546014dd25cf28543cbe9d1334916a58"
],
"version": "==0.7.0"
},
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638",
@ -81,6 +95,13 @@
], ],
"version": "==7.0" "version": "==7.0"
}, },
"constantly": {
"hashes": [
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
"sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"
],
"version": "==15.1.0"
},
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:02602e1672b62e803e08617ec286041cc453e8d43f093a5f4162095506bc0beb", "sha256:02602e1672b62e803e08617ec286041cc453e8d43f093a5f4162095506bc0beb",
@ -129,6 +150,13 @@
"index": "pypi", "index": "pypi",
"version": "==0.16.0" "version": "==0.16.0"
}, },
"hyperlink": {
"hashes": [
"sha256:98da4218a56b448c7ec7d2655cb339af1f7d751cf541469bb4fc28c4a4245b34",
"sha256:f01b4ff744f14bc5d0a22a6b9f1525ab7d6312cb0ff967f59414bbac52f0a306"
],
"version": "==18.0.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
@ -136,6 +164,13 @@
], ],
"version": "==2.7" "version": "==2.7"
}, },
"incremental": {
"hashes": [
"sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f",
"sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3"
],
"version": "==17.5.0"
},
"mcstatus": { "mcstatus": {
"hashes": [ "hashes": [
"sha256:dc0e9fe79d8e3d4c7f3e218921b82b50ab31c991e88cb86831377bd5d2789c12" "sha256:dc0e9fe79d8e3d4c7f3e218921b82b50ab31c991e88cb86831377bd5d2789c12"
@ -146,6 +181,42 @@
"minecraft": { "minecraft": {
"git": "https://github.com/ammaraskar/pyCraft.git" "git": "https://github.com/ammaraskar/pyCraft.git"
}, },
"pyasn1": {
"hashes": [
"sha256:0ad0fe0593dde1e599cac0bf65bb1a4ec663032f0bc68ee44850db4251e8c501",
"sha256:13794d835643ee970b2c059dbfe4eb5d751e16c693c8baee61c526abd209e5c7",
"sha256:49a8ed515f26913049113820b462f698e6ed26df62c389dafb6fa3685ddca8de",
"sha256:74ac8521a0480f228549be20bea555ae35678f0e754c2fbc6f1576b0959bec43",
"sha256:89399ca8ecd4524f974e926d4ef9e7a787903e01f0a9cdff3131ad1361792fe5",
"sha256:8f291e0338d519a1a0d07f0b9d03c9265f6be26eb32fdd21af6d3259d14ea49c",
"sha256:b9d3abc5031e61927c82d4d96c1cec1e55676c1a991623cfed28faea73cdd7ca",
"sha256:d3bbd726c1a760d4ca596a4d450c380b81737612fe0182f5bb3caebc17461fd9",
"sha256:dea873d6c907c1cf1341fd88742a61efce33227d7743cb37564ab7d7e77dd9fd",
"sha256:ded5eea5cb88bc1ce9aa074b5a3092f95ce4741887e317e9b49c7ece75d7ea0e",
"sha256:e8b69ea2200d42201cbedd486eedb8980f320d4534f83ce2fb468e96aa5545d0",
"sha256:edad117649643230493aeb4955456ce19ab4b12e94489dde6f7094cdb5a3c87e",
"sha256:f58f2a3d12fd754aa123e9fa74fb7345333000a035f3921dbdaa08597aa53137"
],
"version": "==0.4.4"
},
"pyasn1-modules": {
"hashes": [
"sha256:077250b34432520430bc1c80dcbda4e354090785567c33ded35faa6df8d24753",
"sha256:0da2f947e8ad2697e86fe5fd0e55a4093a2fd79d839c9e19c34e28097db7002c",
"sha256:35ff894a0b5df8e28b700126b2869c7dcfb2b2db5bc82e5d5e82547069241553",
"sha256:44688b94841349648b1e1a5a7a3d96e6596d5d4f21d0b59a82307e153c4dc74b",
"sha256:833716dde880a7f2f2ccdeea9a096842626981ff2a477d8b318c0906367ac11b",
"sha256:a0cf3e1842e7c60fde97cb22d275eb6f9524f5c5250489e292529de841417547",
"sha256:a38a8811ea784c0136abfdba73963876328f66172db21a05a82f9515909bfb4e",
"sha256:a728bb9502d1fdc104c66f24a176b6a70a32e89d1d8a5b55c959233ed51c67be",
"sha256:c30a098435ea0989c37005a971843e9d3966c7f6d056ddbf052e5061c06e3291",
"sha256:c355a45b32c5bc1d9893eceb704b0cfcd1126f91b5a7b9ee64c1c05383283381",
"sha256:e64679de1940f41ead5170fce364d54e7b9e2e862f064727b6bcb5cee753b7a2",
"sha256:ed71d20225c356881c29f0b1d7a0d6521563a389d9478e8f95d798cc5ba07b88",
"sha256:f183f0940b9f5ed2ad9d04c80cab2451440fa9af4fc959d85113fadd2e777962"
],
"version": "==0.2.2"
},
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
@ -155,6 +226,30 @@
"pycraft": { "pycraft": {
"git": "https://github.com/ammaraskar/pyCraft.git" "git": "https://github.com/ammaraskar/pyCraft.git"
}, },
"pyhamcrest": {
"hashes": [
"sha256:6b672c02fdf7470df9674ab82263841ce8333fb143f32f021f6cb26f0e512420",
"sha256:7a4bdade0ed98c699d728191a058a60a44d2f9c213c51e2dd1e6fb42f2c6128a",
"sha256:8ffaa0a53da57e89de14ced7185ac746227a8894dbd5a3c718bf05ddbd1d56cd",
"sha256:bac0bea7358666ce52e3c6c85139632ed89f115e9af52d44b3c36e0bf8cf16a9",
"sha256:f30e9a310bcc1808de817a92e95169ffd16b60cbc5a016a49c8d0e8ababfae79"
],
"version": "==1.9.0"
},
"pyopenssl": {
"hashes": [
"sha256:26ff56a6b5ecaf3a2a59f132681e2a80afcc76b4f902f612f518f92c2a1bf854",
"sha256:6488f1423b00f73b7ad5167885312bb0ce410d3312eb212393795b53c8caa580"
],
"version": "==18.0.0"
},
"quarry": {
"hashes": [
"sha256:981ee6b2ca05f3ee94098ded0e9ae052ddec0d62448e0419a2cd7d306b55cd11"
],
"index": "pypi",
"version": "==1.1.1"
},
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
@ -163,6 +258,13 @@
"index": "pypi", "index": "pypi",
"version": "==2.19.1" "version": "==2.19.1"
}, },
"service-identity": {
"hashes": [
"sha256:0e76f3c042cc0f5c7e6da002cf646f59dc4023962d1d1166343ce53bdad39e17",
"sha256:4001fbb3da19e0df22c47a06d29681a398473af4aa9d745eca525b3b2c2302ab"
],
"version": "==17.0.0"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
@ -177,12 +279,43 @@
"index": "pypi", "index": "pypi",
"version": "==1.2.12" "version": "==1.2.12"
}, },
"twisted": {
"hashes": [
"sha256:5de7b79b26aee64efe63319bb8f037af88c21287d1502b39706c818065b3d5a4",
"sha256:95ae985716e8107816d8d0df249d558dbaabb677987cc2ace45272c166b267e4"
],
"version": "==18.7.0"
},
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
], ],
"version": "==1.23" "version": "==1.23"
},
"zope.interface": {
"hashes": [
"sha256:21506674d30c009271fe68a242d330c83b1b9d76d62d03d87e1e9528c61beea6",
"sha256:3d184aff0756c44fff7de69eb4cd5b5311b6f452d4de28cb08343b3f21993763",
"sha256:40c89d3a4e7e6c20db80be638a0a2be103b659ae3b2fc92e25ef1c7ac11be798",
"sha256:425e1458d7b893f3ae2fb1bd8344e7bb0beaa114cdc91299c797442638021a17",
"sha256:467d364b24cb398f76ad5e90398d71b9325eb4232be9e8a50d6a3b3c7a1c8789",
"sha256:4d17483ec4177ee085e690d8d4104de913392250bc50f41fb240c9f378bfb88b",
"sha256:57c38470d9f57e37afb460c399eb254e7193ac7fb8042bd09bdc001981a9c74c",
"sha256:727aa737a48dd33d6859296f15edb54e85ccdfa5667513f7a50daf362b3df75b",
"sha256:7307f7e341f47f057756e1e0e9f5bad85d4cf55a6a8aaafe681e2e5e5dd79446",
"sha256:9ada83f4384bbb12dedc152bcdd46a3ac9f5f7720d43ac3ce3e8e8b91d733c10",
"sha256:a1daf9c5120f3cc6f2b5fef8e1d2a3fb7bbbb20ed4bfdc25bc8364bc62dcf54b",
"sha256:aa1f22d5380d440c78bd9b13dbe697292422277a30eeabcd0ba66b6e37a25ef1",
"sha256:b445e2d58d8faa94a9ae863ed10c661e05d7ba6ce00548867a0153e6d927e4a2",
"sha256:c2a0ba07228921393b0e28d7306bf8652d5d392cff7eea3f4c0ba599b28f85de",
"sha256:e6b77ae84f2b8502d99a7855fa33334a1eb6159de45626905cb3e454c023f339",
"sha256:e881ef610ff48aece2f4ee2af03d2db1a146dc7c705561bd6089b2356f61641f",
"sha256:ec3cdfb92f714fc02aaf390ee34e7fa636a0478a0976733ac2b39bc23b11a095",
"sha256:ef4b9bf3ff28a874a2becda91c281ef4dbc89f9ceacfb1d11040893080a48e18",
"sha256:f41037260deaacb875db250021fe883bf536bf6414a4fd25b25059b02e31b120"
],
"version": "==4.5.0"
} }
}, },
"develop": {} "develop": {}

@ -0,0 +1,77 @@
import logging
from datetime import datetime, timezone
from quarry.net.server import ServerFactory, ServerProtocol
from database import AccountLinkToken, MinecraftAccount, DiscordAccount
import database_session
class AuthProtocol(ServerProtocol):
def player_joined(self):
# This method gets called when a player successfully joins the server.
# If we're in online mode (the default), this means auth with the
# session server was successful and the user definitely owns the
# display name they claim to.
# Call super. This switches us to "play" mode, marks the player as
# in-game, and does some logging.
ServerProtocol.player_joined(self)
# Define your own logic here. It could be an HTTP request to an API,
# or perhaps an update to a database table.
display_name = self.display_name
uuid = self.uuid
# Monkey Patch for Forge deciding to append "\x00FML\x00" to the connection string
if self.connect_host.endswith("\x00FML\x00"):
ip_addr = self.connect_host[:-5]
else:
ip_addr = self.connect_host
connect_port = self.connect_port
self.logger.info("[AUTH SERVER] {} ({}) connected to address {}:{}".format(display_name, uuid, ip_addr, connect_port))
try:
connection_token = ip_addr.split(".")[0]
session = database_session.get_session()
token = session.query(AccountLinkToken).filter_by(token=connection_token).first()
if not token:
self.close("You have connected with an invalid token!")
return
discord_account = session.query(DiscordAccount).filter_by(link_token_id=token.id).first()
if not discord_account:
self.close("You have connected with an invalid token!")
return
if datetime.utcnow() < token.expiry:
# Check if they already have a linked account and are re-linking
if discord_account.minecraft_account_id != None:
existing_account = session.query(MinecraftAccount).filter_by(id=discord_account.minecraft_account_id).first()
self.logger.info("[AUTH SERVER] unlinking existing {} account and replacing it with {}".format(existing_account.minecraft_uuid, str(uuid)))
session.delete(existing_account)
mc_account = MinecraftAccount(str(uuid), discord_account.id)
discord_account.minecraft_account = mc_account
session.add(mc_account)
session.delete(token)
session.commit()
session.close()
self.close("Your minecraft account has successfully been linked to your discord account!")
return
else:
session.delete(token)
session.commit()
session.close()
self.close("You have connected with an expired token! Please run the mc!register command again to get a new token.")
return
except Exception as e:
self.logger.error(e)
# Kick the player.
self.close("This shouldn't happen!")
class AuthFactory(ServerFactory):
protocol = AuthProtocol
motd = "Auth Server"

@ -9,6 +9,11 @@
"DISCORD_APP_TOKEN": "", "DISCORD_APP_TOKEN": "",
"LOG_LEVEL": "INFO" "LOG_LEVEL": "INFO"
}, },
"AUTH_SERVER": {
"BIND_IP": "",
"PORT": 9822,
"DNS_WILDCARD": ""
},
"DATABASE": { "DATABASE": {
"CONNECTION_STRING": "sqlite:////data/db.sqlite" "CONNECTION_STRING": "sqlite:////data/db.sqlite"
} }

@ -14,6 +14,9 @@ class Configuration(object):
self.mc_online = self._config["MAIN"]["MC_ONLINE"] self.mc_online = self._config["MAIN"]["MC_ONLINE"]
self.discord_token = self._config["MAIN"]["DISCORD_APP_TOKEN"] self.discord_token = self._config["MAIN"]["DISCORD_APP_TOKEN"]
self.logging_level = self._config["MAIN"]["LOG_LEVEL"] self.logging_level = self._config["MAIN"]["LOG_LEVEL"]
self.auth_ip = self._config["AUTH_SERVER"]["BIND_IP"]
self.auth_port = self._config["AUTH_SERVER"]["PORT"]
self.auth_dns = self._config["AUTH_SERVER"]["DNS_WILDCARD"]
self.database_connection_string = self._config["DATABASE"]["CONNECTION_STRING"] self.database_connection_string = self._config["DATABASE"]["CONNECTION_STRING"]
else: else:
print("error reading config") print("error reading config")

@ -1,5 +1,9 @@
from sqlalchemy import Column, String, Integer, Date from datetime import datetime, timedelta, timezone
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from database_session import Base from database_session import Base
class DiscordChannel(Base): class DiscordChannel(Base):
@ -10,3 +14,46 @@ class DiscordChannel(Base):
def __init__(self, channel_id): def __init__(self, channel_id):
self.channel_id = channel_id self.channel_id = channel_id
class AccountLinkToken(Base):
__tablename__ = 'account_link_tokens'
id = Column(Integer, primary_key=True)
discord_account = relationship("DiscordAccount", back_populates="link_token")
token = Column(String)
expiry = Column(DateTime)
def __init__(self, discord_id, token):
self.discord_id = discord_id
self.token = token
now = datetime.now(timezone.utc)
# Token expires an hour from now
then = now + timedelta(hours=1)
self.expiry = then
class MinecraftAccount(Base):
__tablename__ = 'minecraft_accounts'
id = Column(Integer, primary_key=True)
minecraft_uuid = Column(String)
discord_account = relationship("DiscordAccount", back_populates="minecraft_account")
def __init__(self, minecraft_uuid, discord_id):
self.minecraft_uuid = minecraft_uuid
self.discord_account_id = discord_id
class DiscordAccount(Base):
__tablename__ = 'discord_accounts'
id = Column(Integer, primary_key=True)
discord_id = Column(Integer)
link_token_id = Column(Integer, ForeignKey('account_link_tokens.id'))
minecraft_account_id = Column(Integer, ForeignKey('minecraft_accounts.id'))
link_token = relationship("AccountLinkToken", uselist=False, foreign_keys=[link_token_id], back_populates="discord_account")
minecraft_account = relationship("MinecraftAccount", uselist=False, foreign_keys=[minecraft_account_id], back_populates="discord_account")
def __init__(self, discord_id):
self.discord_id = discord_id

@ -2,6 +2,8 @@ version: '2.3'
services: services:
webhook-bridge: webhook-bridge:
build: . build: .
ports:
- 9822:9822
volumes: volumes:
- ./:/app:Z - ./:/app:Z
- ./:/data:Z - ./:/data:Z

@ -9,9 +9,12 @@ import requests
import json import json
import time import time
import logging import logging
import random
import string
from threading import Thread
from optparse import OptionParser from optparse import OptionParser
from config import Configuration from config import Configuration
from database import DiscordChannel from database import DiscordChannel, AccountLinkToken, MinecraftAccount, DiscordAccount
import database_session import database_session
from minecraft import authentication from minecraft import authentication
@ -38,6 +41,31 @@ def setup_logging(level):
stdout_logger.setFormatter(logging.Formatter(log_format)) stdout_logger.setFormatter(logging.Formatter(log_format))
logging.getLogger().addHandler(stdout_logger) logging.getLogger().addHandler(stdout_logger)
def run_auth_server(port):
# We need to import twisted after setting up the logger because twisted hijacks our logging
# TODO: Fix this in a cleaner way
from twisted.internet import reactor
from auth_server import AuthFactory, AuthProtocol
# Create factory
factory = AuthFactory()
# Listen
logging.info("Starting authentication server on port {}".format(port))
factory.listen("", port)
try:
reactor.run(installSignalHandlers=False)
except KeyboardInterrupt:
reactor.stop()
def generate_random_auth_token(length):
letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
return ''.join(random.choice(letters) for i in range(length))
def main(): def main():
config = Configuration("config.json") config = Configuration("config.json")
setup_logging(config.logging_level) setup_logging(config.logging_level)
@ -46,6 +74,9 @@ def main():
database_session.initialize(config) database_session.initialize(config)
reactor_thread = Thread(target=run_auth_server, args=(config.auth_port,))
reactor_thread.start()
def handle_disconnect(join_game_packet): def handle_disconnect(join_game_packet):
logging.info('Disconnected.') logging.info('Disconnected.')
connection.disconnect(immediate=True) connection.disconnect(immediate=True)
@ -82,6 +113,8 @@ def main():
except YggdrasilError as e: except YggdrasilError as e:
logging.info(e) logging.info(e)
sys.exit() sys.exit()
global BOT_USERNAME
BOT_USERNAME = auth_token.username
logging.info("Logged in as %s..." % auth_token.username) logging.info("Logged in as %s..." % auth_token.username)
if not is_server_online(): if not is_server_online():
logging.info('Not connecting to server because it appears to be offline.') logging.info('Not connecting to server because it appears to be offline.')
@ -203,7 +236,33 @@ def main():
@discord_bot.event @discord_bot.event
async def on_message(message): async def on_message(message):
# We do not want the bot to reply to itself
if message.author == discord_bot.user:
return
this_channel = message.channel.id this_channel = message.channel.id
if message.channel.is_private:
if message.content.startswith("mc!register"):
session = database_session.get_session()
discord_account = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first()
if not discord_account:
new_discord_account = DiscordAccount(message.author.id)
session.add(new_discord_account)
session.commit()
discord_account = session.query(DiscordAccount).filter_by(discord_id=message.author.id).first()
new_token = generate_random_auth_token(16)
account_link_token = AccountLinkToken(message.author.id, new_token)
discord_account.link_token = account_link_token
session.add(account_link_token)
session.commit()
msg = "Please connect your minecraft account to `{}.{}:{}` in order to link it to this bridge!".format(new_token, config.auth_dns, config.auth_port)
await discord_bot.send_message(message.channel, msg)
session.close()
return
else:
msg = "Unknown command, type `mc!help` for a list of commands."
await discord_bot.send_message(message.channel, msg)
return
if message.content.startswith("mc!chathere"): if message.content.startswith("mc!chathere"):
session = database_session.get_session() session = database_session.get_session()
channels = session.query(DiscordChannel).filter_by(channel_id=this_channel).all() channels = session.query(DiscordChannel).filter_by(channel_id=this_channel).all()

Loading…
Cancel
Save