mirror of
https://github.com/khairul169/vaulterm.git
synced 2026-09-15 17:03:30 +07:00
feat: init api, db, app
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func LoadClientCertificate(clientCert string, clientKey string) (*tls.Certificate, error) {
|
||||
@@ -35,3 +44,93 @@ func LoadClientCertificate(clientCert string, clientKey string) (*tls.Certificat
|
||||
PrivateKey: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func VerifyPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GenerateSessionID(size int) (string, error) {
|
||||
sessionID := make([]byte, size)
|
||||
|
||||
// Read random bytes into sessionID
|
||||
_, err := rand.Read(sessionID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Encode as hex string
|
||||
return hex.EncodeToString(sessionID), nil
|
||||
}
|
||||
|
||||
func Encrypt(data string) (string, error) {
|
||||
key := os.Getenv("ENCRYPTION_KEY")
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("ENCRYPTION_KEY is not set")
|
||||
}
|
||||
|
||||
keyDec, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(keyDec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ciphertext := aesGCM.Seal(nonce, nonce, []byte(data), nil)
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func Decrypt(encrypted string) (string, error) {
|
||||
key := os.Getenv("ENCRYPTION_KEY")
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("ENCRYPTION_KEY is not set")
|
||||
}
|
||||
|
||||
keyDec, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(encrypted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(keyDec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonceSize := aesGCM.NonceSize()
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
|
||||
res, err := aesGCM.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(res), nil
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ func (i *IncusServer) InstanceExec(instance string, command []string, interactiv
|
||||
"command": command,
|
||||
"interactive": interactive,
|
||||
"wait-for-websocket": true,
|
||||
"environment": map[string]string{
|
||||
"TERM": "xterm-256color",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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
|
||||
}
|
||||
+4
-3
@@ -70,6 +70,7 @@ type PVEAccessTicket struct {
|
||||
func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
|
||||
url := fmt.Sprintf("https://%s:%d/api2/json/access/ticket", pve.HostName, pve.Port)
|
||||
|
||||
// note for myself: don't forget the realm
|
||||
body, err := fetch("POST", url, &PVERequestInit{Body: map[string]string{
|
||||
"username": pve.Username,
|
||||
"password": pve.Password,
|
||||
@@ -89,9 +90,9 @@ func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
|
||||
}
|
||||
|
||||
type PVEInstance struct {
|
||||
Type string // "qemu" | "lxc"
|
||||
Node string
|
||||
VMID string
|
||||
Type string `json:"type"` // "qemu" | "lxc"
|
||||
Node string `json:"node"`
|
||||
VMID string `json:"vmid"`
|
||||
}
|
||||
|
||||
type PVEVNCTicketData struct {
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
fastWs "github.com/fasthttp/websocket"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
)
|
||||
|
||||
type PVEConfig struct {
|
||||
HostName string
|
||||
User string
|
||||
Password string
|
||||
Port int
|
||||
PrivateKey string
|
||||
PrivateKeyPassphrase string
|
||||
}
|
||||
|
||||
// https://github.com/proxmox/pve-xtermjs/blob/master/README
|
||||
|
||||
func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, instance *PVEInstance) error {
|
||||
access, err := pve.GetAccessTicket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := pve.GetVNCTicket(access, instance, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s",
|
||||
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket))
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Add("Authorization", "PVEAPIToken="+access.Username)
|
||||
headers.Add("Cookie", "PVEAuthCookie="+access.Ticket)
|
||||
|
||||
dialer := fastWs.Dialer{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
ws, _, err := dialer.Dial(url, headers)
|
||||
if err != nil {
|
||||
log.Println("Error connecting to Proxmox WebSocket:", err)
|
||||
return err
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// Send first ticket line
|
||||
ws.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("%s:%s\n", access.Username, access.Ticket)))
|
||||
|
||||
go func() {
|
||||
for {
|
||||
t, 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 {
|
||||
width, _ := strconv.Atoi(parts[0])
|
||||
height, _ := strconv.Atoi(parts[1])
|
||||
ws.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("1:%d:%d:", width, height)))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
msg = []byte(fmt.Sprintf("0:%d:%s\n", len(msg), string(msg)))
|
||||
|
||||
if err = ws.WriteMessage(t, msg); err != nil {
|
||||
log.Println("Error writing to Proxmox:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
t, msg, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading from Proxmox:", err)
|
||||
break
|
||||
}
|
||||
|
||||
if string(msg) == "OK" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.WriteMessage(t, msg); err != nil {
|
||||
log.Println("Error writing to client:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pve *PVEServer) NewVNCSession(c *websocket.Conn, instance *PVEInstance) error {
|
||||
access, err := pve.GetAccessTicket()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := pve.GetVNCTicket(access, instance, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s",
|
||||
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket))
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Add("Authorization", "PVEAPIToken="+access.Username)
|
||||
headers.Add("Cookie", "PVEAuthCookie="+access.Ticket)
|
||||
|
||||
dialer := fastWs.Dialer{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
ws, _, err := dialer.Dial(url, headers)
|
||||
if err != nil {
|
||||
log.Println("Error connecting to Proxmox WebSocket:", err)
|
||||
return err
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// Send vnc password
|
||||
c.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("\x01%s", ticket.Ticket)))
|
||||
|
||||
go func() {
|
||||
for {
|
||||
t, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading from client:", err)
|
||||
break
|
||||
}
|
||||
|
||||
if err = ws.WriteMessage(t, msg); err != nil {
|
||||
log.Println("Error writing to Proxmox:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
t, msg, err := ws.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("Error reading from Proxmox:", err)
|
||||
break
|
||||
}
|
||||
|
||||
if err = c.WriteMessage(t, msg); err != nil {
|
||||
log.Println("Error writing to client:", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type SSHConfig struct {
|
||||
HostName string
|
||||
User string
|
||||
Password string
|
||||
Port int
|
||||
PrivateKey string
|
||||
PrivateKeyPassphrase string
|
||||
}
|
||||
|
||||
func NewSSHWebsocketSession(c *websocket.Conn, cfg *SSHConfig) error {
|
||||
// Set up SSH client configuration
|
||||
port := cfg.Port
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
auth := []ssh.AuthMethod{
|
||||
ssh.Password(cfg.Password),
|
||||
}
|
||||
|
||||
if cfg.PrivateKey != "" {
|
||||
var err error
|
||||
var signer ssh.Signer
|
||||
|
||||
if cfg.PrivateKeyPassphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(cfg.PrivateKey), []byte(cfg.PrivateKeyPassphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(cfg.PrivateKey))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse private key: %v", err)
|
||||
}
|
||||
auth = append(auth, ssh.PublicKeys(signer))
|
||||
}
|
||||
|
||||
sshConfig := &ssh.ClientConfig{
|
||||
User: cfg.User,
|
||||
Auth: auth,
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
}
|
||||
|
||||
// Connect to SSH server
|
||||
hostName := fmt.Sprintf("%s:%d", cfg.HostName, port)
|
||||
sshConn, err := ssh.Dial("tcp", hostName, sshConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sshConn.Close()
|
||||
|
||||
// Start an SSH shell session
|
||||
session, err := sshConn.NewSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
stdoutPipe, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stderrPipe, err := session.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdinPipe, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = session.RequestPty("xterm-256color", 80, 24, ssh.TerminalModes{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := session.Shell(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Goroutine to send SSH stdout to WebSocket
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := stdoutPipe.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("error reading from SSH stdout: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||
log.Printf("error writing to websocket: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Goroutine to handle SSH stderr
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := stderrPipe.Read(buf)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("error reading from SSH stderr: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
if err := c.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
|
||||
log.Printf("error writing to websocket: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle WebSocket to SSH data streaming
|
||||
go func() {
|
||||
defer session.Close()
|
||||
|
||||
for {
|
||||
_, msg, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if strings.HasPrefix(string(msg), "\x01") {
|
||||
parts := strings.Split(string(msg[1:]), ",")
|
||||
if len(parts) == 2 {
|
||||
width, _ := strconv.Atoi(parts[0])
|
||||
height, _ := strconv.Atoi(parts[1])
|
||||
session.WindowChange(height, width)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
stdinPipe.Write(msg)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the SSH session to close
|
||||
if err := session.Wait(); err != nil {
|
||||
log.Printf("SSH session ended with error: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("SSH session ended normally")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user