mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-24 17:52:33 +00:00
Support private key
This commit is contained in:
@@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
type Target = {
|
type Target = {
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -15,6 +16,7 @@ type Target = {
|
|||||||
type FormState = {
|
type FormState = {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SshClient({
|
export default function SshClient({
|
||||||
@@ -26,9 +28,27 @@ export default function SshClient({
|
|||||||
}) {
|
}) {
|
||||||
const [form, setForm] = useState<FormState>({
|
const [form, setForm] = useState<FormState>({
|
||||||
username: "",
|
username: "",
|
||||||
password: ""
|
password: "",
|
||||||
|
privateKey: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
function handleKeyFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
const text = ev.target?.result;
|
||||||
|
if (typeof text === "string") {
|
||||||
|
setForm((prev) => ({ ...prev, privateKey: text }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
// Reset input so the same file can be re-selected if needed.
|
||||||
|
e.target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
const [connectError, setConnectError] = useState<string | null>(null);
|
const [connectError, setConnectError] = useState<string | null>(null);
|
||||||
@@ -143,9 +163,15 @@ export default function SshClient({
|
|||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
// Send the password (or empty string) as the first frame so the
|
// Send credentials as the first frame so the proxy can complete
|
||||||
// proxy can complete SSH authentication before piping pty data.
|
// SSH authentication before piping pty data.
|
||||||
ws.send(JSON.stringify({ type: "auth", password: form.password }));
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "auth",
|
||||||
|
password: form.password,
|
||||||
|
privateKey: form.privateKey
|
||||||
|
})
|
||||||
|
);
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
};
|
};
|
||||||
@@ -236,6 +262,60 @@ export default function SshClient({
|
|||||||
password: e.target.value
|
password: e.target.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
placeholder={
|
||||||
|
form.privateKey
|
||||||
|
? "Optional with key auth"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Private Key (optional)" id="privateKey">
|
||||||
|
<Textarea
|
||||||
|
id="privateKey"
|
||||||
|
value={form.privateKey}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
privateKey: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Paste your private key here (PEM format)…"
|
||||||
|
rows={5}
|
||||||
|
className="font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
fileInputRef.current?.click()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Upload key file
|
||||||
|
</Button>
|
||||||
|
{form.privateKey && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs text-muted-foreground underline"
|
||||||
|
onClick={() =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
privateKey: ""
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept=".pem,.key,.pub,*"
|
||||||
|
onChange={handleKeyFile}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -247,7 +327,11 @@ export default function SshClient({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={connect}
|
onClick={connect}
|
||||||
disabled={connecting || !form.username}
|
disabled={
|
||||||
|
connecting ||
|
||||||
|
!form.username ||
|
||||||
|
(!form.password && !form.privateKey)
|
||||||
|
}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{connecting ? "Connecting..." : "Connect"}
|
{connecting ? "Connecting..." : "Connect"}
|
||||||
|
|||||||
Reference in New Issue
Block a user