diff --git a/api/crypto/tls.go b/api/crypto/tls.go index 976e1a075..83cd6aa7f 100644 --- a/api/crypto/tls.go +++ b/api/crypto/tls.go @@ -8,6 +8,27 @@ import ( "github.com/portainer/portainer" ) +func CreateTLSConfig(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) { + config := &tls.Config{} + config.InsecureSkipVerify = skipServerVerification + + if !skipClientVerification { + certificate, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{certificate} + } + + if !skipServerVerification { + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + config.RootCAs = caCertPool + } + + return config, nil +} + // CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) { TLSConfig := &tls.Config{} diff --git a/api/http/handler/endpoint.go b/api/http/handler/endpoint.go index cd345c1d9..e3bcdb558 100644 --- a/api/http/handler/endpoint.go +++ b/api/http/handler/endpoint.go @@ -1,7 +1,13 @@ package handler import ( + "bytes" + "crypto/tls" + "strings" + "time" + "github.com/portainer/portainer" + "github.com/portainer/portainer/crypto" httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" "github.com/portainer/portainer/http/security" @@ -56,15 +62,6 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag } type ( - postEndpointsRequest struct { - Name string `valid:"required"` - URL string `valid:"required"` - PublicURL string `valid:"-"` - TLS bool - TLSSkipVerify bool - TLSSkipClientVerify bool - } - postEndpointsResponse struct { ID int `json:"Id"` } @@ -82,6 +79,18 @@ type ( TLSSkipVerify bool `valid:"-"` TLSSkipClientVerify bool `valid:"-"` } + + postEndpointPayload struct { + name string + url string + publicURL string + useTLS bool + skipTLSServerVerification bool + skipTLSClientVerification bool + caCert []byte + cert []byte + key []byte + } ) // handleGetEndpoints handles GET requests on /endpoints @@ -107,32 +116,48 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt encodeJSON(w, filteredEndpoints, handler.Logger) } -// handlePostEndpoints handles POST requests on /endpoints -func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) { - if !handler.authorizeEndpointManagement { - httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) - return +func sendPingRequest(host string, tlsConfig *tls.Config) error { + transport := &http.Transport{} + + scheme := "http" + if tlsConfig != nil { + transport.TLSClientConfig = tlsConfig + scheme = "https" } - var req postEndpointsRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) - return + client := &http.Client{ + Timeout: time.Second * 3, + Transport: transport, } - _, err := govalidator.ValidateStruct(req) + pingOperationURL := strings.Replace(host, "tcp://", scheme+"://", 1) + "/_ping" + _, err := client.Get(pingOperationURL) if err != nil { - httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) - return + return err + } + + return nil +} + +func (handler *EndpointHandler) createTLSSecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) { + + tlsConfig, err := crypto.CreateTLSConfig(payload.caCert, payload.cert, payload.key, payload.skipTLSClientVerification, payload.skipTLSServerVerification) + if err != nil { + return nil, err + } + + err = sendPingRequest(payload.url, tlsConfig) + if err != nil { + return nil, err } endpoint := &portainer.Endpoint{ - Name: req.Name, - URL: req.URL, - PublicURL: req.PublicURL, + Name: payload.name, + URL: payload.url, + PublicURL: payload.publicURL, TLSConfig: portainer.TLSConfiguration{ - TLS: req.TLS, - TLSSkipVerify: req.TLSSkipVerify, + TLS: payload.useTLS, + TLSSkipVerify: payload.skipTLSServerVerification, }, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, @@ -141,30 +166,147 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht err = handler.EndpointService.CreateEndpoint(endpoint) if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return nil, err + } + + folder := strconv.Itoa(int(endpoint.ID)) + + if !payload.skipTLSServerVerification { + r := bytes.NewReader(payload.caCert) + // TODO: review the API exposed by the FileService to store + // a file from a byte slice and return the path to the stored file instead + // of using multiple legacy calls (StoreTLSFile, GetPathForTLSFile) here. + err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCA, r) + if err != nil { + handler.EndpointService.DeleteEndpoint(endpoint.ID) + return nil, err + } + caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) + endpoint.TLSConfig.TLSCACertPath = caCertPath + } + + if !payload.skipTLSClientVerification { + r := bytes.NewReader(payload.cert) + err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileCert, r) + if err != nil { + handler.EndpointService.DeleteEndpoint(endpoint.ID) + return nil, err + } + certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) + endpoint.TLSConfig.TLSCertPath = certPath + + r = bytes.NewReader(payload.key) + err = handler.FileService.StoreTLSFile(folder, portainer.TLSFileKey, r) + if err != nil { + handler.EndpointService.DeleteEndpoint(endpoint.ID) + return nil, err + } + keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) + endpoint.TLSConfig.TLSKeyPath = keyPath + } + + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + return nil, err + } + + return endpoint, nil +} + +func (handler *EndpointHandler) createUnsecuredEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) { + + if !strings.HasPrefix(payload.url, "unix://") { + err := sendPingRequest(payload.url, nil) + if err != nil { + return nil, err + } + } + + endpoint := &portainer.Endpoint{ + Name: payload.name, + URL: payload.url, + PublicURL: payload.publicURL, + TLSConfig: portainer.TLSConfiguration{ + TLS: false, + }, + AuthorizedUsers: []portainer.UserID{}, + AuthorizedTeams: []portainer.TeamID{}, + Extensions: []portainer.EndpointExtension{}, + } + + err := handler.EndpointService.CreateEndpoint(endpoint) + if err != nil { + return nil, err + } + + return endpoint, nil +} + +func (handler *EndpointHandler) createEndpoint(payload *postEndpointPayload) (*portainer.Endpoint, error) { + if payload.useTLS { + return handler.createTLSSecuredEndpoint(payload) + } + return handler.createUnsecuredEndpoint(payload) +} + +func convertPostEndpointRequestToPayload(r *http.Request) (*postEndpointPayload, error) { + payload := &postEndpointPayload{} + payload.name = r.FormValue("Name") + payload.url = r.FormValue("URL") + payload.publicURL = r.FormValue("PublicURL") + + if payload.name == "" || payload.url == "" { + return nil, ErrInvalidRequestFormat + } + + payload.useTLS = r.FormValue("TLS") == "true" + + if payload.useTLS { + payload.skipTLSServerVerification = r.FormValue("TLSSkipVerify") == "true" + payload.skipTLSClientVerification = r.FormValue("TLSSkipClientVerify") == "true" + + if !payload.skipTLSServerVerification { + caCert, err := getUploadedFileContent(r, "TLSCACertFile") + if err != nil { + return nil, err + } + payload.caCert = caCert + } + + if !payload.skipTLSClientVerification { + cert, err := getUploadedFileContent(r, "TLSCertFile") + if err != nil { + return nil, err + } + payload.cert = cert + key, err := getUploadedFileContent(r, "TLSKeyFile") + if err != nil { + return nil, err + } + payload.key = key + } + } + + return payload, nil +} + +// handlePostEndpoints handles POST requests on /endpoints +func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) { + if !handler.authorizeEndpointManagement { + httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) return } - if req.TLS { - folder := strconv.Itoa(int(endpoint.ID)) + payload, err := convertPostEndpointRequestToPayload(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } - if !req.TLSSkipVerify { - caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) - endpoint.TLSConfig.TLSCACertPath = caCertPath - } - - if !req.TLSSkipClientVerify { - certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) - endpoint.TLSConfig.TLSCertPath = certPath - keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) - endpoint.TLSConfig.TLSKeyPath = keyPath - } - - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) - if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) - return - } + endpoint, err := handler.createEndpoint(payload) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return } encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger) diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 7c654e766..1cea2f1fa 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "io/ioutil" "log" "net/http" "strings" @@ -95,3 +96,19 @@ func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger) } } + +// getUploadedFileContent retrieve the content of a file uploaded in the request. +// Uses requestParameter as the key to retrieve the file in the request payload. +func getUploadedFileContent(request *http.Request, requestParameter string) ([]byte, error) { + file, _, err := request.FormFile(requestParameter) + if err != nil { + return nil, err + } + defer file.Close() + + fileContent, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + return fileContent, nil +} diff --git a/app/portainer/components/endpointSecurity/porEndpointSecurity.html b/app/portainer/components/endpointSecurity/porEndpointSecurity.html index 70c9551ed..59e02aba3 100644 --- a/app/portainer/components/endpointSecurity/porEndpointSecurity.html +++ b/app/portainer/components/endpointSecurity/porEndpointSecurity.html @@ -84,7 +84,6 @@ {{ $ctrl.formData.TLSCACert.name }} - @@ -100,7 +99,6 @@ {{ $ctrl.formData.TLSCert.name }} - @@ -114,7 +112,6 @@ {{ $ctrl.formData.TLSKey.name }} - diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 34c3ac855..5969212a0 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -2,7 +2,6 @@ angular.module('portainer.app') .factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) { 'use strict'; return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, { - create: { method: 'POST', ignoreLoadingBar: true }, query: { method: 'GET', isArray: true }, get: { method: 'GET', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } }, diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 4905eda77..d67a5296b 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -50,42 +50,28 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { }; service.createLocalEndpoint = function(name, URL, TLS, active) { - var endpoint = { - Name: 'local', - URL: 'unix:///var/run/docker.sock', - TLS: false - }; - return Endpoints.create({}, endpoint).$promise; + var deferred = $q.defer(); + + FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', false) + .then(function success(response) { + deferred.resolve(response.data); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to create endpoint', err: err}); + }); + + return deferred.promise; }; service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { - var endpoint = { - Name: name, - URL: 'tcp://' + URL, - PublicURL: PublicURL, - TLS: TLS, - TLSSkipVerify: TLSSkipVerify, - TLSSkipClientVerify: TLSSkipClientVerify - }; - var deferred = $q.defer(); - Endpoints.create({}, endpoint).$promise - .then(function success(data) { - var endpointID = data.Id; - if (!TLSSkipVerify || !TLSSkipClientVerify) { - deferred.notify({upload: true}); - FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) - .then(function success() { - deferred.notify({upload: false}); - deferred.resolve(data); - }); - } else { - deferred.resolve(data); - } + + FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + .then(function success(response) { + deferred.resolve(response.data); }) .catch(function error(err) { - deferred.notify({upload: false}); - deferred.reject({msg: 'Unable to upload TLS certs', err: err}); + deferred.reject({msg: 'Unable to create endpoint', err: err}); }); return deferred.promise; diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index bcfb73328..590e1d65c 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -42,6 +42,24 @@ angular.module('portainer.app') }); }; + service.createEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + return Upload.upload({ + url: 'api/endpoints', + data: { + Name: name, + URL: URL, + PublicURL: PublicURL, + TLS: TLS, + TLSSkipVerify: TLSSkipVerify, + TLSSkipClientVerify: TLSSkipClientVerify, + TLSCACertFile: TLSCAFile, + TLSCertFile: TLSCertFile, + TLSKeyFile: TLSKeyFile + }, + ignoreLoadingBar: true + }); + }; + service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) { var queue = []; diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index 7bf887d20..389007111 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications', 'SystemService', 'EndpointProvider', -function ($scope, $state, $filter, EndpointService, Notifications, SystemService, EndpointProvider) { +.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications', +function ($scope, $state, $filter, EndpointService, Notifications) { $scope.state = { uploadInProgress: false, actionInProgress: false @@ -30,34 +30,17 @@ function ($scope, $state, $filter, EndpointService, Notifications, SystemService var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - var endpointId; $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) - .then(function success(data) { - endpointId = data.Id; - var currentEndpointId = EndpointProvider.endpointID(); - EndpointProvider.setEndpointID(endpointId); - SystemService.info() - .then(function success() { - Notifications.success('Endpoint created', name); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create endpoint'); - EndpointService.deleteEndpoint(endpointId); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - EndpointProvider.setEndpointID(currentEndpointId); - }); - }, function error(err) { - $scope.state.uploadInProgress = false; - $scope.state.actionInProgress = false; + .then(function success() { + Notifications.success('Endpoint created', name); + $state.reload(); + }) + .catch(function error(err) { Notifications.error('Failure', err, 'Unable to create endpoint'); - }, function update(evt) { - if (evt.upload) { - $scope.state.uploadInProgress = evt.upload; - } + }) + .finally(function final() { + $scope.state.actionInProgress = false; }); }; diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index 8bf9c36b5..0c982b795 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -46,7 +46,6 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - EndpointService.deleteEndpoint(endpointID); }) .finally(function final() { $scope.state.actionInProgress = false; @@ -81,7 +80,6 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - EndpointService.deleteEndpoint(endpointID); }) .finally(function final() { $scope.state.actionInProgress = false;