diff --git a/api/exec/compose_wrapper.go b/api/exec/compose_wrapper.go index 04b1286f3..fdac918a1 100644 --- a/api/exec/compose_wrapper.go +++ b/api/exec/compose_wrapper.go @@ -59,7 +59,7 @@ func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpo if endpoint != nil { if endpoint.URL != "" { - proxy, err := w.proxyManager.CreateAndRegisterEndpointProxy(endpoint) + proxy, err := w.proxyManager.CreateAndRegisterComposeEndpointProxy(endpoint) listener, err := net.Listen("tcp", ":0") if err != nil { diff --git a/api/http/proxy/factory/docker_compose.go b/api/http/proxy/factory/docker_compose.go new file mode 100644 index 000000000..a2660f696 --- /dev/null +++ b/api/http/proxy/factory/docker_compose.go @@ -0,0 +1,111 @@ +package factory + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/http/proxy/factory/dockercompose" +) + +func (factory *ProxyFactory) NewDockerComposeProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") { + return factory.newDockerComposeLocalProxy(endpoint) + } + + return factory.newDockerComposeAgentProxy(endpoint) +} + +func (factory *ProxyFactory) newDockerComposeLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { + tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpoint.URL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port) + } + + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "http" + httpTransport := &http.Transport{} + + if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { + config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + httpTransport.TLSClientConfig = config + endpointURL.Scheme = "https" + } + + dockerComposeTransport := dockercompose.NewAgentTransport(factory.signatureService, httpTransport) + + proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) + proxy.Transport = dockerComposeTransport + return proxy, nil +} + +func (factory *ProxyFactory) newDockerComposeAgentProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "http" + httpTransport := &http.Transport{} + + if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { + config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + httpTransport.TLSClientConfig = config + endpointURL.Scheme = "https" + } + + dockerComposeTransport := dockercompose.NewAgentTransport(factory.signatureService, httpTransport) + + proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) + proxy.Transport = dockerComposeTransport + return proxy, nil +} + +// // ServeHTTP is the http.Handler interface implementation +// // for a local (Unix socket or Windows named pipe) Docker proxy. +// func (proxy *dockerLocalProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { +// // Force URL/domain to http/unixsocket to be able to +// // use http.transport RoundTrip to do the requests via the socket +// r.URL.Scheme = "http" +// r.URL.Host = "unixsocket" + +// res, err := proxy.transport.ProxyDockerRequest(r) +// if err != nil { +// code := http.StatusInternalServerError +// if res != nil && res.StatusCode != 0 { +// code = res.StatusCode +// } + +// httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err) +// return +// } +// defer res.Body.Close() + +// for k, vv := range res.Header { +// for _, v := range vv { +// w.Header().Add(k, v) +// } +// } + +// w.WriteHeader(res.StatusCode) + +// if _, err := io.Copy(w, res.Body); err != nil { +// log.Printf("proxy error: %s\n", err) +// } +// } diff --git a/api/http/proxy/factory/dockercompose/transport.go b/api/http/proxy/factory/dockercompose/transport.go new file mode 100644 index 000000000..45a99efac --- /dev/null +++ b/api/http/proxy/factory/dockercompose/transport.go @@ -0,0 +1,98 @@ +package dockercompose + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" +) + +type ( + LocalTransport struct { + httpTransport *http.Transport + endpointIdentifier portainer.EndpointID + } + + AgentTransport struct { + httpTransport *http.Transport + signatureService portainer.DigitalSignatureService + endpointIdentifier portainer.EndpointID + } + + EdgeTransport struct { + httpTransport *http.Transport + reverseTunnelService portainer.ReverseTunnelService + endpointIdentifier portainer.EndpointID + } +) + +// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API +func NewLocalTransport() (*LocalTransport, error) { + config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true) + if err != nil { + return nil, err + } + + transport := &LocalTransport{ + httpTransport: &http.Transport{ + TLSClientConfig: config, + }, + } + + return transport, nil +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *LocalTransport) RoundTrip(request *http.Request) (*http.Response, error) { + + return transport.httpTransport.RoundTrip(request) +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent +func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport { + transport := &AgentTransport{ + httpTransport: httpTransport, + signatureService: signatureService, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) { + + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) + request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + + return transport.httpTransport.RoundTrip(request) +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent +func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID) *EdgeTransport { + transport := &EdgeTransport{ + httpTransport: &http.Transport{}, + reverseTunnelService: reverseTunnelService, + endpointIdentifier: endpointIdentifier, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *EdgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { + + response, err := transport.httpTransport.RoundTrip(request) + + if err == nil { + transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier) + } else { + transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier) + } + + return response, err +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 491a709c9..53149274d 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -47,6 +47,18 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo return proxy, nil } +// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies. +// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy. +func (manager *Manager) CreateAndRegisterComposeEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + proxy, err := manager.proxyFactory.NewDockerComposeProxy(endpoint) + if err != nil { + return nil, err + } + + manager.endpointProxies.Set(string(endpoint.ID), proxy) + return proxy, nil +} + // GetEndpointProxy returns the proxy associated to a key func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler { proxy, ok := manager.endpointProxies.Get(string(endpoint.ID))