waveterm/pkg/wavejwt/wavejwt.go
Mike Sawka 01a26d59e6
Persistent Terminal Sessions (+ improvements and bug fixes) (#2806)
Lots of updates across all parts of the system to get this working. Big
changes to routing, streaming, connection management, etc.

* Persistent sessions behind a metadata flag for now
* New backlog queue in the router to prevent hanging
* Fix connection Close() issues that caused hangs when network was down
* Fix issue with random routeids (need to be generated fresh each time
the JWT is used and not fixed) so you can run multiple-wsh commands at
once
* Fix issue with domain sockets changing names across wave restarts
(added a symlink mechanism to resolve new names)
* ClientId caching in main server
* Quick reorder queue for input to prevent out of order delivery across
multiple hops
* Fix out-of-order event delivery in router (remove unnecessary go
routine creation)
* Environment testing and fix environment variables for remote jobs (get
from connserver, add to remote job starts)
* Add new ConnServerInit() remote method to call before marking
connection up
* TODO -- remote file transfer needs to be fixed to not create OOM
issues when transferring large files or directories
2026-01-28 13:30:48 -08:00

143 lines
3.2 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wavejwt
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
IssuerWaveTerm = "waveterm"
)
var (
globalLock sync.Mutex
publicKey ed25519.PublicKey
privateKey ed25519.PrivateKey
)
type WaveJwtClaims struct {
jwt.RegisteredClaims
MainServer bool `json:"mainserver,omitempty"`
Sock string `json:"sock,omitempty"`
RouteId string `json:"routeid,omitempty"`
ProcRoute bool `json:"procroute,omitempty"`
BlockId string `json:"blockid,omitempty"`
JobId string `json:"jobid,omitempty"`
Conn string `json:"conn,omitempty"`
Router bool `json:"router,omitempty"`
}
type KeyPair struct {
PublicKey []byte
PrivateKey []byte
}
func GenerateKeyPair() (*KeyPair, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate key pair: %w", err)
}
return &KeyPair{
PublicKey: pubKey,
PrivateKey: privKey,
}, nil
}
func SetPublicKey(keyData []byte) error {
if len(keyData) != ed25519.PublicKeySize {
return fmt.Errorf("invalid public key size: expected %d, got %d", ed25519.PublicKeySize, len(keyData))
}
globalLock.Lock()
defer globalLock.Unlock()
publicKey = ed25519.PublicKey(keyData)
return nil
}
func GetPublicKey() []byte {
globalLock.Lock()
defer globalLock.Unlock()
return publicKey
}
func GetPublicKeyBase64() string {
pubKey := GetPublicKey()
if len(pubKey) == 0 {
return ""
}
return base64.StdEncoding.EncodeToString(pubKey)
}
func SetPrivateKey(keyData []byte) error {
if len(keyData) != ed25519.PrivateKeySize {
return fmt.Errorf("invalid private key size: expected %d, got %d", ed25519.PrivateKeySize, len(keyData))
}
globalLock.Lock()
defer globalLock.Unlock()
privateKey = ed25519.PrivateKey(keyData)
return nil
}
func ValidateAndExtract(tokenStr string) (*WaveJwtClaims, error) {
globalLock.Lock()
pubKey := publicKey
globalLock.Unlock()
if pubKey == nil {
return nil, fmt.Errorf("public key not set")
}
token, err := jwt.ParseWithClaims(tokenStr, &WaveJwtClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return pubKey, nil
})
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}
claims, ok := token.Claims.(*WaveJwtClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
func Sign(claims *WaveJwtClaims) (string, error) {
globalLock.Lock()
privKey := privateKey
globalLock.Unlock()
if privKey == nil {
return "", fmt.Errorf("private key not set")
}
if claims.IssuedAt == nil {
claims.IssuedAt = jwt.NewNumericDate(time.Now())
}
if claims.Issuer == "" {
claims.Issuer = IssuerWaveTerm
}
if claims.ExpiresAt == nil {
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 365))
}
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
tokenStr, err := token.SignedString(privKey)
if err != nil {
return "", fmt.Errorf("error signing token: %w", err)
}
return tokenStr, nil
}