127 lines
4.2 KiB
Python
Executable file
127 lines
4.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
import sys
|
|
import json
|
|
import time
|
|
import subprocess
|
|
import threading
|
|
import urllib.request
|
|
import urllib.error
|
|
import hmac
|
|
import base64
|
|
import struct
|
|
import hashlib
|
|
import os
|
|
|
|
def get_totp_token(secret):
|
|
"""Generates a TOTP code from the secret."""
|
|
try:
|
|
# Pad the secret if needed for base32 decoding
|
|
secret_clean = secret.replace(" ", "").upper()
|
|
padding = '=' * ((8 - len(secret_clean) % 8) % 8)
|
|
key = base64.b32decode(secret_clean + padding)
|
|
|
|
# Time step is 30 seconds
|
|
msg = struct.pack(">Q", int(time.time() / 30))
|
|
h = hmac.new(key, msg, hashlib.sha1).digest()
|
|
o = h[19] & 15
|
|
h = (struct.unpack(">I", h[o:o+4])[0] & 0x7fffffff) % 1000000
|
|
return f"{h:06d}"
|
|
except Exception as e:
|
|
print(f"Error generating TOTP: {e}")
|
|
return None
|
|
|
|
def send_otp(secret):
|
|
"""Attempts to send the OTP to the local XIVLauncher server."""
|
|
url_base = "http://127.0.0.1:4646/ffxivlauncher/"
|
|
print("Waiting for XIVLauncher to accept OTP...")
|
|
|
|
# Try for up to 5 minutes (300 seconds) - launcher might take time to load
|
|
start_time = time.time()
|
|
while time.time() - start_time < 300:
|
|
otp = get_totp_token(secret)
|
|
if not otp:
|
|
print("Failed to generate OTP token.")
|
|
return
|
|
|
|
try:
|
|
# Short timeout so we don't block
|
|
with urllib.request.urlopen(f"{url_base}{otp}", timeout=1) as response:
|
|
if response.status == 200:
|
|
print(f"SUCCESS: OTP {otp} sent to XIVLauncher!")
|
|
# We can stop trying once successful
|
|
return
|
|
except (urllib.error.URLError, ConnectionRefusedError, ConnectionResetError):
|
|
# Server not up or not accepting connections yet
|
|
pass
|
|
except Exception as e:
|
|
# Some other error (e.g. malformed URL if secret is bad?)
|
|
print(f"Debug: Connection error: {e}")
|
|
|
|
time.sleep(1)
|
|
|
|
print("Timed out waiting for XIVLauncher OTP prompt.")
|
|
|
|
def main():
|
|
# Determine config path (XDG standard)
|
|
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
|
config_dir = os.path.join(xdg_config_home, 'xivlauncher-wrapper')
|
|
config_path = os.path.join(config_dir, 'config.json')
|
|
|
|
# Fallback to script directory (legacy/development)
|
|
if not os.path.exists(config_path):
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
local_config = os.path.join(script_dir, "config.json")
|
|
if os.path.exists(local_config):
|
|
config_path = local_config
|
|
|
|
if not os.path.exists(config_path):
|
|
print(f"Config file not found.")
|
|
print(f"Expected at: {config_path}")
|
|
print(f"Please copy config.example.json to {config_path} and fill in your secret.")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
with open(config_path, 'r') as f:
|
|
config = json.load(f)
|
|
except json.JSONDecodeError:
|
|
print(f"Error parsing {config_path}. Invalid JSON.")
|
|
sys.exit(1)
|
|
|
|
secret = config.get("secret")
|
|
cmd = config.get("launcher_cmd", "xivlauncher-core")
|
|
|
|
if not secret or secret == "YOUR_BASE32_SECRET_HERE":
|
|
print("Error: Invalid or missing 'secret' in config.json.")
|
|
sys.exit(1)
|
|
|
|
print(f"Launching: {cmd}")
|
|
|
|
try:
|
|
# Launch the process
|
|
# If the user provided arguments to this script, we could pass them through?
|
|
# But for now, just launch the command from config
|
|
proc = subprocess.Popen(cmd.split())
|
|
except FileNotFoundError:
|
|
print(f"Error: Command '{cmd}' not found in PATH.")
|
|
sys.exit(1)
|
|
|
|
# Start the OTP injector in a background thread
|
|
injector_thread = threading.Thread(target=send_otp, args=(secret,))
|
|
injector_thread.daemon = True
|
|
injector_thread.start()
|
|
|
|
try:
|
|
# Wait for the launcher to exit
|
|
proc.wait()
|
|
except KeyboardInterrupt:
|
|
print("\nStopping wrapper...")
|
|
proc.terminate()
|
|
try:
|
|
proc.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
|
|
sys.exit(proc.returncode)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|