mirror of
https://github.com/khairul169/vaulterm.git
synced 2025-04-28 08:39:37 +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";
|
||||
import PagerView from "@/components/ui/pager-view";
|
||||
|
||||
let nextSession = 1;
|
||||
|
||||
type Session = InteractiveSessionProps & { id: string };
|
||||
|
||||
const HomePage = () => {
|
||||
const [sessions, setSessions] = useState<Session[]>([
|
||||
{
|
||||
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" },
|
||||
type: "incus",
|
||||
params: { client: "xtermjs", serverId: "1", shell: "bash" },
|
||||
},
|
||||
// {
|
||||
// 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);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Stack.Screen options={{ title: "Home" }} />
|
||||
<Stack.Screen options={{ title: "Home", headerShown: false }} />
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{ flexGrow: 0 }}
|
||||
style={{ flexGrow: 0, backgroundColor: "#111" }}
|
||||
contentContainerStyle={{ flexDirection: "row", gap: 8 }}
|
||||
>
|
||||
{sessions.map((session, idx) => (
|
||||
@ -46,7 +49,7 @@ const HomePage = () => {
|
||||
>
|
||||
<Button
|
||||
title={"Session " + session.id}
|
||||
color="#222"
|
||||
color="#333"
|
||||
onPress={() => setSession(idx)}
|
||||
/>
|
||||
<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) => {
|
||||
let url = "";
|
||||
const query = new URLSearchParams({
|
||||
...params,
|
||||
});
|
||||
@ -30,7 +43,15 @@ const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
||||
return <Terminal wsUrl={`${BASE_WS_URL}/ws/ssh?${query}`} />;
|
||||
|
||||
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" ? (
|
||||
<VNCViewer url={url} />
|
||||
) : (
|
||||
|
@ -58,7 +58,9 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
||||
|
||||
function resizeTerminal() {
|
||||
const { cols, rows } = xterm;
|
||||
ws.send(`\x01${cols},${rows}`);
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(`\x01${cols},${rows}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
|
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()
|
||||
|
||||
var pve = &lib.PVEServer{
|
||||
HostName: "pve",
|
||||
HostName: "10.0.0.1",
|
||||
Port: 8006,
|
||||
Username: os.Getenv("PVE_USERNAME"),
|
||||
Password: os.Getenv("PVE_PASSWORD"),
|
||||
@ -75,5 +75,19 @@ func main() {
|
||||
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")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user