From 85515c952869cca8bb43e7ad9ad7c2210a84f521 Mon Sep 17 00:00:00 2001 From: CPTN Cosmo Date: Fri, 9 Jan 2026 22:54:55 +0100 Subject: [PATCH] Initial commit --- .gitignore | 1 + README.md | 37 +++++++++ __pycache__/wrapper.cpython-313.pyc | Bin 0 -> 6012 bytes config.example.json | 4 + wrapper.py | 118 ++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __pycache__/wrapper.cpython-313.pyc create mode 100644 config.example.json create mode 100755 wrapper.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b09926e --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# XIVLauncher OTP Wrapper + +This is a simple wrapper script for Linux that launches `xivlauncher-core` and automatically injects your One-Time Password (OTP) when the launcher requests it. + +## Setup + +1. **Configure:** + Copy the example config and edit it with your details. + ```bash + cp config.example.json config.json + nano config.json + ``` + * `secret`: Enter your TOTP secret key (in Base32 format, usually provided when you set up your authenticator app). + * `launcher_cmd`: The command to launch XIVLauncher (default: `xivlauncher-core`). + +2. **Make Executable:** + Ensure the script is executable: + ```bash + chmod +x wrapper.py + ``` + +3. **XIVLauncher Settings:** + Ensure that "Enable XL Authenticator app/OTP macro support" is enabled in XIVLauncher settings. + +## Usage + +Simply run the wrapper instead of the launcher: + +```bash +./wrapper.py +``` + +The script will: +1. Launch XIVLauncher. +2. Wait for the launcher to start its local HTTP server (port 4646). +3. Generate the current OTP code. +4. Send it to the launcher automatically. diff --git a/__pycache__/wrapper.cpython-313.pyc b/__pycache__/wrapper.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4a8e99ac36658eef2a28d7542a279fe04e5f477 GIT binary patch literal 6012 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", 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 (look in script directory) + script_dir = os.path.dirname(os.path.realpath(__file__)) + config_path = os.path.join(script_dir, "config.json") + + if not os.path.exists(config_path): + print(f"Config file not found at: {config_path}") + print("Please copy config.example.json to config.json 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()