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"
)
type ConnectionArgs struct {
TLSServerCert string
TLSClientCert string
TLSClientKey string
TLSCA string
UserAgent string
AuthType string
Proxy func(*http.Request) (*url.URL, error)
HTTPClient *http.Client
TransportWrapper func(*http.Transport) HTTPTransporter
InsecureSkipVerify bool
CookieJar http.CookieJar
OIDCTokens *oidc.Tokens[*oidc.IDTokenClaims]
SkipGetServer bool
CachePath string
CacheExpiry time.Duration
}
func ConnectIncus(url string, args *ConnectionArgs) (InstanceServer, error) {
return ConnectIncusWithContext(context.Background(), url, args)
}
func ConnectIncusWithContext(ctx context.Context, url string, args *ConnectionArgs) (InstanceServer, error) {
url = strings.TrimSuffix(url, "/")
logger.Debug("Connecting to a remote Incus over HTTPS", logger.Ctx{"url": url})
return httpsIncus(ctx, url, args)
}
func ConnectIncusHTTP(args *ConnectionArgs, client *http.Client) (InstanceServer, error) {
return ConnectIncusHTTPWithContext(context.Background(), args, client)
}
func ConnectIncusHTTPWithContext(ctx context.Context, args *ConnectionArgs, client *http.Client) (InstanceServer, error) {
logger.Debug("Connecting to a VM agent over a VM socket")
if args == nil {
args = &ConnectionArgs{}
}
httpBaseURL, err := url.Parse("https://custom.socket")
if err != nil {
return nil, err
}
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
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),
}
server.http = client
if !args.SkipGetServer {
serverStatus, _, err := server.GetServer()
if err != nil {
return nil, err
}
server.httpCertificate = serverStatus.Environment.Certificate
}
return &server, nil
}
func ConnectIncusUnix(path string, args *ConnectionArgs) (InstanceServer, error) {
return ConnectIncusUnixWithContext(context.Background(), path, args)
}
func ConnectIncusUnixWithContext(ctx context.Context, path string, args *ConnectionArgs) (InstanceServer, error) {
logger.Debug("Connecting to a local Incus over a Unix socket")
if args == nil {
args = &ConnectionArgs{}
}
httpBaseURL, err := url.Parse("http://unix.socket")
if err != nil {
return nil, err
}
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
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) {
path = userPath
projectName = fmt.Sprintf("user-%d", os.Geteuid())
}
}
}
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),
project: projectName,
}
httpClient, err := unixHTTPClient(args, path)
if err != nil {
return nil, err
}
server.http = httpClient
if !args.SkipGetServer {
serverStatus, _, err := server.GetServer()
if err != nil {
return nil, err
}
server.httpCertificate = serverStatus.Environment.Certificate
}
return &server, nil
}
func ConnectPublicIncus(url string, args *ConnectionArgs) (ImageServer, error) {
return ConnectPublicIncusWithContext(context.Background(), url, args)
}
func ConnectPublicIncusWithContext(ctx context.Context, url string, args *ConnectionArgs) (ImageServer, error) {
logger.Debug("Connecting to a remote public Incus over HTTPS")
url = strings.TrimSuffix(url, "/")
return httpsIncus(ctx, url, args)
}
func ConnectSimpleStreams(url string, args *ConnectionArgs) (ImageServer, error) {
logger.Debug("Connecting to a remote simplestreams server", logger.Ctx{"URL": url})
url = strings.TrimSuffix(url, "/")
if args == nil {
args = &ConnectionArgs{}
}
server := ProtocolSimpleStreams{
httpHost: url,
httpUserAgent: args.UserAgent,
httpCertificate: args.TLSServerCert,
}
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.Proxy, args.TransportWrapper)
if err != nil {
return nil, err
}
server.http = httpClient
ssClient := simplestreams.NewClient(url, *httpClient, args.UserAgent)
server.ssClient = ssClient
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(url)))
cachePath := filepath.Join(args.CachePath, hashedURL)
cacheExpiry := args.CacheExpiry
if cacheExpiry == 0 {
cacheExpiry = time.Hour
}
if !util.PathExists(cachePath) {
err := os.Mkdir(cachePath, 0755)
if err != nil {
return nil, err
}
}
ssClient.SetCache(cachePath, cacheExpiry)
}
return &server, nil
}
func ConnectOCI(uri string, args *ConnectionArgs) (ImageServer, error) {
logger.Debug("Connecting to a remote OCI server", logger.Ctx{"URL": uri})
uri = strings.TrimSuffix(uri, "/")
if args == nil {
args = &ConnectionArgs{}
}
server := ProtocolOCI{
httpHost: uri,
httpUserAgent: args.UserAgent,
httpCertificate: args.TLSServerCert,
cache: map[string]ociInfo{},
}
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, args.Proxy, args.TransportWrapper)
if err != nil {
return nil, err
}
server.http = httpClient
return &server, nil
}
func httpsIncus(ctx context.Context, requestURL string, args *ConnectionArgs) (InstanceServer, error) {
if args == nil {
args = &ConnectionArgs{}
}
httpBaseURL, err := url.Parse(requestURL)
if err != nil {
return nil, err
}
ctxConnected, ctxConnectedCancel := context.WithCancel(context.Background())
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),
}
if slices.Contains([]string{api.AuthenticationMethodOIDC}, args.AuthType) {
server.RequireAuthenticated(true)
}
httpClient, err := tlsHTTPClient(args.HTTPClient, args.TLSClientCert, args.TLSClientKey, args.TLSCA, args.TLSServerCert, args.InsecureSkipVerify, 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)
}
if !args.SkipGetServer {
_, _, err := server.GetServer()
if err != nil {
return nil, err
}
}
return &server, nil
}