GLXY Client Changes

This commit is contained in:
firestarsdog
2026-03-10 23:48:41 -04:00
committed by firestar5683
parent b631895e66
commit d7d844ae35
4 changed files with 114 additions and 46 deletions
+52 -23
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
import hmac
import json
import platform
import shutil
import signal
@@ -20,25 +22,47 @@ AUTH_PORT = 8083
process = None
auth_server = None
VALID_PATHS = {"/glxylogin": "glxyauth", "/glxyverify": "glxysession"}
class AuthHandler(BaseHTTPRequestHandler):
"""Serves only GET /glxyauth — returns the PIN hash file contents."""
def do_GET(self):
if self.path == "/glxyauth":
auth_file = GALAXY_DIR / "glxyauth"
if auth_file.exists():
data = auth_file.read_bytes()
"""Validates login hashes and verifies session tokens."""
dongle_id = ""
def do_POST(self):
target = VALID_PATHS.get(self.path)
if not target:
self.send_response(404)
self.end_headers()
return
file_path = GALAXY_DIR / target
if not file_path.exists():
self.send_response(404)
self.end_headers()
return
try:
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length).decode("utf-8").strip()
if hmac.compare_digest(body, file_path.read_text().strip()):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
if self.path == "/glxylogin":
self.send_header("Content-Type", "application/json")
self.end_headers()
token = (GALAXY_DIR / "glxysession").read_text().strip()
self.wfile.write(json.dumps({"dongle_id": self.dongle_id, "token": token}).encode())
else:
self.end_headers()
return
self.send_response(404)
except Exception:
pass
self.send_response(403)
self.end_headers()
def log_message(self, format, *args):
pass # suppress request logs
pass
def start_auth_server():
@@ -111,7 +135,6 @@ def main():
signal.signal(signal.SIGTERM, cleanup_frpc)
signal.signal(signal.SIGINT, cleanup_frpc)
# Wait for DongleId to be set (usually set on boot/pairing)
dongle_id = params.get("DongleId", encoding='utf8')
while not dongle_id:
print("Galaxy: Waiting for DongleId...")
@@ -119,16 +142,22 @@ def main():
dongle_id = params.get("DongleId", encoding='utf8')
print(f"Galaxy: DongleId: {dongle_id}")
AuthHandler.dongle_id = dongle_id
print("Galaxy: Starting manager loop...")
last_slug = None
while True:
glxyauth_file = GALAXY_DIR / "glxyauth"
galaxy_password_hash = glxyauth_file.read_text().strip() if glxyauth_file.exists() else None
is_paired = galaxy_password_hash and len(galaxy_password_hash) == 64
slug_file = GALAXY_DIR / "glxyslug"
slug = slug_file.read_text().strip() if slug_file.exists() else None
is_paired = galaxy_password_hash and len(galaxy_password_hash) == 64 and slug
if is_paired:
if process is None or process.poll() is not None:
if process is not None:
if process is None or process.poll() is not None or slug != last_slug:
cleanup_frpc()
if process is not None and slug == last_slug:
print(f"Galaxy: frpc exited with code {process.returncode}. Restarting...")
print("Galaxy: Password set. Preparing frpc tunnel...")
@@ -137,7 +166,6 @@ def main():
time.sleep(10)
continue
# Start the tiny auth HTTP server (serves /glxyauth on localhost)
start_auth_server()
frpc_toml = GALAXY_DIR / "frpc.toml"
@@ -150,37 +178,38 @@ tls.enable = true
poolCount = 2
[[proxies]]
name = "{dongle_id}_pond"
name = "{slug}_pond"
type = "http"
localIP = "127.0.0.1"
localPort = 8082
customDomains = ["{dongle_id}.devices.local"]
customDomains = ["{slug}.devices.local"]
transport.useCompression = true
[[proxies]]
name = "{dongle_id}_auth"
name = "{slug}_auth"
type = "http"
localIP = "127.0.0.1"
localPort = {AUTH_PORT}
customDomains = ["auth-{dongle_id}.devices.local"]
customDomains = ["auth-{slug}.devices.local"]
"""
frpc_toml.write_text(config)
print("Galaxy: Starting frpc tunnel...")
print(f"Galaxy: Starting frpc tunnel (slug: {slug[:4]}...)...")
log_file = open(FRPC_LOG, 'a')
process = subprocess.Popen(
[str(GALAXY_DIR / "frpc"), "-c", str(frpc_toml)],
stdout=log_file,
stderr=log_file
)
last_slug = slug
else:
if process is not None and process.poll() is None:
print("Galaxy: Password cleared. Stopping frpc tunnel...")
cleanup_frpc()
last_slug = None
time.sleep(3)
if __name__ == "__main__":
main()
+16 -6
View File
@@ -3210,10 +3210,11 @@ def setup(app):
paired = GALAXY_AUTH_FILE.is_file() and len(GALAXY_AUTH_FILE.read_text().strip()) == 64
except Exception:
paired = False
dongle_id = params.get("DongleId", encoding="utf8") or ""
slug_file = GALAXY_DIR / "glxyslug"
slug = slug_file.read_text().strip() if slug_file.is_file() else ""
return jsonify({
"paired": paired,
"url": f"https://galaxy.firestar.link/{dongle_id}" if dongle_id else "",
"url": f"https://galaxy.firestar.link/{slug}" if slug else "",
})
@app.route("/api/galaxy/pair", methods=["POST"])
@@ -3226,17 +3227,26 @@ def setup(app):
pw_hash = hashlib.sha256(password.encode()).hexdigest()
GALAXY_DIR.mkdir(parents=True, exist_ok=True)
GALAXY_AUTH_FILE.write_text(pw_hash)
# Generate 256-bit secure session token
(GALAXY_DIR / "glxysession").write_text(secrets.token_hex(32))
# Generate 16-character alphanumeric routing slug
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
slug = ''.join(secrets.choice(charset) for _ in range(16))
(GALAXY_DIR / "glxyslug").write_text(slug)
dongle_id = params.get("DongleId", encoding="utf8") or ""
return jsonify({
"message": "Pairing successful!",
"url": f"https://galaxy.firestar.link/{dongle_id}" if dongle_id else "",
"url": f"https://galaxy.firestar.link/{slug}",
})
@app.route("/api/galaxy/unpair", methods=["POST"])
def galaxy_unpair():
if GALAXY_AUTH_FILE.is_file():
GALAXY_AUTH_FILE.unlink()
for f in ["glxyauth", "glxysession", "glxyslug"]:
file_path = GALAXY_DIR / f
if file_path.is_file():
file_path.unlink()
return jsonify({"message": "Galaxy unpaired successfully."})
@app.route("/api/tailscale/installed", methods=["GET"])
+45 -16
View File
@@ -6,6 +6,7 @@
#include <QDebug>
#include <QCryptographicHash>
#include <QRandomGenerator>
#include <QrCode.hpp>
#include "common/watchdog.h"
@@ -259,6 +260,16 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
});
addItem(pair_device);
// Shared helper to show QR popup
auto showGalaxyQR = [=]() {
std::string slug = util::read_file("/data/galaxy/glxyslug");
if (slug.empty()) return;
QString url = "https://galaxy.firestar.link/" + QString::fromStdString(slug);
GalaxyQRPopup *popup = new GalaxyQRPopup(url, this);
popup->exec();
popup->deleteLater();
};
pair_galaxy = new ButtonControl(tr("Galaxy"), tr("Pair"), tr("Pair your device with Galaxy for remote access to The Pond."));
connect(pair_galaxy, &ButtonControl::clicked, [=]() {
std::string current_password = util::read_file("/data/galaxy/glxyauth");
@@ -268,32 +279,50 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) {
std::string hash = QCryptographicHash::hash(new_password.toUtf8(), QCryptographicHash::Sha256).toHex().toStdString();
util::create_directories("/data/galaxy", 0775);
util::write_file("/data/galaxy/glxyauth", hash.data(), hash.size(), O_WRONLY | O_CREAT | O_TRUNC);
// Generate 256-bit session token (64 hex chars)
QByteArray session_bytes(32, 0);
QRandomGenerator::securelySeeded().fillRange(reinterpret_cast<quint32*>(session_bytes.data()), 8);
std::string session_token = session_bytes.toHex().toStdString();
util::write_file("/data/galaxy/glxysession", session_token.data(), session_token.size(), O_WRONLY | O_CREAT | O_TRUNC);
// Generate 16-char URL-safe slug (alphanumeric only)
const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
std::string slug(16, '\0');
auto *rng = QRandomGenerator::global();
for (int i = 0; i < 16; i++) slug[i] = charset[rng->bounded(62)];
util::write_file("/data/galaxy/glxyslug", slug.data(), slug.size(), O_WRONLY | O_CREAT | O_TRUNC);
pair_galaxy->setText(tr("Unpair"));
ConfirmationDialog::alert(tr("Pairing successful! Visit galaxy.firestar.link/") + getDongleId().value_or("") + tr(" to connect."), this);
galaxy_qr->setVisible(true);
galaxy_qr_btn->setVisible(true);
showGalaxyQR();
}
} else {
if (ConfirmationDialog::confirm(tr("Are you sure you want to unpair from Galaxy?"), tr("Unpair"), this)) {
std::remove("/data/galaxy/glxyauth");
std::remove("/data/galaxy/glxysession");
std::remove("/data/galaxy/glxyslug");
pair_galaxy->setText(tr("Pair"));
galaxy_qr->setVisible(false);
galaxy_qr_btn->setVisible(false);
}
}
});
// QR button — sits inline next to the Pair/Unpair button
galaxy_qr_btn = new QPushButton(tr("QR"));
galaxy_qr_btn->setFixedSize(250, 100);
galaxy_qr_btn->setStyleSheet(R"(
QPushButton { background-color: #393939; color: #E4E4E4; border-radius: 50px; font-size: 35px; font-weight: 500; }
QPushButton:pressed { background-color: #4a4a4a; }
)");
galaxy_qr_btn->setVisible(false);
connect(galaxy_qr_btn, &QPushButton::clicked, showGalaxyQR);
if (QHBoxLayout *hlayout = pair_galaxy->findChild<QHBoxLayout *>()) {
hlayout->insertWidget(3, galaxy_qr_btn);
}
addItem(pair_galaxy);
galaxy_qr = new ButtonControl(tr("Galaxy QR"), tr("SHOW"), tr("Show a QR code to quickly open Galaxy on your phone."));
connect(galaxy_qr, &ButtonControl::clicked, [=]() {
auto dongleId = getDongleId();
if (!dongleId) return;
QString url = "https://galaxy.firestar.link/" + *dongleId;
GalaxyQRPopup *popup = new GalaxyQRPopup(url, this);
popup->exec();
popup->deleteLater();
});
addItem(galaxy_qr);
// offroad-only buttons
auto dcamBtn = new ButtonControl(tr("Driver Camera"), tr("PREVIEW"),
@@ -437,7 +466,7 @@ void DevicePanel::showEvent(QShowEvent *event) {
std::string galaxy_pin = util::read_file("/data/galaxy/glxyauth");
bool galaxy_paired = !galaxy_pin.empty();
pair_galaxy->setText(galaxy_paired ? tr("Unpair") : tr("Pair"));
galaxy_qr->setVisible(galaxy_paired);
galaxy_qr_btn->setVisible(galaxy_paired);
ListWidget::showEvent(event);
}
+1 -1
View File
@@ -86,7 +86,7 @@ private:
Params params;
ButtonControl *pair_device;
ButtonControl *pair_galaxy;
ButtonControl *galaxy_qr;
QPushButton *galaxy_qr_btn;
};
class TogglesPanel : public ListWidget {