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;