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:
@@ -0,0 +1,37 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/joho/godotenv"
|
||||
"rul.sh/vaulterm/app/auth"
|
||||
"rul.sh/vaulterm/app/hosts"
|
||||
"rul.sh/vaulterm/app/keychains"
|
||||
"rul.sh/vaulterm/app/ws"
|
||||
"rul.sh/vaulterm/db"
|
||||
)
|
||||
|
||||
func NewApp() *fiber.App {
|
||||
// Load deps
|
||||
godotenv.Load()
|
||||
db.Init()
|
||||
|
||||
// Create fiber app
|
||||
app := fiber.New(fiber.Config{ErrorHandler: ErrorHandler})
|
||||
|
||||
// Middlewares
|
||||
app.Use(cors.New())
|
||||
|
||||
// Init app routes
|
||||
auth.Router(app)
|
||||
hosts.Router(app)
|
||||
keychains.Router(app)
|
||||
ws.Router(app)
|
||||
|
||||
// Health check
|
||||
app.Get("/health-check", func(c *fiber.Ctx) error {
|
||||
return c.SendString("OK")
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"rul.sh/vaulterm/db"
|
||||
"rul.sh/vaulterm/lib"
|
||||
"rul.sh/vaulterm/models"
|
||||
)
|
||||
|
||||
type Auth struct{ db *gorm.DB }
|
||||
|
||||
func NewAuthRepository() *Auth {
|
||||
return &Auth{db: db.Get()}
|
||||
}
|
||||
|
||||
func (r *Auth) FindUser(username string) (*models.User, error) {
|
||||
var user models.User
|
||||
ret := r.db.Where("username = ? OR email = ?", username, username).First(&user)
|
||||
|
||||
return &user, ret.Error
|
||||
}
|
||||
|
||||
func (r *Auth) CreateUserSession(user *models.User) (string, error) {
|
||||
sessionId, err := lib.GenerateSessionID(20)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if ret := r.db.Create(&models.UserSession{ID: sessionId, UserID: user.ID}); ret.Error != nil {
|
||||
return "", ret.Error
|
||||
}
|
||||
|
||||
return sessionId, nil
|
||||
}
|
||||
|
||||
func (r *Auth) GetSession(sessionId string) (*models.UserSession, error) {
|
||||
var session models.UserSession
|
||||
res := r.db.Joins("User").Where(&models.UserSession{ID: sessionId}).First(&session)
|
||||
return &session, res.Error
|
||||
}
|
||||
|
||||
func (r *Auth) RemoveUserSession(sessionId string, force bool) error {
|
||||
db := r.db
|
||||
if force {
|
||||
db = db.Unscoped()
|
||||
}
|
||||
|
||||
res := db.Delete(&models.UserSession{ID: sessionId})
|
||||
return res.Error
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"rul.sh/vaulterm/lib"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
func Router(app *fiber.App) {
|
||||
router := app.Group("/auth")
|
||||
|
||||
router.Post("/login", login)
|
||||
router.Get("/user", getUser)
|
||||
router.Post("/logout", logout)
|
||||
}
|
||||
|
||||
func login(c *fiber.Ctx) error {
|
||||
repo := NewAuthRepository()
|
||||
|
||||
var body LoginSchema
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return &fiber.Error{
|
||||
Code: fiber.StatusBadRequest,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
user, err := repo.FindUser(body.Username)
|
||||
if err != nil {
|
||||
return &fiber.Error{
|
||||
Code: fiber.StatusUnauthorized,
|
||||
Message: "Username or password is invalid",
|
||||
}
|
||||
}
|
||||
|
||||
if valid := lib.VerifyPassword(body.Password, user.Password); !valid {
|
||||
return &fiber.Error{
|
||||
Code: fiber.StatusUnauthorized,
|
||||
Message: "Username or password is invalid",
|
||||
}
|
||||
}
|
||||
|
||||
sessionId, err := repo.CreateUserSession(user)
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"user": user,
|
||||
"sessionId": sessionId,
|
||||
})
|
||||
}
|
||||
|
||||
func getUser(c *fiber.Ctx) error {
|
||||
auth := c.Get("Authorization")
|
||||
var sessionId string
|
||||
|
||||
if auth != "" {
|
||||
sessionId = strings.Split(auth, " ")[1]
|
||||
}
|
||||
|
||||
repo := NewAuthRepository()
|
||||
session, err := repo.GetSession(sessionId)
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(session)
|
||||
}
|
||||
|
||||
func logout(c *fiber.Ctx) error {
|
||||
auth := c.Get("Authorization")
|
||||
force := c.Query("force")
|
||||
var sessionId string
|
||||
|
||||
if auth != "" {
|
||||
sessionId = strings.Split(auth, " ")[1]
|
||||
}
|
||||
|
||||
repo := NewAuthRepository()
|
||||
err := repo.RemoveUserSession(sessionId, force == "true")
|
||||
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
"message": "Successfully logged out",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package auth
|
||||
|
||||
type LoginSchema struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ErrorHandler(ctx *fiber.Ctx, err error) error {
|
||||
// Status code defaults to 500
|
||||
code := fiber.StatusInternalServerError
|
||||
|
||||
// Retrieve the custom status code if it's a *fiber.Error
|
||||
var e *fiber.Error
|
||||
if errors.As(err, &e) {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
// Return from handler
|
||||
return ctx.Status(code).JSON(fiber.Map{
|
||||
"status": "error",
|
||||
"code": code,
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package hosts
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"rul.sh/vaulterm/db"
|
||||
"rul.sh/vaulterm/models"
|
||||
)
|
||||
|
||||
type Hosts struct{ db *gorm.DB }
|
||||
|
||||
func NewHostsRepository() *Hosts {
|
||||
return &Hosts{db: db.Get()}
|
||||
}
|
||||
|
||||
func (r *Hosts) GetAll() ([]*models.Host, error) {
|
||||
var rows []*models.Host
|
||||
ret := r.db.Order("created_at DESC").Find(&rows)
|
||||
|
||||
return rows, ret.Error
|
||||
}
|
||||
|
||||
type GetHostResult struct {
|
||||
Host *models.Host
|
||||
Key map[string]interface{}
|
||||
AltKey map[string]interface{}
|
||||
}
|
||||
|
||||
func (r *Hosts) Get(id string) (*GetHostResult, error) {
|
||||
var host models.Host
|
||||
ret := r.db.Joins("Key").Joins("AltKey").Where("hosts.id = ?", id).First(&host)
|
||||
|
||||
if ret.Error != nil {
|
||||
return nil, ret.Error
|
||||
}
|
||||
|
||||
res := &GetHostResult{Host: &host}
|
||||
|
||||
if host.Key.Data != "" {
|
||||
if err := host.Key.DecryptData(&res.Key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if host.AltKey.Data != "" {
|
||||
if err := host.AltKey.DecryptData(&res.AltKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return res, ret.Error
|
||||
}
|
||||
|
||||
func (r *Hosts) Create(item *models.Host) error {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package hosts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
func Router(app *fiber.App) {
|
||||
router := app.Group("/hosts")
|
||||
|
||||
router.Get("/", getAll)
|
||||
router.Post("/", create)
|
||||
}
|
||||
|
||||
func getAll(c *fiber.Ctx) error {
|
||||
repo := NewHostsRepository()
|
||||
rows, err := repo.GetAll()
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"rows": rows,
|
||||
})
|
||||
}
|
||||
|
||||
func create(c *fiber.Ctx) error {
|
||||
var body CreateHostSchema
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
repo := NewHostsRepository()
|
||||
|
||||
item := &models.Host{
|
||||
Type: body.Type,
|
||||
Label: body.Label,
|
||||
Host: body.Host,
|
||||
Port: body.Port,
|
||||
Metadata: body.Metadata,
|
||||
ParentID: body.ParentID,
|
||||
KeyID: body.KeyID,
|
||||
AltKeyID: body.AltKeyID,
|
||||
}
|
||||
if err := repo.Create(item); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(item)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package hosts
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
type CreateHostSchema struct {
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Metadata datatypes.JSONMap `json:"metadata"`
|
||||
|
||||
ParentID *string `json:"parentId"`
|
||||
KeyID *string `json:"keyId"`
|
||||
AltKeyID *string `json:"altKeyId"`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package keychains
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"rul.sh/vaulterm/db"
|
||||
"rul.sh/vaulterm/models"
|
||||
)
|
||||
|
||||
type Keychains struct{ db *gorm.DB }
|
||||
|
||||
func NewKeychainsRepository() *Keychains {
|
||||
return &Keychains{db: db.Get()}
|
||||
}
|
||||
|
||||
func (r *Keychains) GetAll() ([]*models.Keychain, error) {
|
||||
var rows []*models.Keychain
|
||||
ret := r.db.Order("created_at DESC").Find(&rows)
|
||||
|
||||
return rows, ret.Error
|
||||
}
|
||||
|
||||
func (r *Keychains) Create(item *models.Keychain) error {
|
||||
return r.db.Create(item).Error
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package keychains
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"rul.sh/vaulterm/models"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
func Router(app *fiber.App) {
|
||||
router := app.Group("/keychains")
|
||||
|
||||
router.Get("/", getAll)
|
||||
router.Post("/", create)
|
||||
}
|
||||
|
||||
func getAll(c *fiber.Ctx) error {
|
||||
repo := NewKeychainsRepository()
|
||||
rows, err := repo.GetAll()
|
||||
if err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"rows": rows,
|
||||
})
|
||||
}
|
||||
|
||||
func create(c *fiber.Ctx) error {
|
||||
var body CreateKeychainSchema
|
||||
if err := c.BodyParser(&body); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
repo := NewKeychainsRepository()
|
||||
|
||||
item := &models.Keychain{
|
||||
Type: body.Type,
|
||||
Label: body.Label,
|
||||
}
|
||||
|
||||
if err := item.EncryptData(body.Data); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
if err := repo.Create(item); err != nil {
|
||||
return utils.ResponseError(c, err, 500)
|
||||
}
|
||||
|
||||
return c.Status(http.StatusCreated).JSON(item)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package keychains
|
||||
|
||||
type CreateKeychainSchema struct {
|
||||
Type string `json:"type"`
|
||||
Label string `json:"label"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func Router(app *fiber.App) {
|
||||
router := app.Group("/ws")
|
||||
|
||||
router.Use(func(c *fiber.Ctx) error {
|
||||
if websocket.IsWebSocketUpgrade(c) {
|
||||
return c.Next()
|
||||
}
|
||||
return fiber.ErrUpgradeRequired
|
||||
})
|
||||
|
||||
router.Get("/term", websocket.New(HandleTerm))
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"rul.sh/vaulterm/app/hosts"
|
||||
"rul.sh/vaulterm/lib"
|
||||
"rul.sh/vaulterm/utils"
|
||||
)
|
||||
|
||||
func HandleTerm(c *websocket.Conn) {
|
||||
hostId := c.Query("hostId")
|
||||
|
||||
hostRepo := hosts.NewHostsRepository()
|
||||
data, _ := hostRepo.Get(hostId)
|
||||
|
||||
if data == nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Host not found"))
|
||||
return
|
||||
}
|
||||
|
||||
switch data.Host.Type {
|
||||
case "ssh":
|
||||
sshHandler(c, data)
|
||||
case "pve":
|
||||
pveHandler(c, data)
|
||||
case "incus":
|
||||
incusHandler(c, data)
|
||||
default:
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Invalid host type"))
|
||||
}
|
||||
}
|
||||
|
||||
func sshHandler(c *websocket.Conn, data *hosts.GetHostResult) {
|
||||
username, _ := data.Key["username"].(string)
|
||||
password, _ := data.Key["password"].(string)
|
||||
|
||||
cfg := &SSHConfig{
|
||||
HostName: data.Host.Host,
|
||||
Port: data.Host.Port,
|
||||
User: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if err := NewSSHWebsocketSession(c, cfg); err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func pveHandler(c *websocket.Conn, data *hosts.GetHostResult) {
|
||||
client := c.Query("client")
|
||||
username, _ := data.Key["username"].(string)
|
||||
password, _ := data.Key["password"].(string)
|
||||
|
||||
pve := &lib.PVEServer{
|
||||
HostName: data.Host.Host,
|
||||
Port: data.Host.Port,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
var i *lib.PVEInstance
|
||||
if err := utils.ParseMapInterface(data.Host.Metadata, &i); err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if i == nil || i.Type == "" || i.Node == "" || i.VMID == "" {
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Invalid pve instance metadata"))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if client == "vnc" {
|
||||
err = NewVNCSession(c, pve, i)
|
||||
} else {
|
||||
err = NewTerminalSession(c, pve, i)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func incusHandler(c *websocket.Conn, data *hosts.GetHostResult) {
|
||||
shell := c.Query("shell")
|
||||
|
||||
cert, _ := data.Key["cert"].(string)
|
||||
key, _ := data.Key["key"].(string)
|
||||
|
||||
if cert == "" || key == "" {
|
||||
c.WriteMessage(websocket.TextMessage, []byte("Missing certificate or key"))
|
||||
return
|
||||
}
|
||||
|
||||
incus := &lib.IncusServer{
|
||||
HostName: data.Host.Host,
|
||||
Port: data.Host.Port,
|
||||
ClientCert: cert,
|
||||
ClientKey: key,
|
||||
}
|
||||
|
||||
session := &IncusWebsocketSession{}
|
||||
if err := utils.ParseMapInterface(data.Host.Metadata, session); err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if shell != "" {
|
||||
session.Shell = shell
|
||||
}
|
||||
|
||||
if err := session.NewTerminal(c, incus); err != nil {
|
||||
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
fastWs "github.com/fasthttp/websocket"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"rul.sh/vaulterm/lib"
|
||||
)
|
||||
|
||||
type IncusWebsocketSession struct {
|
||||
Instance string `json:"instance"`
|
||||
Shell string `json:"shell"`
|
||||
}
|
||||
|
||||
func (i *IncusWebsocketSession) NewTerminal(c *websocket.Conn, incus *lib.IncusServer) error {
|
||||
if i.Shell == "" {
|
||||
i.Shell = "/bin/sh"
|
||||
}
|
||||
|
||||
exec, err := incus.InstanceExec(i.Instance, []string{i.Shell}, 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
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
fastWs "github.com/fasthttp/websocket"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"rul.sh/vaulterm/lib"
|
||||
)
|
||||
|
||||
// https://github.com/proxmox/pve-xtermjs/blob/master/README
|
||||
|
||||
func NewTerminalSession(c *websocket.Conn, pve *lib.PVEServer, instance *lib.PVEInstance) error {
|
||||
access, err := pve.GetAccessTicket()
|
||||
if err != nil {
|
||||
log.Println("Error getting access ticket:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := pve.GetVNCTicket(access, instance, false)
|
||||
if err != nil {
|
||||
log.Println("Error getting vnc ticket:", err)
|
||||
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 NewVNCSession(c *websocket.Conn, pve *lib.PVEServer, instance *lib.PVEInstance) error {
|
||||
access, err := pve.GetAccessTicket()
|
||||
if err != nil {
|
||||
log.Println("Error getting access ticket:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := pve.GetVNCTicket(access, instance, true)
|
||||
if err != nil {
|
||||
log.Println("Error getting vnc ticket:", err)
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package ws
|
||||
|
||||
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