mirror of https://github.com/jetkvm/kvm.git
271 lines
6.8 KiB
Go
271 lines
6.8 KiB
Go
package kvm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"github.com/coder/websocket/wsjson"
|
|
"time"
|
|
|
|
"github.com/coreos/go-oidc/v3/oidc"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/coder/websocket"
|
|
)
|
|
|
|
type CloudRegisterRequest struct {
|
|
Token string `json:"token"`
|
|
CloudAPI string `json:"cloudApi"`
|
|
OidcGoogle string `json:"oidcGoogle"`
|
|
ClientId string `json:"clientId"`
|
|
}
|
|
|
|
func handleCloudRegister(c *gin.Context) {
|
|
var req CloudRegisterRequest
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(400, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
// Exchange the temporary token for a permanent auth token
|
|
payload := struct {
|
|
TempToken string `json:"tempToken"`
|
|
}{
|
|
TempToken: req.Token,
|
|
}
|
|
jsonPayload, err := json.Marshal(payload)
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": "Failed to encode JSON payload: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
resp, err := http.Post(req.CloudAPI+"/devices/token", "application/json", bytes.NewBuffer(jsonPayload))
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": "Failed to exchange token: " + err.Error()})
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
c.JSON(resp.StatusCode, gin.H{"error": "Failed to exchange token: " + resp.Status})
|
|
return
|
|
}
|
|
|
|
var tokenResp struct {
|
|
SecretToken string `json:"secretToken"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
|
c.JSON(500, gin.H{"error": "Failed to parse token response: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if tokenResp.SecretToken == "" {
|
|
c.JSON(500, gin.H{"error": "Received empty secret token"})
|
|
return
|
|
}
|
|
|
|
config.CloudToken = tokenResp.SecretToken
|
|
config.CloudURL = req.CloudAPI
|
|
|
|
provider, err := oidc.NewProvider(c, "https://accounts.google.com")
|
|
if err != nil {
|
|
c.JSON(500, gin.H{"error": "Failed to initialize OIDC provider: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
oidcConfig := &oidc.Config{
|
|
ClientID: req.ClientId,
|
|
}
|
|
|
|
verifier := provider.Verifier(oidcConfig)
|
|
idToken, err := verifier.Verify(c, req.OidcGoogle)
|
|
if err != nil {
|
|
c.JSON(400, gin.H{"error": "Invalid OIDC token: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
config.GoogleIdentity = idToken.Audience[0] + ":" + idToken.Subject
|
|
|
|
// Save the updated configuration
|
|
if err := SaveConfig(); err != nil {
|
|
c.JSON(500, gin.H{"error": "Failed to save configuration"})
|
|
return
|
|
}
|
|
|
|
c.JSON(200, gin.H{"message": "Cloud registration successful"})
|
|
}
|
|
|
|
func runWebsocketClient() error {
|
|
if config.CloudToken == "" {
|
|
time.Sleep(5 * time.Second)
|
|
return fmt.Errorf("cloud token is not set")
|
|
}
|
|
wsURL, err := url.Parse(config.CloudURL)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse config.CloudURL: %w", err)
|
|
}
|
|
if wsURL.Scheme == "http" {
|
|
wsURL.Scheme = "ws"
|
|
} else {
|
|
wsURL.Scheme = "wss"
|
|
}
|
|
header := http.Header{}
|
|
header.Set("X-Device-ID", GetDeviceID())
|
|
header.Set("Authorization", "Bearer "+config.CloudToken)
|
|
dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Minute)
|
|
defer cancelDial()
|
|
c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
|
|
HTTPHeader: header,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c.CloseNow()
|
|
logger.Infof("WS connected to %v", wsURL.String())
|
|
runCtx, cancelRun := context.WithCancel(context.Background())
|
|
defer cancelRun()
|
|
go func() {
|
|
for {
|
|
time.Sleep(15 * time.Second)
|
|
err := c.Ping(runCtx)
|
|
if err != nil {
|
|
logger.Warnf("websocket ping error: %v", err)
|
|
cancelRun()
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
for {
|
|
typ, msg, err := c.Read(runCtx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if typ != websocket.MessageText {
|
|
// ignore non-text messages
|
|
continue
|
|
}
|
|
var req WebRTCSessionRequest
|
|
err = json.Unmarshal(msg, &req)
|
|
if err != nil {
|
|
logger.Warnf("unable to parse ws message: %v", string(msg))
|
|
continue
|
|
}
|
|
|
|
err = handleSessionRequest(runCtx, c, req)
|
|
if err != nil {
|
|
logger.Infof("error starting new session: %v", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
|
|
oidcCtx, cancelOIDC := context.WithTimeout(ctx, time.Minute)
|
|
defer cancelOIDC()
|
|
provider, err := oidc.NewProvider(oidcCtx, "https://accounts.google.com")
|
|
if err != nil {
|
|
fmt.Println("Failed to initialize OIDC provider:", err)
|
|
return err
|
|
}
|
|
|
|
oidcConfig := &oidc.Config{
|
|
SkipClientIDCheck: true,
|
|
}
|
|
|
|
verifier := provider.Verifier(oidcConfig)
|
|
idToken, err := verifier.Verify(oidcCtx, req.OidcGoogle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
googleIdentity := idToken.Audience[0] + ":" + idToken.Subject
|
|
if config.GoogleIdentity != googleIdentity {
|
|
return fmt.Errorf("google identity mismatch")
|
|
}
|
|
|
|
session, err := newSession()
|
|
if err != nil {
|
|
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
|
return err
|
|
}
|
|
|
|
sd, err := session.ExchangeOffer(req.Sd)
|
|
if err != nil {
|
|
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
|
return err
|
|
}
|
|
if currentSession != nil {
|
|
writeJSONRPCEvent("otherSessionConnected", nil, currentSession)
|
|
peerConn := currentSession.peerConnection
|
|
go func() {
|
|
time.Sleep(1 * time.Second)
|
|
_ = peerConn.Close()
|
|
}()
|
|
}
|
|
currentSession = session
|
|
_ = wsjson.Write(context.Background(), c, gin.H{"sd": sd})
|
|
return nil
|
|
}
|
|
|
|
func RunWebsocketClient() {
|
|
for {
|
|
err := runWebsocketClient()
|
|
if err != nil {
|
|
fmt.Println("Websocket client error:", err)
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
type CloudState struct {
|
|
Connected bool `json:"connected"`
|
|
URL string `json:"url,omitempty"`
|
|
}
|
|
|
|
func rpcGetCloudState() CloudState {
|
|
return CloudState{
|
|
Connected: config.CloudToken != "" && config.CloudURL != "",
|
|
URL: config.CloudURL,
|
|
}
|
|
}
|
|
|
|
func rpcDeregisterDevice() error {
|
|
if config.CloudToken == "" || config.CloudURL == "" {
|
|
return fmt.Errorf("cloud token or URL is not set")
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodDelete, config.CloudURL+"/devices/"+GetDeviceID(), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create deregister request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+config.CloudToken)
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send deregister request: %w", err)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
// We consider both 200 OK and 404 Not Found as successful deregistration.
|
|
// 200 OK means the device was found and deregistered.
|
|
// 404 Not Found means the device is not in the database, which could be due to various reasons
|
|
// (e.g., wrong cloud token, already deregistered). Regardless of the reason, we can safely remove it.
|
|
if resp.StatusCode == http.StatusNotFound || (resp.StatusCode >= 200 && resp.StatusCode < 300) {
|
|
config.CloudToken = ""
|
|
config.CloudURL = ""
|
|
config.GoogleIdentity = ""
|
|
if err := SaveConfig(); err != nil {
|
|
return fmt.Errorf("failed to save configuration after deregistering: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("deregister request failed with status: %s", resp.Status)
|
|
}
|