Update AppImage

This commit is contained in:
MacRimi
2026-05-24 11:37:20 +02:00
parent 4b934db7db
commit 105576cf17
7 changed files with 140 additions and 59 deletions
+3 -15
View File
@@ -35,8 +35,8 @@ import { DialogHeader, DialogDescription } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Dialog as SearchDialog, DialogContent as SearchDialogContent, DialogTitle as SearchDialogTitle } from "@/components/ui/dialog"
import "xterm/css/xterm.css"
import { API_PORT, fetchApi } from "@/lib/api-config"
import { getTicketedWsUrl } from "@/lib/terminal-ws"
import { fetchApi } from "@/lib/api-config"
import { getTicketedWsUrl, getWsUrl } from "@/lib/terminal-ws"
interface LxcTerminalModalProps {
open: boolean
@@ -80,19 +80,7 @@ const proxmoxCommands = [
]
function getWebSocketUrl(): string {
if (typeof window === "undefined") {
return "ws://localhost:8008/ws/terminal"
}
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
if (isStandardPort) {
return `${wsProtocol}//${hostname}/ws/terminal`
} else {
return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal`
}
return getWsUrl("/ws/terminal")
}
export function LxcTerminalModal({
+3 -17
View File
@@ -29,8 +29,7 @@ import {
DropdownMenuLabel,
} from "@/components/ui/dropdown-menu"
import "xterm/css/xterm.css"
import { API_PORT } from "@/lib/api-config"
import { getTicketedWsUrl } from "@/lib/terminal-ws"
import { getTicketedWsUrl, getWsUrl } from "@/lib/terminal-ws"
interface WebInteraction {
type: "yesno" | "menu" | "msgbox" | "input" | "inputbox"
@@ -530,21 +529,8 @@ const initMessage = {
}
}, [isOpen, isComplete, attemptReconnect])
const getScriptWebSocketUrl = (sid: string): string => {
if (typeof window === "undefined") {
return `ws://localhost:${API_PORT}/ws/script/${sid}`
}
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
if (isStandardPort) {
return `${wsProtocol}//${hostname}/ws/script/${sid}`
} else {
return `${wsProtocol}//${hostname}:${API_PORT}/ws/script/${sid}`
}
}
const getScriptWebSocketUrl = (sid: string): string =>
getWsUrl(`/ws/script/${sid}`)
const handleInteractionResponse = (value: string) => {
if (!wsRef.current || !currentInteraction) {
+28 -7
View File
@@ -411,9 +411,27 @@ export function Settings() {
// available version, and updating doesn't need to ask which flavour
// to install in. The user can always re-install via the
// customizable post-install flow if they want different parameters.
// Resolve which flow (auto vs custom) actually has an implementation
// for this tool. Some tools live only in the customizable flow (e.g.
// fastfetch, which needs an interactive menu and has no auto
// variant). When the recorded source is "auto" but the auto flow has
// no function for this tool, the bash wrapper aborts with
// "Function '<x>' is not defined in the auto flow". This helper
// silently routes to the only available flow instead.
const resolveEffectiveSource = (tool: ProxMenuxTool): string => {
const recorded = tool.source || "auto"
if (recorded === "auto" && !tool.function_auto && tool.function_custom) {
return "custom"
}
if (recorded === "custom" && !tool.function_custom && tool.function_auto) {
return "auto"
}
return recorded
}
const handleSingleToolUpdate = (tool: ProxMenuxTool) => {
if (!tool.has_update) return
const source = tool.source || "auto"
const source = resolveEffectiveSource(tool)
runPostInstallUpdates([{
source,
function: deriveFunctionName(tool, source),
@@ -1534,12 +1552,15 @@ export function Settings() {
onClick={() => {
const entries = proxmenuxTools
.filter(t => selectedUpdates.has(t.key))
.map(t => ({
source: t.source || 'auto',
function: deriveFunctionName(t, t.source || 'auto'),
key: t.key,
name: t.name,
}))
.map(t => {
const source = resolveEffectiveSource(t)
return {
source,
function: deriveFunctionName(t, source),
key: t.key,
name: t.name,
}
})
.filter(e => !!e.function)
setUpdateModalOpen(false)
setSelectedUpdates(new Set())
+2 -15
View File
@@ -3,7 +3,7 @@
import type React from "react"
import { useEffect, useRef, useState } from "react"
import { API_PORT, fetchApi } from "@/lib/api-config" // Unificando importaciones de api-config en una sola línea con alias @/
import { getTicketedWsUrl } from "@/lib/terminal-ws"
import { getTicketedWsUrl, getWsUrl } from "@/lib/terminal-ws"
import {
Activity,
Trash2,
@@ -51,20 +51,7 @@ interface TerminalInstance {
}
function getWebSocketUrl(): string {
if (typeof window === "undefined") {
return "ws://localhost:8008/ws/terminal"
}
const { protocol, hostname, port } = window.location
const isStandardPort = port === "" || port === "80" || port === "443"
const wsProtocol = protocol === "https:" ? "wss:" : "ws:"
if (isStandardPort) {
return `${wsProtocol}//${hostname}/ws/terminal`
} else {
return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal`
}
return getWsUrl("/ws/terminal")
}
function getApiUrl(endpoint?: string): string {
+79 -1
View File
@@ -11,7 +11,85 @@
* `_consume_terminal_ticket`, `_ws_auth_check`.
*/
import { fetchApi } from "@/lib/api-config"
import { fetchApi, getApiBaseUrl, API_PORT } from "@/lib/api-config"
/**
* Build a WebSocket URL for a given path (e.g. "/ws/terminal" or
* "/ws/script/<id>"). Centralizes the ws:// vs wss:// decision so a
* single fix benefits every terminal modal in the app.
*
* Why not just `window.location.protocol === "https:" ? "wss:" : "ws:"`?
* On iPad Safari (and some other mobile browsers) with a self-signed
* cert the user manually accepted, `location.protocol` can report
* "http:" even though the page was loaded over HTTPS — secure-context
* downgrade for untrusted certs. The frontend would then open ws://
* against the HTTPS endpoint; the server replies with SSL handshake
* errors and the client retries in a loop. We observed this tipping
* the gevent server into a 4.4 GB RSS spiral on .55 before systemd
* OOM-killed the AppImage.
*
* Resolution: prefer the protocol from the absolute API base URL
* (which is set up at app init by getApiBaseUrl and is always honest
* about ws/wss), only falling back to window.location.protocol when
* the API base is relative (i.e. behind a reverse proxy on a standard
* port — where the proxy decides the actual scheme anyway).
*/
export function getWsUrl(path: string): string {
const normalizedPath = path.startsWith("/") ? path : `/${path}`
if (typeof window === "undefined") {
return `ws://localhost:${API_PORT}${normalizedPath}`
}
// Multi-signal HTTPS detection — any single signal saying https
// wins. The deliberate bias toward https comes from how the two
// failure modes differ: wss:// against a plaintext server closes
// cleanly with one "WebSocket connection error", while ws://
// against an https server triggers the SSL-handshake loop that
// OOM-killed gevent on .55. Bias toward wss is the safer
// direction when in doubt.
//
// Signals:
// - getApiBaseUrl() absolute URL scheme (typically the most
// accurate, but it ultimately derives from
// window.location.protocol — included for completeness)
// - window.location.protocol (the obvious one — but iPad Safari
// with self-signed certs can report "http:" even when the page
// was loaded over HTTPS)
// - window.isSecureContext (true even when protocol misreports;
// the browser still treats the page as secure for crypto APIs)
// - document.URL / document.baseURI (the full URL the browser
// actually thinks it's at — last-resort cross-check)
const apiBase = getApiBaseUrl()
const docUrl =
typeof document !== "undefined"
? (document.URL || document.baseURI || "")
: ""
const isHttps =
apiBase.startsWith("https://") ||
window.location.protocol === "https:" ||
(typeof window.isSecureContext === "boolean" && window.isSecureContext) ||
docUrl.startsWith("https://")
const proto = isHttps ? "wss:" : "ws:"
// Pick the host:port to point the WebSocket at:
// - If apiBase is absolute, strip its scheme — that's where the
// REST API lives, so the WS endpoint lives there too.
// - Otherwise (proxy / standard port), reuse the current
// window.location.host so the proxy fronts both REST and WS.
let hostPort: string
if (apiBase.startsWith("https://")) {
hostPort = apiBase.slice("https://".length)
} else if (apiBase.startsWith("http://")) {
hostPort = apiBase.slice("http://".length)
} else {
hostPort = window.location.host
}
return `${proto}//${hostPort}${normalizedPath}`
}
type TicketResponse = {
success?: boolean
+14
View File
@@ -11789,6 +11789,20 @@ if __name__ == '__main__':
# `::` binds IPv6 + IPv4 (v4-mapped) on Linux when
# net.ipv6.bindv6only=0 (the default). Issue #192 — IPv4-only
# listening broke ProxMenux on dual-stack / v6-only hosts.
#
# NOTE: a prior version subclassed WSGIServer here to
# silence SSL handshake errors (memory leak workaround
# for clients opening ws:// against this https endpoint).
# The subclass interfered with gevent's SSL flow-control
# exceptions (SSLWantReadError) and caused
# ConcurrentObjectUseError on the wss handshake of
# /ws/script/<id>, which manifested only on clients that
# close the WS connection mid-handshake (iPad Safari for
# the updates modal). Since the root cause of the memory
# leak was fixed client-side in lib/terminal-ws.ts
# (getWsUrl now opens wss:// instead of ws:// against
# the https endpoint), the original SSL handshake errors
# are rare and the default gevent behaviour is fine.
server = pywsgi.WSGIServer(
('::', 8008),
app,
+11 -4
View File
@@ -39,13 +39,20 @@ _SAFE_ID_RE = re.compile(r'^[A-Za-z0-9_-]{1,64}$')
# atomically consumes the ticket — if the ticket is missing, expired, or
# already used, the WS is closed immediately.
#
# Tickets live in an in-memory dict guarded by a lock. TTL is intentionally
# short (5 s) — the client should issue and use the ticket immediately.
# See audit Tier 1 #2 + #17d.
# Tickets live in an in-memory dict guarded by a lock. The TTL is the
# window between POST /api/terminal/ticket and the WebSocket handshake
# that consumes it. The original 5 s was too tight for slower devices:
# on an iPad opening the post-install updates modal, xterm.js + the
# Nerd Font load took >5 s, the ticket expired before the wss handshake
# fired, and the modal hung at "Conectando" indefinitely — exactly the
# bug pattern that pushed the gevent server into the 4.4 GB OOM spiral.
# 60 s is wide enough to absorb mobile-rendering delays while still
# being one-shot (each ticket can only be consumed once), so the
# security model from audit Tier 1 #2 + #17d is unchanged.
_TERMINAL_TICKETS = {} # ticket (str) -> created_at_ts (float)
_TICKETS_LOCK = threading.Lock()
_TICKET_TTL = 5 # seconds
_TICKET_TTL = 60 # seconds
_TICKET_MAX_INFLIGHT = 256 # sanity cap to keep memory bounded