diff --git a/client/httpclient.go b/client/httpclient.go index 00249777..23ac6a88 100644 --- a/client/httpclient.go +++ b/client/httpclient.go @@ -55,6 +55,12 @@ func (c *httpClient) Start(ctx context.Context, settings types.StartSettings) er c.sender.EnableCompression() } + if settings.ProxyURL != "" { + if err := c.sender.SetProxy(settings.ProxyURL, settings.ProxyHeaders); err != nil { + return err + } + } + // Prepare the first message to send. err := c.common.PrepareFirstMessage(ctx) if err != nil { diff --git a/client/internal/httpsender.go b/client/internal/httpsender.go index 3d768864..a377d4ce 100644 --- a/client/internal/httpsender.go +++ b/client/internal/httpsender.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "net/url" "sync" "sync/atomic" "time" @@ -84,6 +85,35 @@ func NewHTTPSender(logger types.Logger) *HTTPSender { return h } +// SetProxy will force each request to use passed proxy and use the passed headers when making a CONNECT request to the proxy. +// If the proxy has no schema http is used. +// This method is not thread safe and must be called before h.client is used. +func (h *HTTPSender) SetProxy(proxy string, headers http.Header) error { + proxyURL, err := url.Parse(proxy) + if err != nil || proxyURL.Scheme == "" || proxyURL.Host == "" { // error or bad URL - try to use http as scheme to resolve + proxyURL, err = url.Parse("http://" + proxy) + if err != nil { + return err + } + } + if proxyURL.Hostname() == "" { + return url.InvalidHostError(proxy) + } + + proxyTransport := &http.Transport{} + if h.client.Transport != nil { + transport, ok := h.client.Transport.(*http.Transport) + if !ok { + return fmt.Errorf("unable to coorce client transport as *http.Transport detected type is: %T", h.client.Transport) + } + proxyTransport = transport.Clone() + } + proxyTransport.Proxy = http.ProxyURL(proxyURL) + proxyTransport.ProxyConnectHeader = headers + h.client.Transport = proxyTransport + return nil +} + // Run starts the processing loop that will perform the HTTP request/response. // When there are no more messages to send Run will suspend until either there is // a new message to send or the polling interval elapses. diff --git a/client/internal/httpsender_test.go b/client/internal/httpsender_test.go index 75fff53c..73bda16e 100644 --- a/client/internal/httpsender_test.go +++ b/client/internal/httpsender_test.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/url" "sync" "sync/atomic" "testing" @@ -356,3 +357,147 @@ func TestPackageUpdatesWithError(t *testing.T) { cancel() } + +func TestHTTPSenderSetProxy(t *testing.T) { + tests := []struct { + name string + url string + err error + }{{ + name: "http proxy", + url: "http://proxy.internal:8080", + err: nil, + }, { + name: "socks5 proxy", + url: "socks5://proxy.internal:8080", + err: nil, + }, { + name: "no schema", + url: "proxy.internal:8080", + err: nil, + }, { + name: "empty url", + url: "", + err: url.InvalidHostError(""), + }, { + name: "invalid url", + url: "this is not valid", + err: url.InvalidHostError("this is not valid"), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sender := NewHTTPSender(&sharedinternal.NopLogger{}) + err := sender.SetProxy(tc.url, nil) + if tc.err != nil { + assert.ErrorAs(t, err, &tc.err) + } else { + assert.NoError(t, err) + } + }) + } + + t.Run("old transport settings are preserved", func(t *testing.T) { + sender := &HTTPSender{ + client: &http.Client{ + Transport: &http.Transport{ + MaxResponseHeaderBytes: 1024, + }, + }, + } + err := sender.SetProxy("https://proxy.internal:8080", nil) + assert.NoError(t, err) + transport, ok := sender.client.Transport.(*http.Transport) + if !ok { + t.Logf("Transport: %v", sender.client.Transport) + t.Fatalf("Unable to coorce as *http.Transport detected type: %T", sender.client.Transport) + } + assert.NotNil(t, transport.Proxy) + assert.Equal(t, int64(1024), transport.MaxResponseHeaderBytes) + }) + + t.Run("test https proxy", func(t *testing.T) { + var connected atomic.Bool + // HTTPS Connect proxy, no auth required + proxyServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + t.Logf("Request: %+v", req) + if req.Method != http.MethodConnect { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + connected.Store(true) + + targetConn, err := net.DialTimeout("tcp", req.Host, 10*time.Second) + if err != nil { + w.WriteHeader(http.StatusBadGateway) + return + } + defer targetConn.Close() + + hijacker, ok := w.(http.Hijacker) + if !ok { + w.WriteHeader(http.StatusBadGateway) + return + } + clientConn, _, err := hijacker.Hijack() + if err != nil { + t.Logf("Hijack error: %v", err) + w.WriteHeader(http.StatusBadGateway) + return + } + clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) + defer clientConn.Close() + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + _, err := io.Copy(targetConn, clientConn) + assert.NoError(t, err, "proxy encountered an error copying to destination") + }() + go func() { + defer wg.Done() + _, err := io.Copy(clientConn, targetConn) + assert.NoError(t, err, "proxy encountered an error copying to client") + }() + wg.Wait() + })) + t.Cleanup(proxyServer.Close) + + srv := StartTLSMockServer(t) + t.Cleanup(srv.Close) + srv.OnRequest = func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + } + + sender := NewHTTPSender(&sharedinternal.NopLogger{}) + sender.client = proxyServer.Client() + err := sender.SetProxy(proxyServer.URL, http.Header{"test-header": []string{"test-value"}}) + assert.NoError(t, err) + + t.Logf("Proxy URL: %s", proxyServer.URL) + + sender.NextMessage().Update(func(msg *protobufs.AgentToServer) { + msg.AgentDescription = &protobufs.AgentDescription{ + IdentifyingAttributes: []*protobufs.KeyValue{{ + Key: "service.name", + Value: &protobufs.AnyValue{ + Value: &protobufs.AnyValue_StringValue{StringValue: "test-service"}, + }, + }}, + } + }) + sender.callbacks = types.Callbacks{ + OnConnect: func(_ context.Context) { + }, + OnConnectFailed: func(_ context.Context, err error) { + t.Logf("sender failed to connect: %v", err) + }, + } + sender.url = "https://" + srv.Endpoint + + resp, err := sender.sendRequestWithRetries(context.Background()) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.True(t, connected.Load(), "test request did not use proxy") + }) +} diff --git a/client/types/startsettings.go b/client/types/startsettings.go index c53c141f..7faa62d1 100644 --- a/client/types/startsettings.go +++ b/client/types/startsettings.go @@ -26,6 +26,12 @@ type StartSettings struct { // Optional TLS config for HTTP connection. TLSConfig *tls.Config + // Optional Proxy configuration + // The ProxyURL may be http(s) or socks5; if no schema is specified http is assumed. + ProxyURL string + // ProxyHeaders gives the headers an HTTP client will present on a proxy CONNECT request. + ProxyHeaders http.Header + // Agent information. InstanceUid InstanceUid diff --git a/client/wsclient.go b/client/wsclient.go index 2ef7a7ae..4ec99b2a 100644 --- a/client/wsclient.go +++ b/client/wsclient.go @@ -2,15 +2,19 @@ package client import ( "context" + "crypto/tls" "errors" + "net" "net/http" "net/url" + "strings" "sync" "sync/atomic" "time" "github.com/cenkalti/backoff/v4" "github.com/gorilla/websocket" + dialer "github.com/michel-laterman/proxy-connect-dialer-go" "github.com/open-telemetry/opamp-go/client/internal" "github.com/open-telemetry/opamp-go/client/types" @@ -81,6 +85,12 @@ func (c *wsClient) Start(ctx context.Context, settings types.StartSettings) erro // Prepare connection settings. c.dialer = *websocket.DefaultDialer + if settings.ProxyURL != "" { + if err := c.useProxy(settings.ProxyURL, settings.ProxyHeaders, settings.TLSConfig); err != nil { + return err + } + } + var err error c.url, err = url.Parse(settings.OpAMPServerURL) if err != nil { @@ -426,3 +436,52 @@ func (c *wsClient) runUntilStopped(ctx context.Context) { c.runOneCycle(ctx) } } + +// useProxy sets the websocket dialer to use the passed proxy URL. +// If the proxy has no schema http is used. +// This method is not thread safe and must be called before c.dialer is used. +func (c *wsClient) useProxy(proxy string, headers http.Header, cfg *tls.Config) error { + proxyURL, err := url.Parse(proxy) + if err != nil || proxyURL.Scheme == "" || proxyURL.Host == "" { // error or bad URL - try to use http as scheme to resolve + proxyURL, err = url.Parse("http://" + proxy) + if err != nil { + return err + } + } + if proxyURL.Hostname() == "" { + return url.InvalidHostError(proxy) + } + + // Clear previous settings + c.dialer.Proxy = nil + c.dialer.NetDialContext = nil + c.dialer.NetDialTLSContext = nil + + switch strings.ToLower(proxyURL.Scheme) { + case "http": + // FIXME: dialer.NetDialContext is currently used as a work around instead of setting dialer.Proxy as gorilla/websockets does not have 1st class support for setting proxy connect headers + // Once http://github.com/gorilla/websocket/issues/479 is complete, we should use dialer.Proxy, and dialer.ProxyConnectHeader + if len(headers) > 0 { + dialer, err := dialer.NewProxyConnectDialer(proxyURL, &net.Dialer{}, dialer.WithProxyConnectHeaders(headers)) + if err != nil { + return err + } + c.dialer.NetDialContext = dialer.DialContext + return nil + } + c.dialer.Proxy = http.ProxyURL(proxyURL) // No connect headers, use a regular proxy + case "https": + if len(headers) > 0 { + dialer, err := dialer.NewProxyConnectDialer(proxyURL, &net.Dialer{}, dialer.WithTLS(cfg), dialer.WithProxyConnectHeaders(headers)) + if err != nil { + return err + } + c.dialer.NetDialTLSContext = dialer.DialContext + return nil + } + c.dialer.Proxy = http.ProxyURL(proxyURL) // No connect headers, use a regular proxy + default: // catches socks5 + c.dialer.Proxy = http.ProxyURL(proxyURL) + } + return nil +} diff --git a/client/wsclient_test.go b/client/wsclient_test.go index 0fe4ec5a..7c6ffb0b 100644 --- a/client/wsclient_test.go +++ b/client/wsclient_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "io" + "net" "net/http" "net/http/httptest" "net/url" @@ -866,3 +868,140 @@ func TestWSSenderReportsAvailableComponents(t *testing.T) { }) } } + +func TestWSClientUseProxy(t *testing.T) { + tests := []struct { + name string + headers http.Header + url string + err error + }{{ + name: "http proxy", + headers: nil, + url: "http://proxy.internal:8080", + err: nil, + }, { + name: "https proxy", + headers: nil, + url: "https://proxy.internal:8080", + err: nil, + }, { + name: "socks5 proxy", + url: "socks5://proxy.internal:8080", + err: nil, + }, { + name: "no schema", + headers: nil, + url: "proxy.internal:8080", + err: nil, + }, { + name: "empty url", + url: "", + err: url.InvalidHostError(""), + }, { + name: "http proxy with headers", + headers: http.Header{"test-key": []string{"test-val"}}, + url: "http://proxy.internal:8080", + err: nil, + }, { + name: "https proxy with headers", + headers: http.Header{"test-key": []string{"test-val"}}, + url: "https://proxy.internal:8080", + err: nil, + }, { + name: "invalid url", + url: "this is not valid", + err: url.InvalidHostError("this is not valid"), + }} + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + client := &wsClient{ + dialer: websocket.Dialer{}, + } + err := client.useProxy(tc.url, nil, nil) + if tc.err != nil { + assert.ErrorAs(t, err, &tc.err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestWSClientUseHTTPProxy(t *testing.T) { + var connected atomic.Bool + // HTTPS Connect proxy, no auth required + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + t.Logf("Request: %+v", req) + if req.Method != http.MethodConnect { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + connected.Store(true) + + targetConn, err := net.DialTimeout("tcp", req.Host, 10*time.Second) + if err != nil { + w.WriteHeader(http.StatusBadGateway) + return + } + defer targetConn.Close() + + hijacker, ok := w.(http.Hijacker) + if !ok { + w.WriteHeader(http.StatusBadGateway) + return + } + clientConn, _, err := hijacker.Hijack() + if err != nil { + t.Logf("Hijack error: %v", err) + w.WriteHeader(http.StatusBadGateway) + return + } + clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) + defer clientConn.Close() + + var wg sync.WaitGroup + wg.Add(2) + go func() { + defer wg.Done() + _, err := io.Copy(targetConn, clientConn) + assert.NoError(t, err, "proxy encountered an error copying to destination") + }() + go func() { + defer wg.Done() + _, err := io.Copy(clientConn, targetConn) + assert.NoError(t, err, "proxy encountered an error copying to client") + }() + wg.Wait() + })) + t.Cleanup(proxyServer.Close) + t.Logf("Proxy server: %s", proxyServer.URL) + + var serverConnected atomic.Bool + srv := internal.StartMockServer(t) + t.Cleanup(srv.Close) + srv.OnMessage = func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent { + serverConnected.Store(true) + return nil + } + t.Logf("Server endpoint: %s", srv.Endpoint) + + settings := types.StartSettings{ + OpAMPServerURL: "http://" + srv.Endpoint, + ProxyURL: proxyServer.URL, + ProxyHeaders: http.Header{"test-key": []string{"test-val"}}, + } + client := NewWebSocket(nil) + startClient(t, settings, client) + + assert.Eventually(t, func() bool { + return connected.Load() + }, 3*time.Second, 10*time.Millisecond, "WS client did not connect to proxy") + + assert.Eventually(t, func() bool { + return serverConnected.Load() + }, 3*time.Second, 10*time.Millisecond, "WS client did not connect to server") + + err := client.Stop(context.Background()) + assert.NoError(t, err) +} diff --git a/go.mod b/go.mod index f2a9a3c6..71f5aae7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 github.com/stretchr/testify v1.10.0 google.golang.org/protobuf v1.36.7 ) diff --git a/go.sum b/go.sum index 3f21c093..ddc8cc48 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 h1:Q8asukpmyrEheocd+R+6YEI4jcm62sHHalgTMG+LoLw= +github.com/michel-laterman/proxy-connect-dialer-go v0.1.0/go.mod h1:HTlVkRAqzTRPYbWxgAiwMT9HRZMOqP3Mx7+toa3yJjc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= diff --git a/internal/examples/agent/agent/agent.go b/internal/examples/agent/agent/agent.go index 4c01e9c0..2fbac163 100644 --- a/internal/examples/agent/agent/agent.go +++ b/internal/examples/agent/agent/agent.go @@ -10,6 +10,7 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" + "net/http" "os" "runtime" "sort" @@ -67,12 +68,25 @@ type Agent struct { // certificate is used. opampClientCert *tls.Certificate - tlsConfig *tls.Config + tlsConfig *tls.Config + proxySettings *proxySettings certRequested bool clientPrivateKeyPEM []byte } +type proxySettings struct { + url string + headers http.Header +} + +func (p *proxySettings) Clone() *proxySettings { + return &proxySettings{ + url: p.url, + headers: p.headers.Clone(), + } +} + func NewAgent(logger types.Logger, agentType string, agentVersion string, initialInsecureConnection bool) *Agent { agent := &Agent{ effectiveConfig: localConfig, @@ -102,7 +116,7 @@ func NewAgent(logger types.Logger, agentType string, agentVersion string, initia } agent.tlsConfig = tlsConfig } - if err := agent.connect(agent.tlsConfig); err != nil { + if err := agent.connect(withTLSConfig(agent.tlsConfig)); err != nil { agent.logger.Errorf(context.Background(), "Cannot connect OpAMP client: %v", err) return nil } @@ -110,13 +124,31 @@ func NewAgent(logger types.Logger, agentType string, agentVersion string, initia return agent } -func (agent *Agent) connect(tlsConfig *tls.Config) error { +type settingsOp func(*types.StartSettings) + +// withTLSConfig sets the StartSettings.TLSConfig option. +func withTLSConfig(tlsConfig *tls.Config) settingsOp { + return func(settings *types.StartSettings) { + settings.TLSConfig = tlsConfig + } +} + +// withProxy sets the StartSettings.ProxyURL and StartSettings.ProxyHeaders options. +func withProxy(proxy *proxySettings) settingsOp { + return func(settings *types.StartSettings) { + if proxy == nil { + return + } + settings.ProxyURL = proxy.url + settings.ProxyHeaders = proxy.headers + } +} + +func (agent *Agent) connect(ops ...settingsOp) error { agent.opampClient = client.NewWebSocket(agent.logger) - agent.tlsConfig = tlsConfig settings := types.StartSettings{ OpAMPServerURL: "wss://127.0.0.1:4320/v1/opamp", - TLSConfig: agent.tlsConfig, InstanceUid: types.InstanceUid(agent.instanceId), Callbacks: types.Callbacks{ OnConnect: func(ctx context.Context) { @@ -146,6 +178,14 @@ func (agent *Agent) connect(tlsConfig *tls.Config) error { protobufs.AgentCapabilities_AgentCapabilities_AcceptsOpAMPConnectionSettings | protobufs.AgentCapabilities_AgentCapabilities_ReportsConnectionSettingsStatus, } + for _, op := range ops { + op(&settings) + } + agent.tlsConfig = settings.TLSConfig + agent.proxySettings = &proxySettings{ + url: settings.ProxyURL, + headers: settings.ProxyHeaders, + } err := agent.opampClient.SetAgentDescription(agent.agentDescription) if err != nil { @@ -495,7 +535,7 @@ func (agent *Agent) onMessage(ctx context.Context, msg *types.MessageData) { agent.requestClientCertificate() } -func (agent *Agent) tryChangeOpAMP(ctx context.Context, cert *tls.Certificate, tlsConfig *tls.Config) { +func (agent *Agent) tryChangeOpAMP(ctx context.Context, cert *tls.Certificate, tlsConfig *tls.Config, proxy *proxySettings) { agent.logger.Debugf(ctx, "Reconnecting to verify new OpAMP settings.\n") agent.disconnect(ctx) @@ -508,9 +548,18 @@ func (agent *Agent) tryChangeOpAMP(ctx context.Context, cert *tls.Certificate, t tlsConfig.Certificates = []tls.Certificate{*cert} } - if err := agent.connect(tlsConfig); err != nil { + if proxy != nil { + agent.logger.Debugf(ctx, "Proxy settings revieved: %v\n", proxy) + } + + oldProxy := agent.proxySettings + if proxy == nil && oldProxy != nil { + proxy = oldProxy.Clone() + } + + if err := agent.connect(withTLSConfig(tlsConfig), withProxy(proxy)); err != nil { agent.logger.Errorf(ctx, "Cannot connect after using new tls config: %s. Ignoring the offer\n", err) - if err := agent.connect(oldCfg); err != nil { + if err := agent.connect(withTLSConfig(oldCfg), withProxy(oldProxy)); err != nil { agent.logger.Errorf(ctx, "Unable to reconnect after restoring tls config: %s\n", err) } return @@ -570,8 +619,17 @@ func (agent *Agent) onOpampConnectionSettings(ctx context.Context, settings *pro agent.logger.Debugf(ctx, "CA in offered settings.\n") } } + + // proxy settings + var proxy *proxySettings + if settings.Proxy != nil { + proxy = &proxySettings{ + url: settings.Proxy.Url, + headers: toHeaders(settings.Proxy.ConnectHeaders), + } + } // TODO: also use settings.DestinationEndpoint and settings.Headers for future connections. - go agent.tryChangeOpAMP(ctx, cert, tlsConfig) + go agent.tryChangeOpAMP(ctx, cert, tlsConfig, proxy) return nil } @@ -639,6 +697,18 @@ func (agent *Agent) getCertFromSettings(certificate *protobufs.TLSCertificate) ( return &cert, nil } +// toHeaders transforms a *protobufs.Headers to an http.Header +func toHeaders(ph *protobufs.Headers) http.Header { + var header http.Header + if ph == nil { + return header + } + for _, h := range ph.Headers { + header.Set(h.Key, h.Value) + } + return header +} + func (agent *Agent) onConnectionSettings(ctx context.Context, settings *protobufs.ConnectionSettingsOffers) error { agent.logger.Debugf(context.Background(), "Received connection settings offers from server, hash=%x.", settings.Hash) // TODO handle traces, logs, and other connection settings diff --git a/internal/examples/go.mod b/internal/examples/go.mod index 50b15c8a..ec9831d0 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -29,6 +29,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect + github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 05516f67..abcb6972 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -88,6 +88,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/michel-laterman/proxy-connect-dialer-go v0.1.0 h1:Q8asukpmyrEheocd+R+6YEI4jcm62sHHalgTMG+LoLw= +github.com/michel-laterman/proxy-connect-dialer-go v0.1.0/go.mod h1:HTlVkRAqzTRPYbWxgAiwMT9HRZMOqP3Mx7+toa3yJjc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= diff --git a/internal/examples/server/uisrv/html/agent.html b/internal/examples/server/uisrv/html/agent.html index 5db3e3ae..bef4e38e 100644 --- a/internal/examples/server/uisrv/html/agent.html +++ b/internal/examples/server/uisrv/html/agent.html @@ -138,7 +138,11 @@