mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 16:49:39 +07:00
feat: add incus terminal
This commit is contained in:
parent
2adde048b0
commit
ab9b3368d1
@ -6,37 +6,40 @@ import InteractiveSession, {
|
|||||||
} from "@/components/containers/interactive-session";
|
} from "@/components/containers/interactive-session";
|
||||||
import PagerView from "@/components/ui/pager-view";
|
import PagerView from "@/components/ui/pager-view";
|
||||||
|
|
||||||
let nextSession = 1;
|
|
||||||
|
|
||||||
type Session = InteractiveSessionProps & { id: string };
|
type Session = InteractiveSessionProps & { id: string };
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const [sessions, setSessions] = useState<Session[]>([
|
const [sessions, setSessions] = useState<Session[]>([
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
type: "ssh",
|
type: "incus",
|
||||||
params: { serverId: "1" },
|
params: { client: "xtermjs", serverId: "1", shell: "bash" },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
type: "pve",
|
|
||||||
params: { client: "vnc", serverId: "2" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
type: "pve",
|
|
||||||
params: { client: "xtermjs", serverId: "3" },
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// id: "1",
|
||||||
|
// type: "ssh",
|
||||||
|
// params: { serverId: "1" },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "2",
|
||||||
|
// type: "pve",
|
||||||
|
// params: { client: "vnc", serverId: "2" },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "3",
|
||||||
|
// type: "pve",
|
||||||
|
// params: { client: "xtermjs", serverId: "3" },
|
||||||
|
// },
|
||||||
]);
|
]);
|
||||||
const [curSession, setSession] = useState(0);
|
const [curSession, setSession] = useState(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Stack.Screen options={{ title: "Home" }} />
|
<Stack.Screen options={{ title: "Home", headerShown: false }} />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
style={{ flexGrow: 0 }}
|
style={{ flexGrow: 0, backgroundColor: "#111" }}
|
||||||
contentContainerStyle={{ flexDirection: "row", gap: 8 }}
|
contentContainerStyle={{ flexDirection: "row", gap: 8 }}
|
||||||
>
|
>
|
||||||
{sessions.map((session, idx) => (
|
{sessions.map((session, idx) => (
|
||||||
@ -46,7 +49,7 @@ const HomePage = () => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
title={"Session " + session.id}
|
title={"Session " + session.id}
|
||||||
color="#222"
|
color="#333"
|
||||||
onPress={() => setSession(idx)}
|
onPress={() => setSession(idx)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -18,9 +18,22 @@ type PVESessionProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InteractiveSessionProps = SSHSessionProps | PVESessionProps;
|
type IncusSessionProps = {
|
||||||
|
type: "incus";
|
||||||
|
params: {
|
||||||
|
client: "vnc" | "xtermjs";
|
||||||
|
serverId: string;
|
||||||
|
shell?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveSessionProps =
|
||||||
|
| SSHSessionProps
|
||||||
|
| PVESessionProps
|
||||||
|
| IncusSessionProps;
|
||||||
|
|
||||||
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
||||||
|
let url = "";
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
@ -30,7 +43,15 @@ const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
|||||||
return <Terminal wsUrl={`${BASE_WS_URL}/ws/ssh?${query}`} />;
|
return <Terminal wsUrl={`${BASE_WS_URL}/ws/ssh?${query}`} />;
|
||||||
|
|
||||||
case "pve":
|
case "pve":
|
||||||
const url = `${BASE_WS_URL}/ws/pve?${query}`;
|
url = `${BASE_WS_URL}/ws/pve?${query}`;
|
||||||
|
return params.client === "vnc" ? (
|
||||||
|
<VNCViewer url={url} />
|
||||||
|
) : (
|
||||||
|
<Terminal wsUrl={url} />
|
||||||
|
);
|
||||||
|
|
||||||
|
case "incus":
|
||||||
|
url = `${BASE_WS_URL}/ws/incus?${query}`;
|
||||||
return params.client === "vnc" ? (
|
return params.client === "vnc" ? (
|
||||||
<VNCViewer url={url} />
|
<VNCViewer url={url} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -58,8 +58,10 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
|||||||
|
|
||||||
function resizeTerminal() {
|
function resizeTerminal() {
|
||||||
const { cols, rows } = xterm;
|
const { cols, rows } = xterm;
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
ws.send(`\x01${cols},${rows}`);
|
ws.send(`\x01${cols},${rows}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onResize() {
|
function onResize() {
|
||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
|
37
server/lib/crypto.go
Normal file
37
server/lib/crypto.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadClientCertificate(clientCert string, clientKey string) (*tls.Certificate, error) {
|
||||||
|
// Client certificate
|
||||||
|
ccb, _ := pem.Decode([]byte(clientCert))
|
||||||
|
if ccb == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse client certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(ccb.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse client certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client key
|
||||||
|
ckb, _ := pem.Decode([]byte(clientKey))
|
||||||
|
if ckb == nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse client key")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := x509.ParsePKCS8PrivateKey(ckb.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse client key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tls.Certificate{
|
||||||
|
Certificate: [][]byte{cert.Raw},
|
||||||
|
PrivateKey: key,
|
||||||
|
}, nil
|
||||||
|
}
|
114
server/lib/incus.go
Normal file
114
server/lib/incus.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IncusServer struct {
|
||||||
|
HostName string
|
||||||
|
Port int
|
||||||
|
ClientCert string
|
||||||
|
ClientKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncusFetchConfig struct {
|
||||||
|
Body map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IncusServer) GetCertificate() (*tls.Certificate, error) {
|
||||||
|
return LoadClientCertificate(i.ClientCert, i.ClientKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IncusServer) Fetch(method string, url string, cfg *IncusFetchConfig) ([]byte, error) {
|
||||||
|
var body io.Reader
|
||||||
|
if cfg != nil && cfg.Body != nil {
|
||||||
|
json, _ := json.Marshal(cfg.Body)
|
||||||
|
body = bytes.NewBuffer(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqUrl := fmt.Sprintf("https://%s:%d%s", i.HostName, i.Port, url)
|
||||||
|
req, _ := http.NewRequest(method, reqUrl, body)
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCert, err := i.GetCertificate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
Certificates: []tls.Certificate{*clientCert},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return nil, fmt.Errorf("request failed with status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncusInstanceExecRes struct {
|
||||||
|
ID string
|
||||||
|
Operation string
|
||||||
|
Control string
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IncusServer) InstanceExec(instance string, command []string, interactive bool) (*IncusInstanceExecRes, error) {
|
||||||
|
url := fmt.Sprintf("/1.0/instances/%s/exec?project=default", instance)
|
||||||
|
|
||||||
|
body, err := i.Fetch("POST", url, &IncusFetchConfig{
|
||||||
|
Body: map[string]interface{}{
|
||||||
|
"command": command,
|
||||||
|
"interactive": interactive,
|
||||||
|
"wait-for-websocket": true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res struct {
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
Metadata struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Metadata struct {
|
||||||
|
Fds map[string]string `json:"fds"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
} `json:"metadata"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &res); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
control := res.Metadata.Metadata.Fds["control"]
|
||||||
|
secret := res.Metadata.Metadata.Fds["0"]
|
||||||
|
|
||||||
|
return &IncusInstanceExecRes{
|
||||||
|
ID: res.Metadata.ID,
|
||||||
|
Operation: res.Operation,
|
||||||
|
Control: control,
|
||||||
|
Secret: secret,
|
||||||
|
}, nil
|
||||||
|
}
|
90
server/lib/incus_session.go
Normal file
90
server/lib/incus_session.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
fastWs "github.com/fasthttp/websocket"
|
||||||
|
"github.com/gofiber/contrib/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewIncusWebsocketSession(c *websocket.Conn, incus *IncusServer) error {
|
||||||
|
exec, err := incus.InstanceExec("test", []string{"/bin/sh"}, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCert, err := incus.GetCertificate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := fastWs.Dialer{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
Certificates: []tls.Certificate{*clientCert},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
controlUrl := fmt.Sprintf("wss://%s:%d%s/websocket?secret=%s", incus.HostName, incus.Port, exec.Operation, exec.Control)
|
||||||
|
controlWs, _, err := dialer.Dial(controlUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer controlWs.Close()
|
||||||
|
|
||||||
|
ttyUrl := fmt.Sprintf("wss://%s:%d%s/websocket?secret=%s", incus.HostName, incus.Port, exec.Operation, exec.Secret)
|
||||||
|
ttyWs, _, err := dialer.Dial(ttyUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ttyWs.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_, msg, err := c.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error reading from client:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(string(msg), "\x01") {
|
||||||
|
parts := strings.Split(string(msg[1:]), ",")
|
||||||
|
if len(parts) == 2 {
|
||||||
|
resizeCmd, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"command": "window-resize",
|
||||||
|
"args": map[string]string{
|
||||||
|
"width": parts[0],
|
||||||
|
"height": parts[1],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
controlWs.WriteMessage(websocket.BinaryMessage, resizeCmd)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ttyWs.WriteMessage(websocket.BinaryMessage, msg); err != nil {
|
||||||
|
log.Println("Error writing to Incus:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
t, msg, err := ttyWs.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error reading from Incus:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = c.WriteMessage(t, msg); err != nil {
|
||||||
|
log.Println("Error writing to client:", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -14,7 +14,7 @@ func main() {
|
|||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
|
|
||||||
var pve = &lib.PVEServer{
|
var pve = &lib.PVEServer{
|
||||||
HostName: "pve",
|
HostName: "10.0.0.1",
|
||||||
Port: 8006,
|
Port: 8006,
|
||||||
Username: os.Getenv("PVE_USERNAME"),
|
Username: os.Getenv("PVE_USERNAME"),
|
||||||
Password: os.Getenv("PVE_PASSWORD"),
|
Password: os.Getenv("PVE_PASSWORD"),
|
||||||
@ -75,5 +75,19 @@ func main() {
|
|||||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
app.Get("/ws/incus", websocket.New(func(c *websocket.Conn) {
|
||||||
|
incus := &lib.IncusServer{
|
||||||
|
HostName: "100.64.0.3",
|
||||||
|
Port: 8443,
|
||||||
|
ClientCert: "",
|
||||||
|
ClientKey: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := lib.NewIncusWebsocketSession(c, incus); err != nil {
|
||||||
|
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
app.Listen(":3000")
|
app.Listen(":3000")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user