|
| 1 | +import ipaddress |
| 2 | +import secrets |
| 3 | +import urllib.parse |
| 4 | + |
| 5 | +import discordoauth2 |
| 6 | +from bapi import cfg |
| 7 | +from bapi import db |
| 8 | +from flask import Blueprint |
| 9 | +from flask import jsonify |
| 10 | +from flask import redirect |
| 11 | +from flask import render_template |
| 12 | +from flask import request |
| 13 | +from flask import session |
| 14 | + |
| 15 | +discord_blueprint = Blueprint("discord", __name__, template_folder="templates", url_prefix="/discord") |
| 16 | + |
| 17 | +discord_client = discordoauth2.Client( |
| 18 | + cfg.PRIVATE["discord"]["client_id"], |
| 19 | + secret=cfg.PRIVATE["discord"]["client_secret"], |
| 20 | + redirect=f"{cfg.API['api-url']}/discord/callback", |
| 21 | +) |
| 22 | + |
| 23 | + |
| 24 | +@discord_blueprint.route("/auth", methods=["GET"]) |
| 25 | +def discord_auth(): |
| 26 | + ip = request.args.get("ip") |
| 27 | + if not isinstance(ip, str): |
| 28 | + return jsonify({"error": "provided IP address invalid"}) |
| 29 | + try: |
| 30 | + ip = ipaddress.ip_address(ip) |
| 31 | + except ValueError: |
| 32 | + return jsonify({"error": "provided IP address invalid"}) |
| 33 | + if ip.version == 6: |
| 34 | + return jsonify({"error": "IPv6 address not allowed"}) |
| 35 | + if ip.is_multicast or ip.is_unspecified: |
| 36 | + return jsonify({"error": "multicast or unspecified address not allowed"}) |
| 37 | + seeker_port = request.args.get("seeker_port") |
| 38 | + if not isinstance(seeker_port, str) or not seeker_port.isdigit(): |
| 39 | + seeker_port = "" |
| 40 | + try: |
| 41 | + seeker_port = int(seeker_port) |
| 42 | + except ValueError: |
| 43 | + seeker_port = "" |
| 44 | + if not isinstance(seeker_port, int) or seeker_port > 65535 or seeker_port < 10000: |
| 45 | + seeker_port = "" |
| 46 | + session["oauth2_state"] = ( |
| 47 | + f"{urllib.parse.quote(ip.exploded, safe="", encoding="utf-8")},{seeker_port},{secrets.token_urlsafe(16)}" |
| 48 | + ) |
| 49 | + return redirect(discord_client.generate_uri(scope=["identify"], state=session["oauth2_state"])) |
| 50 | + |
| 51 | + |
| 52 | +@discord_blueprint.route("/callback", methods=["GET"]) |
| 53 | +def discord_callback(): |
| 54 | + code = request.args.get("code") |
| 55 | + state = request.args.get("state") |
| 56 | + |
| 57 | + if code is None: |
| 58 | + return jsonify({"error": "bad oauth code"}) |
| 59 | + |
| 60 | + state_session = session.get("oauth2_state") |
| 61 | + if state is None or state_session is None or state != state_session: |
| 62 | + return jsonify({"error": "bad state"}) # let's not keep this around |
| 63 | + del session["oauth2_state"] |
| 64 | + |
| 65 | + state_attrs = state.split(",") |
| 66 | + ip = urllib.parse.unquote(state_attrs[0]) |
| 67 | + seeker_port = state_attrs[1] |
| 68 | + discord_uid = None |
| 69 | + discord_username = None |
| 70 | + |
| 71 | + try: |
| 72 | + access = discord_client.exchange_code(code) |
| 73 | + identify = access.fetch_identify() |
| 74 | + discord_uid = identify["id"] |
| 75 | + discord_username = identify["username"] |
| 76 | + discriminator = identify["discriminator"] |
| 77 | + # Handle non-unique usernames |
| 78 | + if discriminator != "0": |
| 79 | + discord_username = f"{discord_username}#{discriminator}" |
| 80 | + except discordoauth2.exceptions.RateLimited: |
| 81 | + return jsonify({"error": "too many requests"}), 429 |
| 82 | + except KeyError | discordoauth2.exceptions.HTTPException | discordoauth2.exceptions.Forbidden: |
| 83 | + return jsonify({"error": "error authorizing with Discord"}) |
| 84 | + if discord_uid is None or discord_username is None: |
| 85 | + return jsonify({"error": "error authorizing with Discord"}) |
| 86 | + token = db.Session.create_session(ip, "discord", discord_uid, discord_username, cfg.API["game-session-duration"]) |
| 87 | + if token is not None: |
| 88 | + return render_template( |
| 89 | + "token.html", token=token, token_duration=cfg.API["game-session-duration"], seeker_port=seeker_port |
| 90 | + ) |
| 91 | + else: |
| 92 | + return jsonify({"error": "error creating session"}) |
0 commit comments