mirror of https://github.com/portainer/portainer
				
				
				
			fix(transport): portainer generated kubeconfig causes kubectl exec fail [R8S-430] (#929)
							parent
							
								
									bba3751268
								
							
						
					
					
						commit
						bdb2e2f417
					
				| 
						 | 
					@ -451,7 +451,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	snapshotService.Start()
 | 
						snapshotService.Start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
 | 
						proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService, jwtService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	helmPackageManager, err := initHelmPackageManager()
 | 
						helmPackageManager, err := initHelmPackageManager()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
 | 
				
			||||||
	handler := NewHandler(testhelpers.NewTestRequestBouncer())
 | 
						handler := NewHandler(testhelpers.NewTestRequestBouncer())
 | 
				
			||||||
	handler.DataStore = store
 | 
						handler.DataStore = store
 | 
				
			||||||
	handler.ProxyManager = proxy.NewManager(nil)
 | 
						handler.ProxyManager = proxy.NewManager(nil)
 | 
				
			||||||
	handler.ProxyManager.NewProxyFactory(nil, nil, nil, nil, nil, nil, nil, nil)
 | 
						handler.ProxyManager.NewProxyFactory(nil, nil, nil, nil, nil, nil, nil, nil, nil)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create all the environments and add them to the same edge group
 | 
						// Create all the environments and add them to the same edge group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,11 +24,12 @@ type (
 | 
				
			||||||
		kubernetesTokenCacheManager *kubernetes.TokenCacheManager
 | 
							kubernetesTokenCacheManager *kubernetes.TokenCacheManager
 | 
				
			||||||
		gitService                  portainer.GitService
 | 
							gitService                  portainer.GitService
 | 
				
			||||||
		snapshotService             portainer.SnapshotService
 | 
							snapshotService             portainer.SnapshotService
 | 
				
			||||||
 | 
							jwtService                  portainer.JWTService
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
 | 
					// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
 | 
				
			||||||
func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService) *ProxyFactory {
 | 
					func NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService, jwtService portainer.JWTService) *ProxyFactory {
 | 
				
			||||||
	return &ProxyFactory{
 | 
						return &ProxyFactory{
 | 
				
			||||||
		dataStore:                   dataStore,
 | 
							dataStore:                   dataStore,
 | 
				
			||||||
		signatureService:            signatureService,
 | 
							signatureService:            signatureService,
 | 
				
			||||||
| 
						 | 
					@ -38,6 +39,7 @@ func NewProxyFactory(dataStore dataservices.DataStore, signatureService portaine
 | 
				
			||||||
		kubernetesTokenCacheManager: kubernetesTokenCacheManager,
 | 
							kubernetesTokenCacheManager: kubernetesTokenCacheManager,
 | 
				
			||||||
		gitService:                  gitService,
 | 
							gitService:                  gitService,
 | 
				
			||||||
		snapshotService:             snapshotService,
 | 
							snapshotService:             snapshotService,
 | 
				
			||||||
 | 
							jwtService:                  jwtService,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,7 +38,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
 | 
						transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore, factory.jwtService)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -74,7 +74,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	endpointURL.Scheme = "http"
 | 
						endpointURL.Scheme = "http"
 | 
				
			||||||
	proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL)
 | 
						proxy := NewSingleHostReverseProxyWithHostHeader(endpointURL)
 | 
				
			||||||
	proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.signatureService, factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory)
 | 
						proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.signatureService, factory.reverseTunnelService, endpoint, tokenManager, factory.kubernetesClientFactory, factory.jwtService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return proxy, nil
 | 
						return proxy, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	proxy := NewSingleHostReverseProxyWithHostHeader(remoteURL)
 | 
						proxy := NewSingleHostReverseProxyWithHostHeader(remoteURL)
 | 
				
			||||||
	proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore)
 | 
						proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager, endpoint, factory.kubernetesClientFactory, factory.dataStore, factory.jwtService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return proxy, nil
 | 
						return proxy, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ type agentTransport struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
 | 
					// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
 | 
				
			||||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore) *agentTransport {
 | 
					func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore, jwtService portainer.JWTService) *agentTransport {
 | 
				
			||||||
	transport := &agentTransport{
 | 
						transport := &agentTransport{
 | 
				
			||||||
		baseTransport: newBaseTransport(
 | 
							baseTransport: newBaseTransport(
 | 
				
			||||||
			&http.Transport{
 | 
								&http.Transport{
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo
 | 
				
			||||||
			endpoint,
 | 
								endpoint,
 | 
				
			||||||
			k8sClientFactory,
 | 
								k8sClientFactory,
 | 
				
			||||||
			dataStore,
 | 
								dataStore,
 | 
				
			||||||
 | 
								jwtService,
 | 
				
			||||||
		),
 | 
							),
 | 
				
			||||||
		signatureService: signatureService,
 | 
							signatureService: signatureService,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ type edgeTransport struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
 | 
					// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent
 | 
				
			||||||
func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory) *edgeTransport {
 | 
					func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager, k8sClientFactory *cli.ClientFactory, jwtService portainer.JWTService) *edgeTransport {
 | 
				
			||||||
	transport := &edgeTransport{
 | 
						transport := &edgeTransport{
 | 
				
			||||||
		reverseTunnelService: reverseTunnelService,
 | 
							reverseTunnelService: reverseTunnelService,
 | 
				
			||||||
		signatureService:     signatureService,
 | 
							signatureService:     signatureService,
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ func NewEdgeTransport(dataStore dataservices.DataStore, signatureService portain
 | 
				
			||||||
			endpoint,
 | 
								endpoint,
 | 
				
			||||||
			k8sClientFactory,
 | 
								k8sClientFactory,
 | 
				
			||||||
			dataStore,
 | 
								dataStore,
 | 
				
			||||||
 | 
								jwtService,
 | 
				
			||||||
		),
 | 
							),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ type localTransport struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
 | 
					// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API
 | 
				
			||||||
func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore) (*localTransport, error) {
 | 
					func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore, jwtService portainer.JWTService) (*localTransport, error) {
 | 
				
			||||||
	config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
 | 
						config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint,
 | 
				
			||||||
			endpoint,
 | 
								endpoint,
 | 
				
			||||||
			k8sClientFactory,
 | 
								k8sClientFactory,
 | 
				
			||||||
			dataStore,
 | 
								dataStore,
 | 
				
			||||||
 | 
								jwtService,
 | 
				
			||||||
		),
 | 
							),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,12 +2,18 @@ package kubernetes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (transport *baseTransport) proxyPodsRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
 | 
					func (transport *baseTransport) proxyPodsRequest(request *http.Request, namespace string) (*http.Response, error) {
 | 
				
			||||||
	if request.Method == http.MethodDelete {
 | 
						if request.Method == http.MethodDelete {
 | 
				
			||||||
		transport.refreshRegistry(request, namespace)
 | 
							transport.refreshRegistry(request, namespace)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if request.Method == http.MethodPost && strings.Contains(request.URL.Path, "/exec") {
 | 
				
			||||||
 | 
							if err := transport.addTokenForExec(request); err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return transport.executeKubernetesRequest(request)
 | 
						return transport.executeKubernetesRequest(request)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,15 +26,17 @@ type baseTransport struct {
 | 
				
			||||||
	endpoint         *portainer.Endpoint
 | 
						endpoint         *portainer.Endpoint
 | 
				
			||||||
	k8sClientFactory *cli.ClientFactory
 | 
						k8sClientFactory *cli.ClientFactory
 | 
				
			||||||
	dataStore        dataservices.DataStore
 | 
						dataStore        dataservices.DataStore
 | 
				
			||||||
 | 
						jwtService       portainer.JWTService
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore) *baseTransport {
 | 
					func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, k8sClientFactory *cli.ClientFactory, dataStore dataservices.DataStore, jwtService portainer.JWTService) *baseTransport {
 | 
				
			||||||
	return &baseTransport{
 | 
						return &baseTransport{
 | 
				
			||||||
		httpTransport:    httpTransport,
 | 
							httpTransport:    httpTransport,
 | 
				
			||||||
		tokenManager:     tokenManager,
 | 
							tokenManager:     tokenManager,
 | 
				
			||||||
		endpoint:         endpoint,
 | 
							endpoint:         endpoint,
 | 
				
			||||||
		k8sClientFactory: k8sClientFactory,
 | 
							k8sClientFactory: k8sClientFactory,
 | 
				
			||||||
		dataStore:        dataStore,
 | 
							dataStore:        dataStore,
 | 
				
			||||||
 | 
							jwtService:       jwtService,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,7 +84,7 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	switch {
 | 
						switch {
 | 
				
			||||||
	case strings.HasPrefix(requestPath, "pods"):
 | 
						case strings.HasPrefix(requestPath, "pods"):
 | 
				
			||||||
		return transport.proxyPodsRequest(request, namespace, requestPath)
 | 
							return transport.proxyPodsRequest(request, namespace)
 | 
				
			||||||
	case strings.HasPrefix(requestPath, "deployments"):
 | 
						case strings.HasPrefix(requestPath, "deployments"):
 | 
				
			||||||
		return transport.proxyDeploymentsRequest(request, namespace, requestPath)
 | 
							return transport.proxyDeploymentsRequest(request, namespace, requestPath)
 | 
				
			||||||
	case requestPath == "" && request.Method == "DELETE":
 | 
						case requestPath == "" && request.Method == "DELETE":
 | 
				
			||||||
| 
						 | 
					@ -92,6 +94,23 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// addTokenForExec injects a kubeconfig token into the request header
 | 
				
			||||||
 | 
					// this is only used with kubeconfig for kubectl exec requests
 | 
				
			||||||
 | 
					func (transport *baseTransport) addTokenForExec(request *http.Request) error {
 | 
				
			||||||
 | 
						tokenData, err := security.RetrieveTokenData(request)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						token, err := transport.jwtService.GenerateTokenForKubeconfig(tokenData)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						request.Header.Set("Authorization", "Bearer "+token)
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (transport *baseTransport) executeKubernetesRequest(request *http.Request) (*http.Response, error) {
 | 
					func (transport *baseTransport) executeKubernetesRequest(request *http.Request) (*http.Response, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := transport.httpTransport.RoundTrip(request)
 | 
						resp, err := transport.httpTransport.RoundTrip(request)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,359 @@
 | 
				
			||||||
 | 
					package kubernetes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						portainer "github.com/portainer/portainer/api"
 | 
				
			||||||
 | 
						"github.com/portainer/portainer/api/datastore"
 | 
				
			||||||
 | 
						"github.com/portainer/portainer/api/http/security"
 | 
				
			||||||
 | 
						"github.com/portainer/portainer/api/jwt"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MockJWTService implements portainer.JWTService for testing
 | 
				
			||||||
 | 
					type MockJWTService struct {
 | 
				
			||||||
 | 
						generateTokenFunc func(data *portainer.TokenData) (string, error)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *MockJWTService) GenerateToken(data *portainer.TokenData) (string, time.Time, error) {
 | 
				
			||||||
 | 
						if m.generateTokenFunc != nil {
 | 
				
			||||||
 | 
							token, err := m.generateTokenFunc(data)
 | 
				
			||||||
 | 
							return token, time.Now().Add(24 * time.Hour), err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "mock-token", time.Now().Add(24 * time.Hour), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *MockJWTService) GenerateTokenForKubeconfig(data *portainer.TokenData) (string, error) {
 | 
				
			||||||
 | 
						if m.generateTokenFunc != nil {
 | 
				
			||||||
 | 
							return m.generateTokenFunc(data)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "mock-kubeconfig-token", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *MockJWTService) ParseAndVerifyToken(token string) (*portainer.TokenData, string, time.Time, error) {
 | 
				
			||||||
 | 
						return &portainer.TokenData{ID: 1, Username: "mock", Role: portainer.AdministratorRole}, "mock-id", time.Now().Add(24 * time.Hour), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *MockJWTService) SetUserSessionDuration(userSessionDuration time.Duration) {
 | 
				
			||||||
 | 
						// Mock implementation - not used in tests
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBaseTransport_AddTokenForExec(t *testing.T) {
 | 
				
			||||||
 | 
						// Setup test store and JWT service
 | 
				
			||||||
 | 
						_, store := datastore.MustNewTestStore(t, true, false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create test users
 | 
				
			||||||
 | 
						adminUser := &portainer.User{
 | 
				
			||||||
 | 
							ID:       1,
 | 
				
			||||||
 | 
							Username: "admin",
 | 
				
			||||||
 | 
							Role:     portainer.AdministratorRole,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err := store.User().Create(adminUser)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						standardUser := &portainer.User{
 | 
				
			||||||
 | 
							ID:       2,
 | 
				
			||||||
 | 
							Username: "standard",
 | 
				
			||||||
 | 
							Role:     portainer.StandardUserRole,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = store.User().Create(standardUser)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create JWT service
 | 
				
			||||||
 | 
						jwtService, err := jwt.NewService("24h", store)
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create base transport
 | 
				
			||||||
 | 
						transport := &baseTransport{
 | 
				
			||||||
 | 
							jwtService: jwtService,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name           string
 | 
				
			||||||
 | 
							tokenData      *portainer.TokenData
 | 
				
			||||||
 | 
							setupRequest   func(*http.Request) *http.Request
 | 
				
			||||||
 | 
							expectError    bool
 | 
				
			||||||
 | 
							errorMsg       string
 | 
				
			||||||
 | 
							expectPanic    bool
 | 
				
			||||||
 | 
							verifyResponse func(*testing.T, *http.Request, *portainer.TokenData)
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "admin user - successful token generation",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       adminUser.ID,
 | 
				
			||||||
 | 
									Username: adminUser.Username,
 | 
				
			||||||
 | 
									Role:     adminUser.Role,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
 | 
				
			||||||
 | 
										ID:       adminUser.ID,
 | 
				
			||||||
 | 
										Username: adminUser.Username,
 | 
				
			||||||
 | 
										Role:     adminUser.Role,
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectError: false,
 | 
				
			||||||
 | 
								verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
 | 
				
			||||||
 | 
									authHeader := req.Header.Get("Authorization")
 | 
				
			||||||
 | 
									assert.NotEmpty(t, authHeader)
 | 
				
			||||||
 | 
									assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									token := authHeader[7:] // Remove "Bearer " prefix
 | 
				
			||||||
 | 
									parsedTokenData, _, _, err := jwtService.ParseAndVerifyToken(token)
 | 
				
			||||||
 | 
									assert.NoError(t, err)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.ID, parsedTokenData.ID)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.Username, parsedTokenData.Username)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.Role, parsedTokenData.Role)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "standard user - successful token generation",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       standardUser.ID,
 | 
				
			||||||
 | 
									Username: standardUser.Username,
 | 
				
			||||||
 | 
									Role:     standardUser.Role,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
 | 
				
			||||||
 | 
										ID:       standardUser.ID,
 | 
				
			||||||
 | 
										Username: standardUser.Username,
 | 
				
			||||||
 | 
										Role:     standardUser.Role,
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectError: false,
 | 
				
			||||||
 | 
								verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
 | 
				
			||||||
 | 
									authHeader := req.Header.Get("Authorization")
 | 
				
			||||||
 | 
									assert.NotEmpty(t, authHeader)
 | 
				
			||||||
 | 
									assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									token := authHeader[7:] // Remove "Bearer " prefix
 | 
				
			||||||
 | 
									parsedTokenData, _, _, err := jwtService.ParseAndVerifyToken(token)
 | 
				
			||||||
 | 
									assert.NoError(t, err)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.ID, parsedTokenData.ID)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.Username, parsedTokenData.Username)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.Role, parsedTokenData.Role)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:      "request without token data in context",
 | 
				
			||||||
 | 
								tokenData: nil,
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									return req // Don't add token data to context
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectError: true,
 | 
				
			||||||
 | 
								errorMsg:    "Unable to find JWT data in request context",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:      "request with nil token data",
 | 
				
			||||||
 | 
								tokenData: nil,
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									return req.WithContext(security.StoreTokenData(req, nil))
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectPanic: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "JWT service failure",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       1,
 | 
				
			||||||
 | 
									Username: "test",
 | 
				
			||||||
 | 
									Role:     portainer.AdministratorRole,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
 | 
				
			||||||
 | 
										ID:       1,
 | 
				
			||||||
 | 
										Username: "test",
 | 
				
			||||||
 | 
										Role:     portainer.AdministratorRole,
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectPanic: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "verify authorization header format",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       adminUser.ID,
 | 
				
			||||||
 | 
									Username: adminUser.Username,
 | 
				
			||||||
 | 
									Role:     adminUser.Role,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
 | 
				
			||||||
 | 
										ID:       adminUser.ID,
 | 
				
			||||||
 | 
										Username: adminUser.Username,
 | 
				
			||||||
 | 
										Role:     adminUser.Role,
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectError: false,
 | 
				
			||||||
 | 
								verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
 | 
				
			||||||
 | 
									authHeader := req.Header.Get("Authorization")
 | 
				
			||||||
 | 
									assert.NotEmpty(t, authHeader)
 | 
				
			||||||
 | 
									assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									token := authHeader[7:] // Remove "Bearer " prefix
 | 
				
			||||||
 | 
									assert.NotEmpty(t, token)
 | 
				
			||||||
 | 
									assert.Greater(t, len(token), 0, "Token should not be empty")
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "verify header is overwritten on subsequent calls",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       adminUser.ID,
 | 
				
			||||||
 | 
									Username: adminUser.Username,
 | 
				
			||||||
 | 
									Role:     adminUser.Role,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								setupRequest: func(req *http.Request) *http.Request {
 | 
				
			||||||
 | 
									req = req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
 | 
				
			||||||
 | 
										ID:       adminUser.ID,
 | 
				
			||||||
 | 
										Username: adminUser.Username,
 | 
				
			||||||
 | 
										Role:     adminUser.Role,
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
 | 
									// Set an existing Authorization header
 | 
				
			||||||
 | 
									req.Header.Set("Authorization", "Bearer old-token")
 | 
				
			||||||
 | 
									return req
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								expectError: false,
 | 
				
			||||||
 | 
								verifyResponse: func(t *testing.T, req *http.Request, tokenData *portainer.TokenData) {
 | 
				
			||||||
 | 
									authHeader := req.Header.Get("Authorization")
 | 
				
			||||||
 | 
									assert.NotEqual(t, "Bearer old-token", authHeader)
 | 
				
			||||||
 | 
									assert.True(t, strings.HasPrefix(authHeader, "Bearer "))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									token := authHeader[7:] // Remove "Bearer " prefix
 | 
				
			||||||
 | 
									parsedTokenData, _, _, err := jwtService.ParseAndVerifyToken(token)
 | 
				
			||||||
 | 
									assert.NoError(t, err)
 | 
				
			||||||
 | 
									assert.Equal(t, tokenData.ID, parsedTokenData.ID)
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								// Create request
 | 
				
			||||||
 | 
								request := httptest.NewRequest("GET", "/", nil)
 | 
				
			||||||
 | 
								request = tt.setupRequest(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Determine which transport to use based on test case
 | 
				
			||||||
 | 
								var testTransport *baseTransport
 | 
				
			||||||
 | 
								if tt.name == "JWT service failure" {
 | 
				
			||||||
 | 
									testTransport = &baseTransport{
 | 
				
			||||||
 | 
										jwtService: nil,
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									testTransport = transport
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Call the function
 | 
				
			||||||
 | 
								if tt.expectPanic {
 | 
				
			||||||
 | 
									assert.Panics(t, func() {
 | 
				
			||||||
 | 
										_ = testTransport.addTokenForExec(request)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								err := testTransport.addTokenForExec(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Check results
 | 
				
			||||||
 | 
								if tt.expectError {
 | 
				
			||||||
 | 
									assert.Error(t, err)
 | 
				
			||||||
 | 
									if tt.errorMsg != "" {
 | 
				
			||||||
 | 
										assert.Contains(t, err.Error(), tt.errorMsg)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									assert.NoError(t, err)
 | 
				
			||||||
 | 
									if tt.verifyResponse != nil {
 | 
				
			||||||
 | 
										tt.verifyResponse(t, request, tt.tokenData)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestBaseTransport_AddTokenForExec_Integration(t *testing.T) {
 | 
				
			||||||
 | 
						// Create a test HTTP server to capture requests
 | 
				
			||||||
 | 
						var capturedRequest *http.Request
 | 
				
			||||||
 | 
						var capturedHeaders http.Header
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							capturedRequest = r
 | 
				
			||||||
 | 
							capturedHeaders = r.Header.Clone()
 | 
				
			||||||
 | 
							w.WriteHeader(http.StatusOK)
 | 
				
			||||||
 | 
							w.Write([]byte("success"))
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
						defer testServer.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create mock JWT service
 | 
				
			||||||
 | 
						mockJWTService := &MockJWTService{
 | 
				
			||||||
 | 
							generateTokenFunc: func(data *portainer.TokenData) (string, error) {
 | 
				
			||||||
 | 
								return "mock-token-" + data.Username, nil
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create base transport
 | 
				
			||||||
 | 
						transport := &baseTransport{
 | 
				
			||||||
 | 
							httpTransport: &http.Transport{},
 | 
				
			||||||
 | 
							jwtService:    mockJWTService,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name          string
 | 
				
			||||||
 | 
							tokenData     *portainer.TokenData
 | 
				
			||||||
 | 
							requestPath   string
 | 
				
			||||||
 | 
							expectedToken string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "admin user exec request",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       1,
 | 
				
			||||||
 | 
									Username: "admin",
 | 
				
			||||||
 | 
									Role:     portainer.AdministratorRole,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								requestPath:   "/api/endpoints/1/kubernetes/api/v1/namespaces/default/pods/test-pod/exec",
 | 
				
			||||||
 | 
								expectedToken: "mock-token-admin",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "standard user exec request",
 | 
				
			||||||
 | 
								tokenData: &portainer.TokenData{
 | 
				
			||||||
 | 
									ID:       2,
 | 
				
			||||||
 | 
									Username: "standard",
 | 
				
			||||||
 | 
									Role:     portainer.StandardUserRole,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								requestPath:   "/api/endpoints/1/kubernetes/api/v1/namespaces/default/pods/test-pod/exec",
 | 
				
			||||||
 | 
								expectedToken: "mock-token-standard",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								// Reset captured data
 | 
				
			||||||
 | 
								capturedRequest = nil
 | 
				
			||||||
 | 
								capturedHeaders = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Create request to the test server
 | 
				
			||||||
 | 
								request, err := http.NewRequest("POST", testServer.URL+tt.requestPath, strings.NewReader(""))
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Add token data to request context
 | 
				
			||||||
 | 
								request = request.WithContext(security.StoreTokenData(request, tt.tokenData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Call proxyPodsRequest which triggers addTokenForExec for POST /exec requests
 | 
				
			||||||
 | 
								resp, err := transport.proxyPodsRequest(request, "default")
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
								defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Verify the response
 | 
				
			||||||
 | 
								assert.Equal(t, http.StatusOK, resp.StatusCode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Verify the request was captured
 | 
				
			||||||
 | 
								assert.NotNil(t, capturedRequest)
 | 
				
			||||||
 | 
								assert.Equal(t, "POST", capturedRequest.Method)
 | 
				
			||||||
 | 
								assert.Equal(t, tt.requestPath, capturedRequest.URL.Path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Verify the authorization header was set correctly
 | 
				
			||||||
 | 
								capturedAuthHeader := capturedHeaders.Get("Authorization")
 | 
				
			||||||
 | 
								assert.NotEmpty(t, capturedAuthHeader)
 | 
				
			||||||
 | 
								assert.True(t, strings.HasPrefix(capturedAuthHeader, "Bearer "))
 | 
				
			||||||
 | 
								assert.Equal(t, "Bearer "+tt.expectedToken, capturedAuthHeader)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -9,17 +9,20 @@ import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Note that we discard any non-canonical headers by design
 | 
					// Note that we discard any non-canonical headers by design
 | 
				
			||||||
var allowedHeaders = map[string]struct{}{
 | 
					var allowedHeaders = map[string]struct{}{
 | 
				
			||||||
	"Accept":                  {},
 | 
						"Accept":                    {},
 | 
				
			||||||
	"Accept-Encoding":         {},
 | 
						"Accept-Encoding":           {},
 | 
				
			||||||
	"Accept-Language":         {},
 | 
						"Accept-Language":           {},
 | 
				
			||||||
	"Cache-Control":           {},
 | 
						"Cache-Control":             {},
 | 
				
			||||||
	"Content-Length":          {},
 | 
						"Connection":                {},
 | 
				
			||||||
	"Content-Type":            {},
 | 
						"Content-Length":            {},
 | 
				
			||||||
	"Private-Token":           {},
 | 
						"Content-Type":              {},
 | 
				
			||||||
	"User-Agent":              {},
 | 
						"Private-Token":             {},
 | 
				
			||||||
	"X-Portaineragent-Target": {},
 | 
						"Upgrade":                   {},
 | 
				
			||||||
	"X-Portainer-Volumename":  {},
 | 
						"User-Agent":                {},
 | 
				
			||||||
	"X-Registry-Auth":         {},
 | 
						"X-Portaineragent-Target":   {},
 | 
				
			||||||
 | 
						"X-Portainer-Volumename":    {},
 | 
				
			||||||
 | 
						"X-Registry-Auth":           {},
 | 
				
			||||||
 | 
						"X-Stream-Protocol-Version": {},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
 | 
					// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,8 +32,8 @@ func NewManager(kubernetesClientFactory *cli.ClientFactory) *Manager {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (manager *Manager) NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService) {
 | 
					func (manager *Manager) NewProxyFactory(dataStore dataservices.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *dockerclient.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager, gitService portainer.GitService, snapshotService portainer.SnapshotService, jwtService portainer.JWTService) {
 | 
				
			||||||
	manager.proxyFactory = factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
 | 
						manager.proxyFactory = factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService, jwtService)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on environment(endpoint) properties and adds it to the registered proxies.
 | 
					// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on environment(endpoint) properties and adds it to the registered proxies.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue