v1.8.0 #429

Merged
LukeGus merged 198 commits from dev-1.8.0 into main 2025-11-05 16:36:16 +00:00
9 changed files with 320 additions and 329 deletions
Showing only changes of commit 7de387b987 - Show all commits

View File

@@ -34,7 +34,6 @@ http {
ssl_certificate_key ${SSL_KEY_PATH}; ssl_certificate_key ${SSL_KEY_PATH};
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always; add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
@@ -49,6 +48,15 @@ http {
log_not_found off; log_not_found off;
} }
location ~ ^/users/sessions(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/users(/.*)?$ { location ~ ^/users(/.*)?$ {
proxy_pass http://127.0.0.1:30001; proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -45,6 +45,15 @@ http {
log_not_found off; log_not_found off;
} }
location ~ ^/users/sessions(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ ^/users(/.*)?$ { location ~ ^/users(/.*)?$ {
proxy_pass http://127.0.0.1:30001; proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -62,10 +62,11 @@ function createWindow() {
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
webSecurity: true, webSecurity: false,
preload: path.join(__dirname, "preload.js"), preload: path.join(__dirname, "preload.js"),
partition: "persist:termix", partition: "persist:termix",
allowRunningInsecureContent: false, allowRunningInsecureContent: true,
webviewTag: true,
}, },
show: false, show: false,
}); });

View File

@@ -57,10 +57,6 @@ app.use(
"http://127.0.0.1:3000", "http://127.0.0.1:3000",
]; ];
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
if (origin.startsWith("https://")) { if (origin.startsWith("https://")) {
return callback(null, true); return callback(null, true);
} }
@@ -69,6 +65,10 @@ app.use(
return callback(null, true); return callback(null, true);
} }
if (allowedOrigins.includes(origin)) {
return callback(null, true);
}
callback(new Error("Not allowed by CORS")); callback(new Error("Not allowed by CORS"));
}, },
credentials: true, credentials: true,
@@ -172,6 +172,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
keyPassword, keyPassword,
authType, authType,
credentialId, credentialId,
userProvidedPassword,
} = req.body; } = req.body;
const userId = (req as AuthenticatedRequest).userId; const userId = (req as AuthenticatedRequest).userId;
@@ -264,44 +265,31 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
keepaliveCountMax: 3, keepaliveCountMax: 3,
algorithms: { algorithms: {
kex: [ kex: [
"curve25519-sha256",
"curve25519-sha256@libssh.org",
"ecdh-sha2-nistp521",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp256",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1", "diffie-hellman-group14-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group1-sha1", "diffie-hellman-group1-sha1",
], "diffie-hellman-group-exchange-sha256",
serverHostKey: [ "diffie-hellman-group-exchange-sha1",
"ssh-ed25519", "ecdh-sha2-nistp256",
"ecdsa-sha2-nistp521", "ecdh-sha2-nistp384",
"ecdsa-sha2-nistp384", "ecdh-sha2-nistp521",
"ecdsa-sha2-nistp256",
"rsa-sha2-512",
"rsa-sha2-256",
"ssh-rsa",
"ssh-dss",
], ],
cipher: [ cipher: [
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr", "aes128-ctr",
"aes256-cbc", "aes192-ctr",
"aes192-cbc", "aes256-ctr",
"aes128-gcm@openssh.com",
"aes256-gcm@openssh.com",
"aes128-cbc", "aes128-cbc",
"aes192-cbc",
"aes256-cbc",
"3des-cbc", "3des-cbc",
], ],
hmac: [ hmac: [
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512", "hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256", "hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1", "hmac-sha1",
"hmac-md5", "hmac-md5",
], ],
@@ -309,8 +297,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
}, },
}; };
let authMethodNotAvailable = false;
if ( if (
resolvedCredentials.authType === "key" && resolvedCredentials.authType === "key" &&
resolvedCredentials.sshKey && resolvedCredentials.sshKey &&
@@ -348,33 +334,11 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
.status(400) .status(400)
.json({ error: "Password required for password authentication" }); .json({ error: "Password required for password authentication" });
} }
config.password = resolvedCredentials.password;
if (userProvidedPassword) {
config.password = resolvedCredentials.password;
}
} else if (resolvedCredentials.authType === "none") { } else if (resolvedCredentials.authType === "none") {
config.authHandler = (
methodsLeft: string[] | null,
partialSuccess: boolean,
callback: (nextMethod: string | false) => void,
) => {
if (methodsLeft && methodsLeft.length > 0) {
if (methodsLeft.includes("keyboard-interactive")) {
callback("keyboard-interactive");
} else {
authMethodNotAvailable = true;
fileLogger.error(
"Server does not support keyboard-interactive auth",
{
operation: "ssh_auth_handler_no_keyboard",
hostId,
sessionId,
methodsAvailable: methodsLeft,
},
);
callback(false);
}
} else {
callback(false);
}
};
} else { } else {
fileLogger.warn( fileLogger.warn(
"No valid authentication method provided for file manager", "No valid authentication method provided for file manager",
@@ -451,36 +415,26 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
client.on("error", (err) => { client.on("error", (err) => {
if (responseSent) return; if (responseSent) return;
responseSent = true; responseSent = true;
fileLogger.error("SSH connection failed for file manager", {
operation: "file_connect",
sessionId,
hostId,
ip,
port,
username,
error: err.message,
});
if (authMethodNotAvailable && resolvedCredentials.authType === "none") { if (
res.status(200).json({
status: "auth_required",
message:
"The server does not support keyboard-interactive authentication. Please provide credentials.",
reason: "no_keyboard",
});
} else if (
resolvedCredentials.authType === "none" && resolvedCredentials.authType === "none" &&
(err.message.includes("All configured authentication methods failed") || (err.message.includes("authentication") ||
err.message.includes("No supported authentication methods available") || err.message.includes("All configured authentication methods failed"))
err.message.includes("authentication methods failed"))
) { ) {
res.status(200).json({ res.json({
status: "auth_required", status: "auth_required",
message:
"The server does not support keyboard-interactive authentication. Please provide credentials.",
reason: "no_keyboard", reason: "no_keyboard",
}); });
} else { } else {
fileLogger.error("SSH connection failed for file manager", {
operation: "file_connect",
sessionId,
hostId,
ip,
port,
username,
error: err.message,
});
res.status(500).json({ status: "error", message: err.message }); res.status(500).json({ status: "error", message: err.message });
} }
}); });
@@ -564,13 +518,43 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
/password/i.test(p.prompt), /password/i.test(p.prompt),
); );
if (
resolvedCredentials.authType === "none" &&
passwordPromptIndex !== -1
) {
if (responseSent) return;
responseSent = true;
client.end();
res.json({
status: "auth_required",
reason: "no_keyboard",
});
return;
}
if (!hasStoredPassword && passwordPromptIndex !== -1) { if (!hasStoredPassword && passwordPromptIndex !== -1) {
if (responseSent) { if (responseSent) {
const responses = prompts.map((p) => {
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
return resolvedCredentials.password;
}
return "";
});
finish(responses);
return; return;
} }
responseSent = true; responseSent = true;
if (pendingTOTPSessions[sessionId]) { if (pendingTOTPSessions[sessionId]) {
const responses = prompts.map((p) => {
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
return resolvedCredentials.password;
}
return "";
});
finish(responses);
return; return;
} }
@@ -2446,6 +2430,15 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
: code; : code;
const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim(); const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim();
fileLogger.info("File execution completed", {
operation: "execute_file",
sessionId,
filePath,
exitCode: actualExitCode,
outputLength: cleanOutput.length,
errorLength: errorOutput.length,
});
res.json({ res.json({
success: true, success: true,
exitCode: actualExitCode, exitCode: actualExitCode,

View File

@@ -64,47 +64,21 @@ const wss = new WebSocketServer({
const token = url.query.token as string; const token = url.query.token as string;
if (!token) { if (!token) {
sshLogger.warn("WebSocket connection rejected: missing token", {
operation: "websocket_auth_reject",
reason: "missing_token",
ip: info.req.socket.remoteAddress,
});
return false; return false;
} }
const payload = await authManager.verifyJWTToken(token); const payload = await authManager.verifyJWTToken(token);
if (!payload) { if (!payload) {
sshLogger.warn("WebSocket connection rejected: invalid token", {
operation: "websocket_auth_reject",
reason: "invalid_token",
ip: info.req.socket.remoteAddress,
});
return false; return false;
} }
if (payload.pendingTOTP) { if (payload.pendingTOTP) {
sshLogger.warn(
"WebSocket connection rejected: TOTP verification pending",
{
operation: "websocket_auth_reject",
reason: "totp_pending",
userId: payload.userId,
ip: info.req.socket.remoteAddress,
},
);
return false; return false;
} }
const existingConnections = userConnections.get(payload.userId); const existingConnections = userConnections.get(payload.userId);
if (existingConnections && existingConnections.size >= 3) { if (existingConnections && existingConnections.size >= 3) {
sshLogger.warn("WebSocket connection rejected: too many connections", {
operation: "websocket_auth_reject",
reason: "connection_limit",
userId: payload.userId,
currentConnections: existingConnections.size,
ip: info.req.socket.remoteAddress,
});
return false; return false;
} }
@@ -127,28 +101,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
const token = url.query.token as string; const token = url.query.token as string;
if (!token) { if (!token) {
sshLogger.warn(
"WebSocket connection rejected: missing token in connection",
{
operation: "websocket_connection_reject",
reason: "missing_token",
ip: req.socket.remoteAddress,
},
);
ws.close(1008, "Authentication required"); ws.close(1008, "Authentication required");
return; return;
} }
const payload = await authManager.verifyJWTToken(token); const payload = await authManager.verifyJWTToken(token);
if (!payload) { if (!payload) {
sshLogger.warn(
"WebSocket connection rejected: invalid token in connection",
{
operation: "websocket_connection_reject",
reason: "invalid_token",
ip: req.socket.remoteAddress,
},
);
ws.close(1008, "Authentication required"); ws.close(1008, "Authentication required");
return; return;
} }
@@ -169,11 +127,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
const dataKey = userCrypto.getUserDataKey(userId); const dataKey = userCrypto.getUserDataKey(userId);
if (!dataKey) { if (!dataKey) {
sshLogger.warn("WebSocket connection rejected: data locked", {
operation: "websocket_data_locked",
userId,
ip: req.socket.remoteAddress,
});
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "error", type: "error",
@@ -213,11 +166,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
ws.on("message", (msg: RawData) => { ws.on("message", (msg: RawData) => {
const currentDataKey = userCrypto.getUserDataKey(userId); const currentDataKey = userCrypto.getUserDataKey(userId);
if (!currentDataKey) { if (!currentDataKey) {
sshLogger.warn("WebSocket message rejected: data access expired", {
operation: "websocket_message_rejected",
userId,
reason: "data_access_expired",
});
ws.send( ws.send(
JSON.stringify({ JSON.stringify({
type: "error", type: "error",
@@ -371,6 +319,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
if (credentialsData.password) { if (credentialsData.password) {
credentialsData.hostConfig.password = credentialsData.password; credentialsData.hostConfig.password = credentialsData.password;
credentialsData.hostConfig.authType = "password"; credentialsData.hostConfig.authType = "password";
(credentialsData.hostConfig as any).userProvidedPassword = true;
} else if (credentialsData.sshKey) { } else if (credentialsData.sshKey) {
credentialsData.hostConfig.key = credentialsData.sshKey; credentialsData.hostConfig.key = credentialsData.sshKey;
credentialsData.hostConfig.keyPassword = credentialsData.keyPassword; credentialsData.hostConfig.keyPassword = credentialsData.keyPassword;
@@ -776,13 +725,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.on("close", () => { sshConn.on("close", () => {
clearTimeout(connectionTimeout); clearTimeout(connectionTimeout);
sshLogger.warn("SSH connection closed by server", {
operation: "ssh_close",
hostId: id,
ip,
port,
hadStream: !!sshStream,
});
cleanupSSH(connectionTimeout); cleanupSSH(connectionTimeout);
}); });
@@ -795,15 +737,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
prompts: Array<{ prompt: string; echo: boolean }>, prompts: Array<{ prompt: string; echo: boolean }>,
finish: (responses: string[]) => void, finish: (responses: string[]) => void,
) => { ) => {
if (resolvedCredentials.authType === "none") {
ws.send(
JSON.stringify({
type: "keyboard_interactive_available",
message: "Keyboard-interactive authentication is available",
}),
);
}
const promptTexts = prompts.map((p) => p.prompt); const promptTexts = prompts.map((p) => p.prompt);
const totpPromptIndex = prompts.findIndex((p) => const totpPromptIndex = prompts.findIndex((p) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test( /verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
@@ -854,7 +787,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
); );
if (!hasStoredPassword && passwordPromptIndex !== -1) { if (!hasStoredPassword && passwordPromptIndex !== -1) {
if (keyboardInteractiveResponded && totpPromptSent) { if (keyboardInteractiveResponded) {
return; return;
} }
keyboardInteractiveResponded = true; keyboardInteractiveResponded = true;
@@ -898,7 +831,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
host: ip, host: ip,
port, port,
username, username,
tryKeyboard: resolvedCredentials.authType === "none", tryKeyboard: true,
keepaliveInterval: 30000, keepaliveInterval: 30000,
keepaliveCountMax: 3, keepaliveCountMax: 3,
readyTimeout: 60000, readyTimeout: 60000,
@@ -964,22 +897,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
}; };
if (resolvedCredentials.authType === "none") { if (resolvedCredentials.authType === "none") {
connectConfig.authHandler = (
methodsLeft: string[] | null,
partialSuccess: boolean,
callback: (nextMethod: string | false) => void,
) => {
if (methodsLeft && methodsLeft.length > 0) {
if (methodsLeft.includes("keyboard-interactive")) {
callback("keyboard-interactive");
} else {
authMethodNotAvailable = true;
callback(false);
}
} else {
callback(false);
}
};
} else if (resolvedCredentials.authType === "password") { } else if (resolvedCredentials.authType === "password") {
if (!resolvedCredentials.password) { if (!resolvedCredentials.password) {
sshLogger.error( sshLogger.error(
@@ -994,7 +911,10 @@ wss.on("connection", async (ws: WebSocket, req) => {
); );
return; return;
} }
connectConfig.password = resolvedCredentials.password;
if ((hostConfig as any).userProvidedPassword) {
connectConfig.password = resolvedCredentials.password;
}
} else if ( } else if (
resolvedCredentials.authType === "key" && resolvedCredentials.authType === "key" &&
resolvedCredentials.key resolvedCredentials.key

View File

@@ -29,7 +29,6 @@ function AppContent() {
setAuthLoading(true); setAuthLoading(true);
getUserInfo() getUserInfo()
.then((meRes) => { .then((meRes) => {
// Check if response is actually HTML (Vite dev server page)
if (typeof meRes === "string" || !meRes.username) { if (typeof meRes === "string" || !meRes.username) {
setIsAuthenticated(false); setIsAuthenticated(false);
setIsAdmin(false); setIsAdmin(false);

View File

@@ -1344,6 +1344,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
authType: credentials.password ? "password" : "key", authType: credentials.password ? "password" : "key",
credentialId: currentHost.credentialId, credentialId: currentHost.credentialId,
userId: currentHost.userId, userId: currentHost.userId,
userProvidedPassword: true,
}); });
if (result?.requires_totp) { if (result?.requires_totp) {

View File

@@ -5,6 +5,22 @@ import { useTranslation } from "react-i18next";
import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react"; import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react";
import { getCookie, getUserInfo } from "@/ui/main-axios.ts"; import { getCookie, getUserInfo } from "@/ui/main-axios.ts";
declare global {
namespace JSX {
interface IntrinsicElements {
webview: React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
> & {
src?: string;
partition?: string;
allowpopups?: string;
ref?: React.Ref<any>;
};
}
}
}
interface ElectronLoginFormProps { interface ElectronLoginFormProps {
serverUrl: string; serverUrl: string;
onAuthSuccess: () => void; onAuthSuccess: () => void;
@@ -20,10 +36,12 @@ export function ElectronLoginForm({
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isAuthenticating, setIsAuthenticating] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(false);
const iframeRef = useRef<HTMLIFrameElement>(null); const webviewRef = useRef<any>(null);
const hasAuthenticatedRef = useRef(false); const hasAuthenticatedRef = useRef(false);
const [currentUrl, setCurrentUrl] = useState(serverUrl); const [currentUrl, setCurrentUrl] = useState(serverUrl);
const hasLoadedOnce = useRef(false); const hasLoadedOnce = useRef(false);
const urlCheckInterval = useRef<NodeJS.Timeout | null>(null);
const loadTimeout = useRef<NodeJS.Timeout | null>(null);
useEffect(() => { useEffect(() => {
const handleMessage = async (event: MessageEvent) => { const handleMessage = async (event: MessageEvent) => {
@@ -57,7 +75,17 @@ export function ElectronLoginForm({
await getUserInfo(); await getUserInfo();
} catch (verifyErr) { } catch (verifyErr) {
localStorage.removeItem("jwt"); localStorage.removeItem("jwt");
throw new Error("Invalid or expired authentication token"); const errorMsg =
verifyErr instanceof Error
? verifyErr.message
: "Failed to verify authentication";
console.error("Authentication verification failed:", verifyErr);
throw new Error(
errorMsg.includes("registration") ||
errorMsg.includes("allowed")
? "Authentication failed. Please check your server connection and try again."
: errorMsg,
);
} }
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
@@ -85,159 +113,186 @@ export function ElectronLoginForm({
}, [serverUrl, isAuthenticating, onAuthSuccess, t]); }, [serverUrl, isAuthenticating, onAuthSuccess, t]);
useEffect(() => { useEffect(() => {
const iframe = iframeRef.current; const checkWebviewUrl = () => {
if (!iframe) return; const webview = webviewRef.current;
if (!webview) return;
try {
const webviewUrl = webview.getURL();
if (webviewUrl && webviewUrl !== currentUrl) {
setCurrentUrl(webviewUrl);
}
} catch (e) {}
};
urlCheckInterval.current = setInterval(checkWebviewUrl, 500);
return () => {
if (urlCheckInterval.current) {
clearInterval(urlCheckInterval.current);
urlCheckInterval.current = null;
}
};
}, [currentUrl]);
useEffect(() => {
const webview = webviewRef.current;
if (!webview) return;
loadTimeout.current = setTimeout(() => {
if (!hasLoadedOnce.current && loading) {
setLoading(false);
setError(
"Unable to connect to server. Please check the server URL and try again.",
);
}
}, 15000);
const handleLoad = () => { const handleLoad = () => {
if (loadTimeout.current) {
clearTimeout(loadTimeout.current);
loadTimeout.current = null;
}
setLoading(false); setLoading(false);
hasLoadedOnce.current = true; hasLoadedOnce.current = true;
setError(null); setError(null);
try { try {
if (iframe.contentWindow) { const webviewUrl = webview.getURL();
setCurrentUrl(iframe.contentWindow.location.href); setCurrentUrl(webviewUrl || serverUrl);
}
} catch (e) { } catch (e) {
setCurrentUrl(serverUrl); setCurrentUrl(serverUrl);
} }
try { const injectedScript = `
const injectedScript = ` (function() {
(function() { window.IS_ELECTRON = true;
window.IS_ELECTRON = true; if (typeof window.electronAPI === 'undefined') {
if (typeof window.electronAPI === 'undefined') { window.electronAPI = { isElectron: true };
window.electronAPI = { isElectron: true }; }
let hasNotified = false;
function postJWTToParent(token, source) {
if (hasNotified) {
return;
} }
hasNotified = true;
let hasNotified = false;
function postJWTToParent(token, source) {
if (hasNotified) {
return;
}
hasNotified = true;
try {
window.parent.postMessage({
type: 'AUTH_SUCCESS',
token: token,
source: source,
platform: 'desktop',
timestamp: Date.now()
}, '*');
} catch (e) {
}
}
function clearAuthData() {
try {
localStorage.removeItem('jwt');
sessionStorage.removeItem('jwt');
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
if (name === 'jwt') {
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=' + window.location.hostname;
}
}
} catch (error) {
}
}
window.addEventListener('message', function(event) {
try {
if (event.data && typeof event.data === 'object') {
if (event.data.type === 'CLEAR_AUTH_DATA') {
clearAuthData();
}
}
} catch (error) {
}
});
function checkAuth() {
try {
const localToken = localStorage.getItem('jwt');
if (localToken && localToken.length > 20) {
postJWTToParent(localToken, 'localStorage');
return true;
}
const sessionToken = sessionStorage.getItem('jwt');
if (sessionToken && sessionToken.length > 20) {
postJWTToParent(sessionToken, 'sessionStorage');
return true;
}
const cookies = document.cookie;
if (cookies && cookies.length > 0) {
const cookieArray = cookies.split('; ');
const tokenCookie = cookieArray.find(row => row.startsWith('jwt='));
if (tokenCookie) {
const token = tokenCookie.split('=')[1];
if (token && token.length > 20) {
postJWTToParent(token, 'cookie');
return true;
}
}
}
} catch (error) {
}
return false;
}
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
originalSetItem.apply(this, arguments);
if (key === 'jwt' && value && value.length > 20 && !hasNotified) {
setTimeout(() => checkAuth(), 100);
}
};
const originalSessionSetItem = sessionStorage.setItem;
sessionStorage.setItem = function(key, value) {
originalSessionSetItem.apply(this, arguments);
if (key === 'jwt' && value && value.length > 20 && !hasNotified) {
setTimeout(() => checkAuth(), 100);
}
};
const intervalId = setInterval(() => {
if (hasNotified) {
clearInterval(intervalId);
return;
}
if (checkAuth()) {
clearInterval(intervalId);
}
}, 500);
setTimeout(() => {
clearInterval(intervalId);
}, 300000);
setTimeout(() => checkAuth(), 500);
})();
`;
try {
if (iframe.contentWindow) {
try { try {
iframe.contentWindow.eval(injectedScript); window.parent.postMessage({
} catch (evalError) { type: 'AUTH_SUCCESS',
iframe.contentWindow.postMessage( token: token,
{ type: "INJECT_SCRIPT", script: injectedScript }, source: source,
"*", platform: 'desktop',
); timestamp: Date.now()
}, '*');
} catch (e) {
} }
} }
} catch (err) {}
} catch (err) {} function clearAuthData() {
try {
localStorage.removeItem('jwt');
sessionStorage.removeItem('jwt');
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substr(0, eqPos).trim() : cookie.trim();
if (name === 'jwt') {
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=' + window.location.hostname;
}
}
} catch (error) {
}
}
window.addEventListener('message', function(event) {
try {
if (event.data && typeof event.data === 'object') {
if (event.data.type === 'CLEAR_AUTH_DATA') {
clearAuthData();
}
}
} catch (error) {
}
});
function checkAuth() {
try {
const localToken = localStorage.getItem('jwt');
if (localToken && localToken.length > 20) {
postJWTToParent(localToken, 'localStorage');
return true;
}
const sessionToken = sessionStorage.getItem('jwt');
if (sessionToken && sessionToken.length > 20) {
postJWTToParent(sessionToken, 'sessionStorage');
return true;
}
const cookies = document.cookie;
if (cookies && cookies.length > 0) {
const cookieArray = cookies.split('; ');
const tokenCookie = cookieArray.find(row => row.startsWith('jwt='));
if (tokenCookie) {
const token = tokenCookie.split('=')[1];
if (token && token.length > 20) {
postJWTToParent(token, 'cookie');
return true;
}
}
}
} catch (error) {
}
return false;
}
const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
originalSetItem.apply(this, arguments);
if (key === 'jwt' && value && value.length > 20 && !hasNotified) {
setTimeout(() => checkAuth(), 100);
}
};
const originalSessionSetItem = sessionStorage.setItem;
sessionStorage.setItem = function(key, value) {
originalSessionSetItem.apply(this, arguments);
if (key === 'jwt' && value && value.length > 20 && !hasNotified) {
setTimeout(() => checkAuth(), 100);
}
};
const intervalId = setInterval(() => {
if (hasNotified) {
clearInterval(intervalId);
return;
}
if (checkAuth()) {
clearInterval(intervalId);
}
}, 500);
setTimeout(() => {
clearInterval(intervalId);
}, 300000);
setTimeout(() => checkAuth(), 500);
})();
`;
try {
webview.executeJavaScript(injectedScript);
} catch (err) {
console.error("Failed to inject authentication script:", err);
}
}; };
const handleError = () => { const handleError = () => {
@@ -247,18 +302,27 @@ export function ElectronLoginForm({
} }
}; };
iframe.addEventListener("load", handleLoad); webview.addEventListener("did-finish-load", handleLoad);
iframe.addEventListener("error", handleError); webview.addEventListener("did-fail-load", handleError);
return () => { return () => {
iframe.removeEventListener("load", handleLoad); webview.removeEventListener("did-finish-load", handleLoad);
iframe.removeEventListener("error", handleError); webview.removeEventListener("did-fail-load", handleError);
if (loadTimeout.current) {
clearTimeout(loadTimeout.current);
loadTimeout.current = null;
}
}; };
}, [t]); }, [t, loading, serverUrl]);
const handleRefresh = () => { const handleRefresh = () => {
if (iframeRef.current) { if (webviewRef.current) {
iframeRef.current.src = serverUrl; if (loadTimeout.current) {
clearTimeout(loadTimeout.current);
loadTimeout.current = null;
}
webviewRef.current.src = serverUrl;
setLoading(true); setLoading(true);
setError(null); setError(null);
} }
@@ -336,14 +400,13 @@ export function ElectronLoginForm({
)} )}
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<iframe <webview
ref={iframeRef} ref={webviewRef}
src={serverUrl} src={serverUrl}
className="w-full h-full border-0" className="w-full h-full border-0"
title="Server Authentication" partition="persist:termix"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-storage-access-by-user-activation allow-top-navigation allow-top-navigation-by-user-activation allow-modals allow-downloads" allowpopups="false"
allow="clipboard-read; clipboard-write; cross-origin-isolated; camera; microphone; geolocation; storage-access" style={{ width: "100%", height: "100%" }}
credentialless={false}
/> />
</div> </div>
</div> </div>

View File

@@ -136,11 +136,8 @@ export function SSHAuthDialog({
: `${hostInfo.username}@${hostInfo.ip}:${hostInfo.port}`; : `${hostInfo.username}@${hostInfo.ip}:${hostInfo.port}`;
return ( return (
<div <div className="absolute inset-0 z-50 flex items-center justify-center bg-dark-bg">
className="absolute inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" <Card className="w-full max-w-2xl mx-4 border-2">
style={{ backgroundColor: `${backgroundColor}dd` }}
>
<Card className="w-full max-w-2xl mx-4 shadow-2xl">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" /> <Shield className="w-5 h-5" />