Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/OneOfOne/xxhash v1.2.8
github.com/PaesslerAG/gval v1.2.3
github.com/PaesslerAG/jsonpath v0.1.1
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e
github.com/apache/pulsar-client-go v0.15.0
github.com/aws/aws-lambda-go v1.46.0
github.com/aws/aws-msk-iam-sasl-signer-go v1.0.4
Expand Down Expand Up @@ -184,6 +185,7 @@ require (
github.com/gomlx/gopjrt v0.7.3 // indirect
github.com/gomlx/onnx-gomlx v0.2.4 // indirect
github.com/hamba/avro/v2 v2.26.0 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
Expand Down Expand Up @@ -330,7 +332,7 @@ require (
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
Expand Down Expand Up @@ -1305,7 +1307,9 @@ github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkM
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
Expand Down
43 changes: 28 additions & 15 deletions internal/httpclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package httpclient
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -75,34 +76,46 @@ func NewClientFromOldConfig(conf OldConfig, mgr *service.Resources, opts ...Requ
h.client.Timeout = conf.Timeout
}

var tlsClientConfig *tls.Config
var proxy func(*http.Request) (*url.URL, error)

if conf.TLSEnabled && conf.TLSConf != nil {
if c, ok := http.DefaultTransport.(*http.Transport); ok {
cloned := c.Clone()
cloned.TLSClientConfig = conf.TLSConf
h.client.Transport = cloned
} else {
h.client.Transport = &http.Transport{
TLSClientConfig: conf.TLSConf,
}
}
tlsClientConfig = conf.TLSConf
}

if conf.ProxyURL != "" {
proxyURL, err := url.Parse(conf.ProxyURL)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy_url string: %v", err)
}
if h.client.Transport != nil {
if tr, ok := h.client.Transport.(*http.Transport); ok {
tr.Proxy = http.ProxyURL(proxyURL)
} else {
return nil, fmt.Errorf("unable to apply proxy_url to transport, unexpected type %T", h.client.Transport)
proxy = http.ProxyURL(proxyURL)
}

if tlsClientConfig != nil || proxy != nil {
if c, ok := http.DefaultTransport.(*http.Transport); ok {
cloned := c.Clone()

if tlsClientConfig != nil {
cloned.TLSClientConfig = tlsClientConfig
}

if proxy != nil {
cloned.Proxy = proxy
}

h.client.Transport = cloned
} else {
h.client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: tlsClientConfig,
Proxy: proxy,
}
}

if conf.Negotiate {
h.client.Transport = newSPPNEGORoundTripper(h.client.Transport)
}
} else if conf.Negotiate {
h.client.Transport = newSPPNEGORoundTripper(http.DefaultTransport)
}

h.client.Transport, err = newRequestLog(h.client.Transport, h.log, conf.DumpRequestLogLevel)
Expand Down
7 changes: 7 additions & 0 deletions internal/httpclient/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
hcFieldDumpRequestLogLevel = "dump_request_log_level"
hcFieldTLS = "tls"
hcFieldProxyURL = "proxy_url"
hcFieldNegotiate = "negotiate"
)

// ConfigField returns a public API config field spec for an HTTP component,
Expand Down Expand Up @@ -101,6 +102,10 @@ func ConfigField(defaultVerb string, forOutput bool, extraChildren ...*service.C
Description("An optional HTTP proxy URL.").
Advanced().
Optional(),
service.NewBoolField(hcFieldNegotiate).
Description("Use Negotiate (SPNEGO) authentication.").
Advanced().
Default(false),
)

innerFields = append(innerFields, extraChildren...)
Expand Down Expand Up @@ -154,6 +159,7 @@ func ConfigFromParsed(pConf *service.ParsedConfig) (conf OldConfig, err error) {
return
}
conf.ProxyURL, _ = pConf.FieldString(hcFieldProxyURL)
conf.Negotiate, _ = pConf.FieldBool(hcFieldNegotiate)
if conf.authSigner, err = pConf.HTTPRequestAuthSignerFromParsed(); err != nil {
return
}
Expand Down Expand Up @@ -182,6 +188,7 @@ type OldConfig struct {
TLSEnabled bool
TLSConf *tls.Config
ProxyURL string
Negotiate bool
authSigner func(f fs.FS, req *http.Request) error
clientCtor func(context.Context, *http.Client) *http.Client
}
165 changes: 165 additions & 0 deletions internal/httpclient/spnego_kerberos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//go:build !windows
// +build !windows

package httpclient

import (
"bytes"
"io"
"net/http"
"os"
"os/user"
"runtime"
"strings"

"github.com/jcmturner/gokrb5/v8/client"
"github.com/jcmturner/gokrb5/v8/config"
"github.com/jcmturner/gokrb5/v8/credentials"
"github.com/jcmturner/gokrb5/v8/spnego"
)

func kerberosConfigFromPath(path string) (*config.Config, error) {
file, err := os.Open(path)

if err != nil {
return nil, err
}

defer file.Close()

return config.NewFromReader(file)
}

func kerberosConfig() (*config.Config, error) {
configPath := os.Getenv("KRB5_CONFIG")

if len(configPath) == 0 {
switch runtime.GOOS {
case "linux":
configPath = "/etc/krb5.conf"
case "darwin":
configPath = "/opt/local/etc/krb5.conf"
default:
configPath = "/etc/krb5/krb5.conf"
}
return kerberosConfigFromPath(configPath)
}

config, err := kerberosConfigFromPath(configPath)

if err != nil {
if os.IsNotExist(err) {
switch runtime.GOOS {
case "linux":
configPath = "/etc/krb5.conf"
case "darwin":
configPath = "/opt/local/etc/krb5.conf"
default:
configPath = "/etc/krb5/krb5.conf"
}
return kerberosConfigFromPath(configPath)
}
}

return config, nil
}

type kerberosTransport struct {
http.RoundTripper
}

func (t *kerberosTransport) RoundTrip(req *http.Request) (*http.Response, error) {
body := bytes.Buffer{}
if req.Body != nil {
_, err := body.ReadFrom(req.Body)
if err != nil {
return nil, err
}
req.Body.Close()
req.Body = io.NopCloser(bytes.NewReader(body.Bytes()))
}
res, err := t.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusUnauthorized {
return res, err
}

wwwAuthenticateHeaders := res.Header.Values("WWW-Authenticate")

hasAuth := false

for _, wwwAuthenticate := range wwwAuthenticateHeaders {
if strings.HasPrefix(wwwAuthenticate, "Negotiate ") {
hasAuth = true
break
}

if strings.HasPrefix(wwwAuthenticate, "NTLM ") {
hasAuth = true
break
}
}

if !hasAuth {
return res, err
}

config, err := kerberosConfig()
if err != nil {
return nil, err
}

var ccpath string

ccname := os.Getenv("KRB5CCNAME")
if strings.HasPrefix(ccname, "FILE:") {
ccpath = ccname[len("FILE:"):]
} else {
u, err := user.Current()
if err != nil {
return nil, err
}

ccpath = "/tmp/krb5cc_" + u.Uid
}

ccache, err := credentials.LoadCCache(ccpath)
if err != nil {
return nil, err
}

client, err := client.NewFromCCache(ccache, config, client.DisablePAFXFAST(true))

if err != nil {
return nil, err
}

spn := req.Host

if len(spn) == 0 {
spn = req.URL.Host
}

spn = "http/" + spn

err = spnego.SetSPNEGOHeader(client, req, spn)

if err != nil {
return nil, err
}

req.Body = io.NopCloser(bytes.NewReader(body.Bytes()))

return t.RoundTripper.RoundTrip(req)
}

func newSPPNEGORoundTripper(base http.RoundTripper) http.RoundTripper {
if base == nil {
base = http.DefaultTransport
}
return &kerberosTransport{
RoundTripper: base,
}
}
Loading