Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/login/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@

//! Child-process entry point: hosts a single tao window with a wry WebView
//! pointed at the server's login page and intercepts navigation to the
//! `ffxiv://login_success?sessionId=…` custom scheme.
//! `ffxiv://login_success?sessionId=…` custom scheme. Login pages that
//! instead answer with the retail-era `<x-sqexauth sid="…">` element
//! (Project Meteor lineage servers such as AetherXIV 1.3) are bridged to
//! the same scheme by an injected script that scrapes the element and
//! triggers the `ffxiv://` navigation itself.
//!
//! Communicates the outcome to the parent over stdout using three
//! one-line sentinels defined below. The parent (see
Expand All @@ -43,6 +47,21 @@ use crate::version::APP_NAME;

/// Printed on success, followed by the 56-char session id.
pub const SESSION_PREFIX: &str = "SESSION_ID=";

/// Bridges the retail `<x-sqexauth sid="…"/>` login contract to the
/// `ffxiv://login_success` navigation the handler below understands.
/// Runs on every page load; pages without the element are untouched.
const SQEXAUTH_BRIDGE_SCRIPT: &str = r#"
document.addEventListener("DOMContentLoaded", function () {
var el = document.querySelector("x-sqexauth");
if (el) {
var sid = el.getAttribute("sid");
if (sid) {
window.location.href = "ffxiv://login_success?sessionId=" + sid;
}
}
});
"#;
/// Printed when the user closes the webview without logging in.
pub const CANCEL_SENTINEL: &str = "LOGIN_CANCELLED";
/// Printed when the webview itself errors out (e.g. fails to load).
Expand Down Expand Up @@ -92,6 +111,7 @@ fn build_webview(window: &tao::window::Window, login_url: &str) -> Result<wry::W
.context("tao login window is missing its default GTK vbox")?;
WebViewBuilder::new_gtk(vbox)
.with_url(login_url)
.with_initialization_script(SQEXAUTH_BRIDGE_SCRIPT)
.with_navigation_handler(navigation_handler)
.build()
.context("building wry webview")
Expand All @@ -101,6 +121,7 @@ fn build_webview(window: &tao::window::Window, login_url: &str) -> Result<wry::W
fn build_webview(window: &tao::window::Window, login_url: &str) -> Result<wry::WebView> {
WebViewBuilder::new(window)
.with_url(login_url)
.with_initialization_script(SQEXAUTH_BRIDGE_SCRIPT)
.with_navigation_handler(navigation_handler)
.build()
.context("building wry webview")
Expand All @@ -112,6 +133,7 @@ fn build_webview(window: &tao::window::Window, login_url: &str) -> Result<wry::W
// since the window has no other content.
WebViewBuilder::new(window)
.with_url(login_url)
.with_initialization_script(SQEXAUTH_BRIDGE_SCRIPT)
.with_navigation_handler(navigation_handler)
.build()
.context("building wry webview")
Expand Down
10 changes: 10 additions & 0 deletions src/servers/default_servers.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,13 @@ login_url = "http://127.0.0.1:54993/login"
name = "Project Meteor"
address = "127.0.0.1"
login_url = "http://127.0.0.1:8080/login.php"

# The AetherXIV 1.3 docker stack in the sibling AetherXIV-EchoGate repo
# (its main working line). Lobby on the stock 54994 port; the PHP web
# container serves the login page on 8080. The page answers with the
# retail x-sqexauth element, which the login WebView bridges to
# ffxiv://login_success itself, so the normal Login flow works.
[[server]]
name = "AetherXIV 1.3 (Docker Local)"
address = "127.0.0.1"
login_url = "http://127.0.0.1:8080/login/index.php"
50 changes: 43 additions & 7 deletions src/servers/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

Expand All @@ -32,9 +31,12 @@ pub struct ServerDefinition {
pub login_url: String,
}

/// Order-preserving: `iter()` yields entries in the order they appear in the
/// TOML file, which is also the dropdown order and the fresh-install default
/// (first entry).
#[derive(Debug, Clone, Default)]
pub struct ServerDefinitions {
servers: BTreeMap<String, ServerDefinition>,
servers: Vec<ServerDefinition>,
}

#[derive(Debug, Deserialize, Default)]
Expand All @@ -56,21 +58,27 @@ impl ServerDefinitions {

pub fn parse(toml_text: &str) -> Result<Self> {
let parsed: ServersFile = toml::from_str(toml_text).context("parsing servers TOML")?;
let mut servers: BTreeMap<String, ServerDefinition> = BTreeMap::new();
let mut servers: Vec<ServerDefinition> = Vec::new();
for server in parsed.servers {
if !server.name.is_empty() {
servers.insert(server.name.clone(), server);
if server.name.is_empty() {
continue;
}
// Duplicate names keep the first occurrence's position but take
// the later definition, matching the previous map semantics.
match servers.iter_mut().find(|s| s.name == server.name) {
Some(existing) => *existing = server,
None => servers.push(server),
}
}
Ok(Self { servers })
}

pub fn iter(&self) -> impl Iterator<Item = &ServerDefinition> {
self.servers.values()
self.servers.iter()
}

pub fn get(&self, name: &str) -> Option<&ServerDefinition> {
self.servers.get(name)
self.servers.iter().find(|s| s.name == name)
}

pub fn is_empty(&self) -> bool {
Expand Down Expand Up @@ -125,4 +133,32 @@ login_url = "https://b/login"
let defs = ServerDefinitions::parse("").unwrap();
assert!(defs.is_empty());
}

#[test]
fn iteration_preserves_file_order() {
let toml_text = r#"
[[server]]
name = "Zeta"
address = "z.example"
login_url = "https://z/login"

[[server]]
name = "Alpha"
address = "a.example"
login_url = "https://a/login"
"#;
let defs = ServerDefinitions::parse(toml_text).unwrap();
let names: Vec<&str> = defs.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, ["Zeta", "Alpha"]);
}

#[test]
fn aetherxiv_13_entry_points_at_docker_login() {
let defs = ServerDefinitions::load_default().unwrap();
let entry = defs
.get("AetherXIV 1.3 (Docker Local)")
.expect("AetherXIV 1.3 present");
assert_eq!(entry.address, "127.0.0.1");
assert_eq!(entry.login_url, "http://127.0.0.1:8080/login/index.php");
}
}
Loading