diff --git a/server/cmd/main.go b/server/cmd/main.go index b2036cf..41ac110 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -56,7 +56,9 @@ func main() { }() // Start REST server - r := gin.Default() + r := gin.New() + r.Use(gin.Recovery()) + r.Use(gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/api/console/tunnel"}})) r.Use(corsMiddleware()) api.RegisterRoutes(r) diff --git a/server/internal/api/console.go b/server/internal/api/console.go index 0d534e6..71618c5 100644 --- a/server/internal/api/console.go +++ b/server/internal/api/console.go @@ -44,6 +44,13 @@ func consoleConnect(c *gin.Context) { return } + if body.Protocol == "rdp" && (body.RDPUsername != "" || body.RDPPassword != "") { + if err := services.StashConsoleRDPCreds(sess.SessionID, body.RDPUsername, body.RDPPassword); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + } + services.LogEvent("console.opened", actorFromCtx(c), srv.ServerID, "", "console session opened ("+body.Protocol+")") @@ -83,10 +90,15 @@ func consoleTunnel(c *gin.Context) { } } - // RDP creds are single-use, passed via the connect step into the session - // document is avoided; instead they are re-supplied here as query params - // over the already-authenticated WS token. For ssh they are empty. - gp, err := services.BuildGuacParams(srv, sess.Protocol, privKey, c.Query("u"), c.Query("p")) + var rdpUser, rdpPass string + if sess.Protocol == "rdp" { + rdpUser, rdpPass, err = services.ConsumeConsoleRDPCreds(sessionID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "could not load credentials"}) + return + } + } + gp, err := services.BuildGuacParams(srv, sess.Protocol, privKey, rdpUser, rdpPass) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return diff --git a/server/internal/models/console_session.go b/server/internal/models/console_session.go index b0a9635..9df54ea 100644 --- a/server/internal/models/console_session.go +++ b/server/internal/models/console_session.go @@ -16,4 +16,7 @@ type ConsoleSession struct { StartedAt time.Time `bson:"started_at" json:"started_at"` EndedAt *time.Time `bson:"ended_at,omitempty" json:"ended_at,omitempty"` ClientIP string `bson:"client_ip,omitempty" json:"client_ip,omitempty"` + + RDPUserEnc string `bson:"rdp_user_enc,omitempty" json:"-"` + RDPPassEnc string `bson:"rdp_pass_enc,omitempty" json:"-"` } diff --git a/server/internal/services/console.go b/server/internal/services/console.go index 1825399..793c9d1 100644 --- a/server/internal/services/console.go +++ b/server/internal/services/console.go @@ -150,6 +150,56 @@ func GetConsoleSession(sessionID string) (*models.ConsoleSession, error) { return &s, nil } +// StashConsoleRDPCreds encrypts and stores single-use RDP credentials on the +// session document. They are consumed (and cleared) when the tunnel opens. +func StashConsoleRDPCreds(sessionID, username, password string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + u, err := encryptString(username) + if err != nil { + return err + } + p, err := encryptString(password) + if err != nil { + return err + } + _, err = db.Col("console_sessions").UpdateOne(ctx, + bson.M{"session_id": sessionID}, + bson.M{"$set": bson.M{"rdp_user_enc": u, "rdp_pass_enc": p}}, + ) + return err +} + +// ConsumeConsoleRDPCreds decrypts and returns the stored RDP credentials, then +// clears them from the session document (single-use). Returns empty strings if +// none were stored. +func ConsumeConsoleRDPCreds(sessionID string) (username, password string, err error) { + s, err := GetConsoleSession(sessionID) + if err != nil { + return "", "", err + } + if s.RDPUserEnc == "" && s.RDPPassEnc == "" { + return "", "", nil + } + if s.RDPUserEnc != "" { + if username, err = decryptString(s.RDPUserEnc); err != nil { + return "", "", err + } + } + if s.RDPPassEnc != "" { + if password, err = decryptString(s.RDPPassEnc); err != nil { + return "", "", err + } + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, _ = db.Col("console_sessions").UpdateOne(ctx, + bson.M{"session_id": sessionID}, + bson.M{"$unset": bson.M{"rdp_user_enc": "", "rdp_pass_enc": ""}}, + ) + return username, password, nil +} + func EndConsoleSession(sessionID string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()