From e59887da05a57866cd78b5a83ddb62bb8f7eca12 Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Mon, 12 Jan 2026 10:40:49 -0500 Subject: [PATCH] allow configuring a unix socket proxy --- cmd/humioctl/profiles.go | 11 ++++++----- cmd/humioctl/profiles_add.go | 22 ++++++++++++---------- cmd/humioctl/profiles_set_default.go | 10 ++++++---- cmd/humioctl/root.go | 8 +++++++- internal/api/client.go | 1 + internal/api/httpclient.go | 21 +++++++++++++++++++++ internal/viperkey/viperkey.go | 1 + 7 files changed, 54 insertions(+), 20 deletions(-) diff --git a/cmd/humioctl/profiles.go b/cmd/humioctl/profiles.go index d861bb5d..9f5fe355 100644 --- a/cmd/humioctl/profiles.go +++ b/cmd/humioctl/profiles.go @@ -10,11 +10,12 @@ import ( ) type login struct { - address string - token string - username string - caCertificate string - insecure bool + address string + token string + username string + caCertificate string + insecure bool + unixSocketProxy string } // usersCmd represents the users command diff --git a/cmd/humioctl/profiles_add.go b/cmd/humioctl/profiles_add.go index ae09d4a0..1ead3f40 100644 --- a/cmd/humioctl/profiles_add.go +++ b/cmd/humioctl/profiles_add.go @@ -67,11 +67,12 @@ func addAccount(newName string, profile *login) { } profiles[newName] = map[string]interface{}{ - viperkey.Address: profile.address, - viperkey.Token: profile.token, - viperkey.Username: profile.username, - viperkey.CACertificate: profile.caCertificate, - viperkey.Insecure: profile.insecure, + viperkey.Address: profile.address, + viperkey.Token: profile.token, + viperkey.Username: profile.username, + viperkey.CACertificate: profile.caCertificate, + viperkey.Insecure: profile.insecure, + viperkey.UnixSocketProxy: profile.unixSocketProxy, } viper.Set(viperkey.Profiles, profiles) @@ -79,11 +80,12 @@ func addAccount(newName string, profile *login) { func mapToLogin(data interface{}) *login { return &login{ - address: getMapKeyString(data, viperkey.Address), - username: getMapKeyString(data, viperkey.Username), - token: getMapKeyString(data, viperkey.Token), - caCertificate: getMapKeyString(data, viperkey.CACertificate), - insecure: getMapKeyBool(data, viperkey.Insecure), + address: getMapKeyString(data, viperkey.Address), + username: getMapKeyString(data, viperkey.Username), + token: getMapKeyString(data, viperkey.Token), + caCertificate: getMapKeyString(data, viperkey.CACertificate), + insecure: getMapKeyBool(data, viperkey.Insecure), + unixSocketProxy: getMapKeyString(data, viperkey.UnixSocketProxy), } } diff --git a/cmd/humioctl/profiles_set_default.go b/cmd/humioctl/profiles_set_default.go index 4a7f83b1..bee90488 100644 --- a/cmd/humioctl/profiles_set_default.go +++ b/cmd/humioctl/profiles_set_default.go @@ -22,6 +22,7 @@ func newProfilesSetDefaultCmd() *cobra.Command { viper.Set(viperkey.Token, profile.token) viper.Set(viperkey.CACertificateFile, profile.caCertificate) viper.Set(viperkey.Insecure, profile.insecure) + viper.Set(viperkey.UnixSocketProxy, profile.unixSocketProxy) err = saveConfig() exitOnError(cmd, err, "Error saving config") @@ -44,10 +45,11 @@ func loadProfile(profileName string) (*login, error) { insecureFromProfileData, _ := profileData[viperkey.Insecure].(bool) // false if not found in map, or type isn't bool profile := login{ - address: getMapKeyString(profileData, viperkey.Address), - token: getMapKeyString(profileData, viperkey.Token), - caCertificate: getMapKeyString(profileData, viperkey.CACertificate), - insecure: insecureFromProfileData, + address: getMapKeyString(profileData, viperkey.Address), + token: getMapKeyString(profileData, viperkey.Token), + caCertificate: getMapKeyString(profileData, viperkey.CACertificate), + insecure: insecureFromProfileData, + unixSocketProxy: getMapKeyString(profileData, viperkey.UnixSocketProxy), } return &profile, nil diff --git a/cmd/humioctl/root.go b/cmd/humioctl/root.go index d5c0fa51..e91f5013 100644 --- a/cmd/humioctl/root.go +++ b/cmd/humioctl/root.go @@ -28,7 +28,7 @@ import ( "github.com/spf13/viper" ) -var cfgFile, tokenFile, token, address, caCertificateFile, profileFlag, proxyOrganization string +var cfgFile, tokenFile, token, address, caCertificateFile, profileFlag, proxyOrganization, unixSocketProxy string var insecure bool var printVersion bool @@ -96,6 +96,7 @@ Common Management Commands: rootCmd.PersistentFlags().StringVar(&caCertificateFile, "ca-certificate-file", "", "File path to a file containing the CA certificate in PEM format. Overrides the value in your config file.") rootCmd.PersistentFlags().BoolVar(&insecure, "insecure", false, "By default, all encrypted connections will verify that the hostname in the TLS certificate matches the name from the URL. Set this to true to ignore hostname validation.") rootCmd.PersistentFlags().StringVar(&proxyOrganization, "proxy-organization", "", "Commands are executed in the specified organization.") + rootCmd.PersistentFlags().StringVar(&unixSocketProxy, "unix-socket-proxy", "", "Path to a unix socket to use as an HTTP proxy.") rootCmd.PersistentFlags().String("format", "", "Change output format of commands, if supported. Valid formats: json") _ = viper.BindPFlag(viperkey.Address, rootCmd.PersistentFlags().Lookup("address")) @@ -104,6 +105,7 @@ Common Management Commands: _ = viper.BindPFlag(viperkey.CACertificateFile, rootCmd.PersistentFlags().Lookup("ca-certificate-file")) _ = viper.BindPFlag(viperkey.Insecure, rootCmd.PersistentFlags().Lookup("insecure")) _ = viper.BindPFlag(viperkey.ProxyOrganization, rootCmd.PersistentFlags().Lookup("proxy-organization")) + _ = viper.BindPFlag(viperkey.UnixSocketProxy, rootCmd.PersistentFlags().Lookup("unix-socket-proxy")) rootCmd.Flags().BoolVarP(&printVersion, "version", "v", false, "Print the client version") @@ -182,6 +184,9 @@ func initConfig() { if !insecure { viper.Set(viperkey.Insecure, profile.insecure) } + if unixSocketProxy == "" { + viper.Set(viperkey.UnixSocketProxy, profile.unixSocketProxy) + } } if tokenFile != "" { @@ -223,6 +228,7 @@ func newApiClientE(opts ...func(config *api.Config)) (*api.Client, error) { config.CACertificatePEM = viper.GetString(viperkey.CACertificate) config.Insecure = viper.GetBool(viperkey.Insecure) config.ProxyOrganization = viper.GetString(viperkey.ProxyOrganization) + config.UnixSocketProxy = viper.GetString(viperkey.UnixSocketProxy) config.UserAgent = fmt.Sprintf("humioctl/%s (%s on %s)", version, commit, date) for _, opt := range opts { diff --git a/internal/api/client.go b/internal/api/client.go index fb8771bf..73abdd4b 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -174,6 +174,7 @@ type Config struct { CACertificatePEM string Insecure bool ProxyOrganization string + UnixSocketProxy string DialContext func(ctx context.Context, network, addr string) (net.Conn, error) } diff --git a/internal/api/httpclient.go b/internal/api/httpclient.go index b0cbcadc..dcfd572c 100644 --- a/internal/api/httpclient.go +++ b/internal/api/httpclient.go @@ -1,8 +1,10 @@ package api import ( + "context" "crypto/tls" "crypto/x509" + "fmt" "net" "net/http" "time" @@ -16,6 +18,20 @@ type headerTransport struct { headers map[string]string } +// newUnixSocketProxyDialer creates a dialer that connects through a unix socket proxy +func newUnixSocketProxyDialer(socketPath string, fallbackDialer func(ctx context.Context, network, addr string) (net.Conn, error)) func(ctx context.Context, network, addr string) (net.Conn, error) { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + // Connect directly to the unix socket + // The proxy is expected to handle the connection transparently + conn, err := net.Dial("unix", socketPath) + if err != nil { + return nil, fmt.Errorf("failed to connect to unix socket proxy: %w", err) + } + + return conn, nil + } +} + func NewHttpTransport(config Config) *http.Transport { dialContext := config.DialContext if dialContext == nil { @@ -26,6 +42,11 @@ func NewHttpTransport(config Config) *http.Transport { }).DialContext } + // If a unix socket proxy is specified, wrap the dialer to use it + if config.UnixSocketProxy != "" { + dialContext = newUnixSocketProxyDialer(config.UnixSocketProxy, dialContext) + } + if config.Insecure { // Return HTTP transport where we skip certificate verification return &http.Transport{ diff --git a/internal/viperkey/viperkey.go b/internal/viperkey/viperkey.go index ddda1e0d..102b6bc3 100644 --- a/internal/viperkey/viperkey.go +++ b/internal/viperkey/viperkey.go @@ -10,4 +10,5 @@ const ( Username = "username" Profiles = "profiles" ProxyOrganization = "proxy-organization" + UnixSocketProxy = "unix-socket-proxy" )