package incus import ( "context" "crypto/sha256" "fmt" "net/http" "net/url" "os" "path/filepath" "slices" "strings" "time" "github.com/gorilla/websocket" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/lxc/incus/v6/shared/api" "github.com/lxc/incus/v6/shared/logger" "github.com/lxc/incus/v6/shared/simplestreams" "github.com/lxc/incus/v6/shared/util" ) // ConnectionArgs represents a set of common connection properties. type ConnectionArgs struct { // TLS certificate of the remote server. If not specified, the system CA is used. TLSServerCert string // TLS certificate to use for client authentication. TLSClientCert string // TLS key to use for client authentication. TLSClientKey string // TLS CA to validate against when in PKI mode. TLSCA string // User agent string UserAgent string // Authentication type AuthType string // Custom proxy Proxy func(*http.Request) (*url.URL, error) // Custom HTTP Client (used as base for the connection) HTTPClient *http.Client // TransportWrapper wraps the *http.Transport set by Incus TransportWrapper func(*http.Transport) HTTPTransporter // Controls whether a client verifies the server's certificate chain and host name. InsecureSkipVerify bool // Controls whether to perform an exact certificate match (will ignore expiry). IdenticalCertificate bool // Cookie jar CookieJar http.CookieJar // OpenID Connect tokens OIDCTokens *oidc.Tokens[*oidc.IDTokenClaims] // Skip the event listener endpoint SkipGetEvents bool // Skip automatic GetServer request upon connection SkipGetServer bool // Caching support for image servers CachePath string CacheExpiry time.Duration // Temp storage. TempPath string } // ConnectIncus lets you connect to a remote Incus daemon over HTTPs. // // A client certificate (TLSClientCert) and key (TLSClientKey) must be provided. // // If connecting to an Incus daemon running in PKI mode, the PKI CA (TLSCA) must also be provided. // // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert). func ConnectIncus(uri string, args *ConnectionArgs) (InstanceServer, error) { return ConnectIncusWithContext(context.Background(), uri, args) } // ConnectIncusWithContext lets you connect to a remote Incus daemon over HTTPs with context.Context. // // A client certificate (TLSClientCert) and key (TLSClientKey) must be provided. // // If connecting to an Incus daemon running in PKI mode, the PKI CA (TLSCA) must also be provided. // // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert). func ConnectIncusWithContext(ctx context.Context, uri string, args *ConnectionArgs) (InstanceServer, error) { // Cleanup URL uri = strings.TrimSuffix(uri, "/") logger.Debug("Connecting to a remote Incus over HTTPS", logger.Ctx{"url": uri}) return httpsIncus(ctx, uri, args) } // ConnectIncusHTTP lets you connect to a VM agent over a VM socket. func ConnectIncusHTTP(args *ConnectionArgs, client *http.Client) (InstanceServer, error) { return ConnectIncusHTTPWithContext(context.Background(), args, client) } // ConnectIncusHTTPWithContext lets you connect to a VM agent over a VM socket with context.Context. func ConnectIncusHTTPWithContext(ctx context.Context, args *ConnectionArgs, client *http.Client) (InstanceServer, error) { logger.Debug("Connecting to a VM agent over a VM socket") // Use empty args if not specified if args == nil { args = &ConnectionArgs{} } httpBaseURL, err := url.Parse("https://custom.socket") if err != nil { return nil, err } ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background()) // Initialize the client struct server := ProtocolIncus{ ctx: ctx, httpBaseURL: *httpBaseURL, httpProtocol: "custom", httpUserAgent: args.UserAgent, ctxConnected: ctxConnected, ctxConnectedCancel: ctxConnectedCancel, eventConns: make(map[string]*websocket.Conn), eventListeners: make(map[string][]*EventListener), skipEvents: args.SkipGetEvents, tempPath: args.TempPath, } // Setup the HTTP client server.http = client // Test the connection and seed the server information if !args.SkipGetServer { serverStatus, _, err := server.GetServer() if err != nil { return nil, err } // Record the server certificate server.httpCertificate = serverStatus.Environment.Certificate } return &server, nil } // ConnectIncusUnix lets you connect to a remote Incus daemon over a local unix socket. // // If the path argument is empty, then $INCUS_SOCKET will be used, if // unset $INCUS_DIR/unix.socket will be used and if that one isn't set // either, then the path will default to /var/lib/incus/unix.socket or /run/incus/unix.socket. func ConnectIncusUnix(path string, args *ConnectionArgs) (InstanceServer, error) { return ConnectIncusUnixWithContext(context.Background(), path, args) } // ConnectIncusUnixWithContext lets you connect to a remote Incus daemon over a local unix socket with context.Context. // // If the path argument is empty, then $INCUS_SOCKET will be used, if // unset $INCUS_DIR/unix.socket will be used and if that one isn't set // either, then the path will default to /var/lib/incus/unix.socket or /run/incus/unix.socket. func ConnectIncusUnixWithContext(ctx context.Context, path string, args *ConnectionArgs) (InstanceServer, error) { logger.Debug("Connecting to a local Incus over a Unix socket") // Use empty args if not specified if args == nil { args = &ConnectionArgs{} } httpBaseURL, err := url.Parse("http://unix.socket") if err != nil { return nil, err } ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background()) // Determine the socket path var projectName string if path == "" { path = os.Getenv("INCUS_SOCKET") if path == "" { incusDir := os.Getenv("INCUS_DIR") if incusDir == "" { _, err := os.Lstat("/run/incus/unix.socket") if err == nil { incusDir = "/run/incus" } else { incusDir = "/var/lib/incus" } } path = filepath.Join(incusDir, "unix.socket") userPath := filepath.Join(incusDir, "unix.socket.user") if !util.PathIsWritable(path) && util.PathIsWritable(userPath) { // Handle the use of incus-user. path = userPath // When using incus-user, the project list is typically restricted. // So let's try to be smart about the project we're using. projectName = fmt.Sprintf("user-%d", os.Geteuid()) } } } // Initialize the client struct server := ProtocolIncus{ ctx: ctx, httpBaseURL: *httpBaseURL, httpUnixPath: path, httpProtocol: "unix", httpUserAgent: args.UserAgent, ctxConnected: ctxConnected, ctxConnectedCancel: ctxConnectedCancel, eventConns: make(map[string]*websocket.Conn), eventListeners: make(map[string][]*EventListener), skipEvents: args.SkipGetEvents, project: projectName, tempPath: args.TempPath, } // Setup the HTTP client httpClient, err := unixHTTPClient(args, path) if err != nil { return nil, err } server.http = httpClient // Test the connection and seed the server information if !args.SkipGetServer { serverStatus, _, err := server.GetServer() if err != nil { return nil, err } // Record the server certificate server.httpCertificate = serverStatus.Environment.Certificate } return &server, nil } // ConnectPublicIncus lets you connect to a remote public Incus daemon over HTTPs. // // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert). func ConnectPublicIncus(uri string, args *ConnectionArgs) (ImageServer, error) { return ConnectPublicIncusWithContext(context.Background(), uri, args) } // ConnectPublicIncusWithContext lets you connect to a remote public Incus daemon over HTTPs with context.Context. // // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert). func ConnectPublicIncusWithContext(ctx context.Context, uri string, args *ConnectionArgs) (ImageServer, error) { logger.Debug("Connecting to a remote public Incus over HTTPS") // Cleanup URL uri = strings.TrimSuffix(uri, "/") return httpsIncus(ctx, uri, args) } // ConnectSimpleStreams lets you connect to a remote SimpleStreams image server over HTTPs. // // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert). func ConnectSimpleStreams(uri string, args *ConnectionArgs) (ImageServer, error) { logger.Debug("Connecting to a remote simplestreams server", logger.Ctx{"URL": uri}) // Cleanup URL uri = strings.TrimSuffix(uri, "/") // Use empty args if not specified if args == nil { args = &ConnectionArgs{} } // Initialize the client struct server := ProtocolSimpleStreams{ httpHost: uri, httpUserAgent: args.UserAgent, httpCertificate: args.TLSServerCert, tempPath: args.TempPath, } // Setup the HTTP client httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.IdenticalCertificate, args.Proxy, args.TransportWrapper) if err != nil { return nil, err } server.http = httpClient // Get simplestreams client ssClient := simplestreams.NewClient(uri, *httpClient, args.UserAgent) server.ssClient = ssClient // Setup the cache if args.CachePath != "" { if !util.PathExists(args.CachePath) { return nil, fmt.Errorf("Cache directory %q doesn't exist", args.CachePath) } hashedURL := fmt.Sprintf("%x", sha256.Sum256([]byte(uri))) cachePath := filepath.Join(args.CachePath, hashedURL) cacheExpiry := args.CacheExpiry if cacheExpiry == 0 { cacheExpiry = time.Hour } if !util.PathExists(cachePath) { err := os.Mkdir(cachePath, 0o755) if err != nil { return nil, err } } ssClient.SetCache(cachePath, cacheExpiry) } return &server, nil } // ConnectOCI lets you connect to a remote OCI image registry over HTTPs. // // Unless the remote server is trusted by the system CA, the remote certificate must be provided (TLSServerCert). func ConnectOCI(uri string, args *ConnectionArgs) (ImageServer, error) { logger.Debug("Connecting to a remote OCI server", logger.Ctx{"URL": uri}) // Cleanup URL uri = strings.TrimSuffix(uri, "/") // Use empty args if not specified if args == nil { args = &ConnectionArgs{} } // Initialize the client struct server := ProtocolOCI{ httpHost: uri, httpUserAgent: args.UserAgent, httpCertificate: args.TLSServerCert, cache: map[string]ociInfo{}, tempPath: args.TempPath, } // Setup the HTTP client httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.IdenticalCertificate, args.Proxy, args.TransportWrapper) if err != nil { return nil, err } server.http = httpClient return &server, nil } // Internal function called by ConnectIncus and ConnectPublicIncus. func httpsIncus(ctx context.Context, requestURL string, args *ConnectionArgs) (InstanceServer, error) { // Use empty args if not specified if args == nil { args = &ConnectionArgs{} } httpBaseURL, err := url.Parse(requestURL) if err != nil { return nil, err } ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background()) // Initialize the client struct server := ProtocolIncus{ ctx: ctx, httpCertificate: args.TLSServerCert, httpBaseURL: *httpBaseURL, httpProtocol: "https", httpUserAgent: args.UserAgent, ctxConnected: ctxConnected, ctxConnectedCancel: ctxConnectedCancel, eventConns: make(map[string]*websocket.Conn), eventListeners: make(map[string][]*EventListener), skipEvents: args.SkipGetEvents, tempPath: args.TempPath, } if slices.Contains([]string{api.AuthenticationMethodOIDC}, args.AuthType) { server.RequireAuthenticated(true) } // Setup the HTTP client httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.IdenticalCertificate, args.Proxy, args.TransportWrapper) if err != nil { return nil, err } if args.CookieJar != nil { httpClient.Jar = args.CookieJar } server.http = httpClient if args.AuthType == api.AuthenticationMethodOIDC { server.setupOIDCClient(args.OIDCTokens) } // Test the connection and seed the server information if !args.SkipGetServer { _, _, err := server.GetServer() if err != nil { return nil, err } } return &server, nil }