Initial commit
This commit is contained in:
commit
85515c9528
5 changed files with 160 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
config.json
|
||||
37
README.md
Normal file
37
README.md
Normal file
|
|
@ -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.
|
||||
BIN
__pycache__/wrapper.cpython-313.pyc
Normal file
BIN
__pycache__/wrapper.cpython-313.pyc
Normal file
Binary file not shown.
4
config.example.json
Normal file
4
config.example.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"secret": "YOUR_BASE32_SECRET_HERE",
|
||||
"launcher_cmd": "xivlauncher-core"
|
||||
}
|
||||
118
wrapper.py
Executable file
118
wrapper.py
Executable file
|
|
@ -0,0 +1,118 @@
|
|||
#!/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 (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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue