mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-30 07:51:53 +02:00
Compare commits
3 Commits
6df4bba3b6
...
0943cf5d4c
| Author | SHA1 | Date | |
|---|---|---|---|
| 0943cf5d4c | |||
| 3b82ac568f | |||
| b695f34dc8 |
@@ -61,6 +61,7 @@ export type VerifyResourceSessionSchema = z.infer<
|
||||
>;
|
||||
|
||||
type BasicUserData = {
|
||||
dontStripSession?: boolean;
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
@@ -74,6 +75,7 @@ export type VerifyUserResponse = {
|
||||
redirectUrl?: string;
|
||||
userData?: BasicUserData;
|
||||
pangolinVersion?: string;
|
||||
dontStripSession?: boolean;
|
||||
};
|
||||
|
||||
export async function verifyResourceSession(
|
||||
@@ -191,7 +193,8 @@ export async function verifyResourceSession(
|
||||
return notAllowed(res);
|
||||
}
|
||||
|
||||
const { sso, blockAccess } = resource;
|
||||
const { sso, blockAccess, mode } = resource;
|
||||
const dontStripSession = ["ssh", "rdp", "vnc"].includes(mode);
|
||||
|
||||
if (blockAccess) {
|
||||
logger.debug("Resource blocked", host);
|
||||
@@ -234,7 +237,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
} else if (action == "DROP") {
|
||||
logger.debug("Resource denied by rule");
|
||||
|
||||
@@ -282,7 +285,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
|
||||
const redirectPath = `/auth/resource/${encodeURIComponent(
|
||||
@@ -348,7 +351,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +402,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +425,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
} else if (
|
||||
await verifyPassword(
|
||||
clientHeaderAuth,
|
||||
@@ -443,7 +446,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -590,7 +593,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
|
||||
if (password && resourceSession.passwordId) {
|
||||
@@ -609,7 +612,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -631,7 +634,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
|
||||
if (resourceSession.accessTokenId) {
|
||||
@@ -654,7 +657,7 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res);
|
||||
return allowed(res, undefined, dontStripSession);
|
||||
}
|
||||
|
||||
if (resourceSession.userSessionId && sso) {
|
||||
@@ -699,7 +702,11 @@ export async function verifyResourceSession(
|
||||
parsedBody.data
|
||||
);
|
||||
|
||||
return allowed(res, allowedUserData);
|
||||
return allowed(
|
||||
res,
|
||||
{ ...allowedUserData, dontStripSession },
|
||||
dontStripSession
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -832,16 +839,23 @@ async function notAllowed(
|
||||
message: "Access denied",
|
||||
status: HttpCode.OK
|
||||
};
|
||||
logger.debug(JSON.stringify(data));
|
||||
// logger.debug(JSON.stringify(data));
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
function allowed(res: Response, userData?: BasicUserData) {
|
||||
function allowed(
|
||||
res: Response,
|
||||
userData?: BasicUserData,
|
||||
dontStripSession?: boolean
|
||||
) {
|
||||
const baseData =
|
||||
userData !== undefined && userData !== null
|
||||
? { valid: true, ...userData, pangolinVersion: APP_VERSION }
|
||||
: { valid: true, pangolinVersion: APP_VERSION };
|
||||
const data = {
|
||||
data:
|
||||
userData !== undefined && userData !== null
|
||||
? { valid: true, ...userData, pangolinVersion: APP_VERSION }
|
||||
: { valid: true, pangolinVersion: APP_VERSION },
|
||||
data: dontStripSession
|
||||
? { ...baseData, dontStripSession: true }
|
||||
: baseData,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access allowed",
|
||||
@@ -894,7 +908,7 @@ async function headerAuthChallenged(
|
||||
message: "Access denied",
|
||||
status: HttpCode.OK
|
||||
};
|
||||
logger.debug(JSON.stringify(data));
|
||||
// logger.debug(JSON.stringify(data));
|
||||
return response<VerifyUserResponse>(res, data);
|
||||
}
|
||||
|
||||
|
||||
+59
-13
@@ -199,17 +199,30 @@ export default function SshClient({
|
||||
|
||||
const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`;
|
||||
const url = new URL(proxyAddress);
|
||||
url.searchParams.set("host", target.ip ?? "");
|
||||
url.searchParams.set("port", String(target.port ?? 22));
|
||||
url.searchParams.set(
|
||||
"mode",
|
||||
target.authDaemonMode === "native" ? "native" : "proxy"
|
||||
);
|
||||
if (target.authDaemonMode !== "native") {
|
||||
url.searchParams.set("host", target.ip ?? "");
|
||||
url.searchParams.set("port", String(target.port ?? 22));
|
||||
}
|
||||
url.searchParams.set("username", username);
|
||||
url.searchParams.set("authToken", target.authToken ?? "");
|
||||
|
||||
const ws = new WebSocket(url.toString(), ["ssh"]);
|
||||
wsRef.current = ws;
|
||||
|
||||
// Track whether the server has confirmed auth by sending the first
|
||||
// data frame. Until then, errors are shown in the login form.
|
||||
let authConfirmed = false;
|
||||
let authErrorShown = false;
|
||||
|
||||
ws.onopen = () => {
|
||||
// Send credentials as the first frame so the proxy can complete
|
||||
// SSH authentication before piping pty data.
|
||||
// SSH authentication before piping pty data. Stay in "connecting"
|
||||
// state until the server responds — this prevents the flash to the
|
||||
// terminal page that would occur if we set connected=true here.
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "auth",
|
||||
@@ -225,8 +238,6 @@ export default function SshClient({
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
};
|
||||
|
||||
ws.onmessage = (evt) => {
|
||||
@@ -238,17 +249,43 @@ export default function SshClient({
|
||||
error?: string;
|
||||
};
|
||||
if (msg.type === "data" && msg.data) {
|
||||
if (!authConfirmed) {
|
||||
authConfirmed = true;
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
}
|
||||
xtermRef.current?.write(msg.data);
|
||||
} else if (msg.type === "error") {
|
||||
xtermRef.current?.writeln(
|
||||
`\r\n\x1b[31mError: ${msg.error}\x1b[0m\r\n`
|
||||
);
|
||||
if (!authConfirmed) {
|
||||
// Auth-phase error — show in the login form.
|
||||
authErrorShown = true;
|
||||
setConnecting(false);
|
||||
setConnectError(
|
||||
msg.error ?? "Authentication failed"
|
||||
);
|
||||
} else {
|
||||
xtermRef.current?.writeln(
|
||||
`\r\n\x1b[31mError: ${msg.error}\x1b[0m\r\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (!authConfirmed) {
|
||||
authConfirmed = true;
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
}
|
||||
xtermRef.current?.write(evt.data);
|
||||
}
|
||||
} else if (evt.data instanceof Blob) {
|
||||
evt.data.text().then((t) => xtermRef.current?.write(t));
|
||||
evt.data.text().then((t) => {
|
||||
if (!authConfirmed) {
|
||||
authConfirmed = true;
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
}
|
||||
xtermRef.current?.write(t);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -260,10 +297,19 @@ export default function SshClient({
|
||||
|
||||
ws.onclose = (evt) => {
|
||||
setConnecting(false);
|
||||
setConnected(false);
|
||||
xtermRef.current?.writeln(
|
||||
`\r\n\x1b[33mConnection closed (code ${evt.code})\x1b[0m\r\n`
|
||||
);
|
||||
if (authConfirmed) {
|
||||
setConnected(false);
|
||||
xtermRef.current?.writeln(
|
||||
`\r\n\x1b[33mConnection closed (code ${evt.code})\x1b[0m\r\n`
|
||||
);
|
||||
}
|
||||
// If auth was never confirmed the login form is already visible;
|
||||
// a generic error is shown only when no specific error was received.
|
||||
if (!authConfirmed && !authErrorShown) {
|
||||
setConnectError(
|
||||
"Connection closed before authentication completed"
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user