@@ -4,6 +4,8 @@ package ssh
44import (
55 "crypto/ed25519"
66 "crypto/rand"
7+ "encoding/base64"
8+ "encoding/json"
79 "encoding/pem"
810 "fmt"
911 "net/url"
@@ -60,26 +62,70 @@ func GenerateKeyPair() (*KeyPair, error) {
6062 }, nil
6163}
6264
63- // ExtractVMDomain extracts the VM hostname from a BrowserLiveViewURL or CdpWsURL .
64- // Examples:
65- // - "https ://vm-abc123.kernel.live/..." -> "vm-abc123.kernel.live"
66- // - "wss://vm-abc123.kernel.live/..." -> " vm-abc123.kernel.live"
67- func ExtractVMDomain (rawURL string ) (string , error ) {
68- if rawURL == "" {
65+ // ExtractVMDomain extracts the VM FQDN from a CDP WebSocket URL by decoding the JWT .
66+ // The CDP URL contains a JWT with the actual Unikraft FQDN in the payload.
67+ // Example CDP URL: wss ://proxy.xxx.dev.onkernel.com:8443/browser/cdp?jwt=eyJ...
68+ // The JWT payload contains: {"session": {"fqdn": "actual- vm-domain.onkernel.app"}}
69+ func ExtractVMDomain (cdpURL string ) (string , error ) {
70+ if cdpURL == "" {
6971 return "" , fmt .Errorf ("empty URL" )
7072 }
7173
72- parsed , err := url .Parse (rawURL )
74+ parsed , err := url .Parse (cdpURL )
7375 if err != nil {
7476 return "" , fmt .Errorf ("failed to parse URL: %w" , err )
7577 }
7678
77- host := parsed .Hostname ()
78- if host == "" {
79- return "" , fmt .Errorf ("no hostname in URL: %s" , rawURL )
79+ // Extract JWT from query parameter
80+ jwt := parsed .Query ().Get ("jwt" )
81+ if jwt == "" {
82+ // Fallback to hostname if no JWT (shouldn't happen in practice)
83+ host := parsed .Hostname ()
84+ if host == "" {
85+ return "" , fmt .Errorf ("no hostname in URL: %s" , cdpURL )
86+ }
87+ return host , nil
8088 }
8189
82- return host , nil
90+ // JWT is header.payload.signature - we need the payload (middle part)
91+ parts := strings .Split (jwt , "." )
92+ if len (parts ) != 3 {
93+ return "" , fmt .Errorf ("invalid JWT format" )
94+ }
95+
96+ // Decode base64url payload
97+ payload := parts [1 ]
98+ // Add padding if needed (base64url may omit padding)
99+ switch len (payload ) % 4 {
100+ case 2 :
101+ payload += "=="
102+ case 3 :
103+ payload += "="
104+ }
105+ // Convert base64url to standard base64
106+ payload = strings .ReplaceAll (payload , "-" , "+" )
107+ payload = strings .ReplaceAll (payload , "_" , "/" )
108+
109+ decoded , err := base64 .StdEncoding .DecodeString (payload )
110+ if err != nil {
111+ return "" , fmt .Errorf ("failed to decode JWT payload: %w" , err )
112+ }
113+
114+ // Parse JSON payload
115+ var claims struct {
116+ Session struct {
117+ FQDN string `json:"fqdn"`
118+ } `json:"session"`
119+ }
120+ if err := json .Unmarshal (decoded , & claims ); err != nil {
121+ return "" , fmt .Errorf ("failed to parse JWT payload: %w" , err )
122+ }
123+
124+ if claims .Session .FQDN == "" {
125+ return "" , fmt .Errorf ("no FQDN in JWT payload" )
126+ }
127+
128+ return claims .Session .FQDN , nil
83129}
84130
85131// CheckWebsocatInstalled verifies websocat is available in PATH.
0 commit comments