This is an automated email from the ASF dual-hosted git repository. cbickel pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git
The following commit(s) were added to refs/heads/master by this push: new a8476ab Enable ssl on the path between edge and controllers. (#3077) a8476ab is described below commit a8476ab970b4f8804d0d26fa319fe4aaa7c9ab04 Author: Vadim Raskin <raskinva...@gmail.com> AuthorDate: Mon Mar 12 10:15:39 2018 +0100 Enable ssl on the path between edge and controllers. (#3077) --- .gitignore | 4 ++ ansible/group_vars/all | 19 ++++++ ansible/roles/controller/tasks/deploy.yml | 34 +++++++++- ansible/roles/nginx/tasks/deploy.yml | 9 +++ ansible/roles/nginx/templates/nginx.conf.j2 | 12 +++- ansible/setup.yml | 12 ++++ ansible/templates/whisk.properties.j2 | 1 + .../scala/src/main/scala/whisk/common/Https.scala | 75 ++++++++++++++++++++++ .../main/scala/whisk/http/BasicHttpService.scala | 35 ++++++---- .../controller/src/main/resources/application.conf | 3 + .../scala/whisk/core/controller/Controller.scala | 8 ++- .../scala/whisk/core/controller/Triggers.scala | 32 ++++++++- .../main/scala/whisk/core/invoker/Invoker.scala | 4 +- tests/src/test/resources/application.conf.j2 | 13 ++++ tests/src/test/scala/common/rest/WskRest.scala | 62 ++++++++++++++++-- .../src/test/scala/ha/CacheInvalidationTests.scala | 34 ++++++---- tests/src/test/scala/ha/ShootComponentsTests.scala | 13 +++- tests/src/test/scala/services/HeadersTests.scala | 14 ++-- tools/travis/setup.sh | 2 +- 19 files changed, 338 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 710e6f5..6a97330 100644 --- a/.gitignore +++ b/.gitignore @@ -60,9 +60,13 @@ ansible/db_local.ini* ansible/tmp/* ansible/roles/nginx/files/openwhisk-client* ansible/roles/nginx/files/*.csr +ansible/roles/nginx/files/*.p12 ansible/roles/nginx/files/*cert.pem ansible/roles/nginx/files/*p12 ansible/roles/kafka/files/* +ansible/roles/controller/files/*.pem +ansible/roles/controller/files/*.key +ansible/roles/controller/files/*.p12 # .zip files must be explicited whitelisted *.zip diff --git a/ansible/group_vars/all b/ansible/group_vars/all index 3105c5b..f0dca5f 100644 --- a/ansible/group_vars/all +++ b/ansible/group_vars/all @@ -66,6 +66,25 @@ controller: loadbalancer: spi: "{{ controller_loadbalancer_spi | default('') }}" loglevel: "{{ controller_loglevel | default(whisk_loglevel) | default('INFO') }}" + protocol: "{{ controllerProtocolForSetup }}" + ssl: + cn: openwhisk-controllers + cert: "{{ controller_ca_cert | default('controller-openwhisk-server-cert.pem') }}" + key: "{{ controller_key | default('controller-openwhisk-server-key.pem') }}" + clientAuth: "{{ controller_client_auth | default('true') }}" + storeFlavor: PKCS12 + keystore: + password: "{{ controllerKeystorePassword }}" + path: "/conf/{{ controllerKeystoreName }}" +# keystore and truststore are the same as long as controller and nginx share the certificate + truststore: + password: "{{ controllerKeystorePassword }}" + path: "/conf/{{ controllerKeystoreName }}" +# move controller protocol outside to not evaluate controller variables during execution of setup.yml +controllerProtocolForSetup: "{{ controller_protocol | default('https') }}" +controllerKeystoreName: "{{ controllerKeyPrefix }}openwhisk-keystore.p12" +controllerKeyPrefix: "controller-" +controllerKeystorePassword: openwhisk jmx: basePortController: 15000 diff --git a/ansible/roles/controller/tasks/deploy.yml b/ansible/roles/controller/tasks/deploy.yml index 9dc2de7..68d7efa 100644 --- a/ansible/roles/controller/tasks/deploy.yml +++ b/ansible/roles/controller/tasks/deploy.yml @@ -51,6 +51,25 @@ src: "{{ openwhisk_home }}/ansible/roles/kafka/files/{{ kafka.ssl.keystore.name }}" dest: "{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}" +- name: copy nginx certificate keystore + when: controller.protocol == 'https' + copy: + src: files/{{ controllerKeystoreName }} + mode: 0666 + dest: "{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}" + become: "{{ controller.dir.become }}" + +- name: copy certificates + when: controller.protocol == 'https' + copy: + src: "{{ openwhisk_home }}/ansible/roles/controller/files/{{ item }}" + mode: 0666 + dest: "{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}" + with_items: + - "{{ controller.ssl.cert }}" + - "{{ controller.ssl.key }}" + become: "{{ controller.dir.become }}" + - name: check, that required databases exist include: "{{ openwhisk_home }}/ansible/tasks/db/checkDb.yml" vars: @@ -154,11 +173,17 @@ "CONTROLLER_LOCALBOOKKEEPING": "{{ controller.localBookkeeping }}" "AKKA_CLUSTER_SEED_NODES": "{{seed_nodes_list | join(' ') }}" - "METRICS_KAMON": "{{ metrics.kamon.enabled }}" "METRICS_KAMON_TAGS": "{{ metrics.kamon.tags }}" "METRICS_LOG": "{{ metrics.log.enabled }}" - + "CONFIG_whisk_controller_protocol": "{{ controller.protocol }}" + "CONFIG_whisk_controller_https_keystorePath": "{{ controller.ssl.keystore.path }}" + "CONFIG_whisk_controller_https_keystorePassword": "{{ controller.ssl.keystore.password }}" + "CONFIG_whisk_controller_https_keystoreFlavor": "{{ controller.ssl.storeFlavor }}" + "CONFIG_whisk_controller_https_truststorePath": "{{ controller.ssl.truststore.path }}" + "CONFIG_whisk_controller_https_truststorePassword": "{{ controller.ssl.truststore.password }}" + "CONFIG_whisk_controller_https_truststoreFlavor": "{{ controller.ssl.storeFlavor }}" + "CONFIG_whisk_controller_https_clientAuth": "{{ controller.ssl.clientAuth }}" "CONFIG_whisk_loadbalancer_invokerBusyThreshold": "{{ invoker.busyThreshold }}" "CONFIG_whisk_loadbalancer_blackboxFraction": "{{ controller.blackboxFraction }}" @@ -183,7 +208,10 @@ - name: wait until the Controller in this host is up and running uri: - url: "http://{{ ansible_host }}:{{ controller.basePort + (controller_index | int) }}/ping" + url: "{{ controller.protocol }}://{{ ansible_host }}:{{ controller.basePort + groups['controllers'].index(inventory_hostname) }}/ping" + validate_certs: no + client_key: "{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}/{{ controller.ssl.key }}" + client_cert: "{{ controller.confdir }}/controller{{ groups['controllers'].index(inventory_hostname) }}/{{ controller.ssl.cert }}" register: result until: result.status == 200 retries: 12 diff --git a/ansible/roles/nginx/tasks/deploy.yml b/ansible/roles/nginx/tasks/deploy.yml index 0a6531f..d343e2f 100644 --- a/ansible/roles/nginx/tasks/deploy.yml +++ b/ansible/roles/nginx/tasks/deploy.yml @@ -27,6 +27,15 @@ dest: "{{ nginx.confdir }}" when: nginx.ssl.password_enabled == true +- name: copy controller cert for authentication + copy: + src: "{{ openwhisk_home }}/ansible/roles/controller/files/{{ item }}" + dest: "{{ nginx.confdir }}" + with_items: + - "{{ controller.ssl.cert }}" + - "{{ controller.ssl.key }}" + when: "{{ controller.protocol == 'https' }}" + - name: ensure nginx log directory is created with permissions file: path: "{{ whisk_logs_dir }}/nginx" diff --git a/ansible/roles/nginx/templates/nginx.conf.j2 b/ansible/roles/nginx/templates/nginx.conf.j2 index c31026c..427b48e 100644 --- a/ansible/roles/nginx/templates/nginx.conf.j2 +++ b/ansible/roles/nginx/templates/nginx.conf.j2 @@ -23,6 +23,14 @@ http { proxy_http_version 1.1; proxy_set_header Connection ""; +{% if controller.protocol == 'https' %} + proxy_ssl_session_reuse on; + proxy_ssl_name {{ controller.ssl.cn }}; + proxy_ssl_protocols TLSv1.1 TLSv1.2; + proxy_ssl_certificate /etc/nginx/{{ controller.ssl.cert }}; + proxy_ssl_certificate_key /etc/nginx/{{ controller.ssl.key }}; +{% endif %} + upstream controllers { # fail_timeout: period of time the server will be considered unavailable # Mark the controller as unavailable for at least 60 seconds, to not get any requests during restart. @@ -80,7 +88,7 @@ http { if ($namespace) { rewrite /(.*) /api/v1/web/${namespace}/$1 break; } - proxy_pass http://controllers; + proxy_pass {{ controller.protocol }}://controllers; proxy_read_timeout 75s; # 70+5 additional seconds to allow controller to terminate request } @@ -89,7 +97,7 @@ http { if ($namespace) { rewrite ^ /api/v1/web/${namespace}/public/index.html break; } - proxy_pass http://controllers; + proxy_pass {{ controller.protocol }}://controllers; proxy_read_timeout 75s; # 70+5 additional seconds to allow controller to terminate request } diff --git a/ansible/setup.yml b/ansible/setup.yml index 6a20d99..c9769d0 100644 --- a/ansible/setup.yml +++ b/ansible/setup.yml @@ -48,3 +48,15 @@ - name: generate kafka certificates local_action: shell "{{ playbook_dir }}/files/genssl.sh" "openwhisk-kafka" "server_with_JKS_keystore" "{{ playbook_dir }}/roles/kafka/files" openwhisk "generateKey" "kafka-" when: kafka_protocol_for_setup == 'SSL' + + - name: ensure controller files directory exists + file: + path: "{{ playbook_dir }}/roles/controller/files/" + state: directory + mode: 0777 + become: "{{ logs.dir.become }}" + when: controllerProtocolForSetup == 'https' + + - name: generate controller certificates + when: controllerProtocolForSetup == 'https' + local_action: shell "{{ playbook_dir }}/files/genssl.sh" "openwhisk-controllers" "server" "{{ playbook_dir }}/roles/controller/files" {{ controllerKeystorePassword }} "generateKey" {{ controllerKeyPrefix }} diff --git a/ansible/templates/whisk.properties.j2 b/ansible/templates/whisk.properties.j2 index f8d6a1b..bc7ff36 100644 --- a/ansible/templates/whisk.properties.j2 +++ b/ansible/templates/whisk.properties.j2 @@ -56,6 +56,7 @@ invoker.hosts.basePort={{ invoker.port }} controller.hosts={{ groups["controllers"] | map('extract', hostvars, 'ansible_host') | list | join(",") }} controller.host.basePort={{ controller.basePort }} controller.instances={{ controller.instances }} +controller.protocol={{ controller.protocol }} invoker.container.network=bridge invoker.container.policy={{ invoker_container_policy_name | default()}} diff --git a/common/scala/src/main/scala/whisk/common/Https.scala b/common/scala/src/main/scala/whisk/common/Https.scala new file mode 100644 index 0000000..2751498 --- /dev/null +++ b/common/scala/src/main/scala/whisk/common/Https.scala @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package whisk.common + +import java.io.{FileInputStream, InputStream} +import java.security.{KeyStore, SecureRandom} +import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} + +import akka.http.scaladsl.ConnectionContext +import akka.stream.TLSClientAuth +import com.typesafe.sslconfig.akka.AkkaSSLConfig +import whisk.core.WhiskConfig +import pureconfig._ + +object Https { + case class HttpsConfig(keystorePassword: String, + keystoreFlavor: String, + keystorePath: String, + truststorePath: String, + truststorePassword: String, + truststoreFlavor: String, + clientAuth: String) + private val httpsConfig = loadConfigOrThrow[HttpsConfig]("whisk.controller.https") + + def getCertStore(password: Array[Char], flavor: String, path: String): KeyStore = { + val certStorePassword: Array[Char] = password + val cs: KeyStore = KeyStore.getInstance(flavor) + val certStore: InputStream = new FileInputStream(path) + cs.load(certStore, certStorePassword) + cs + } + + def connectionContext(config: WhiskConfig, sslConfig: Option[AkkaSSLConfig] = None) = { + + val keyFactoryType = "SunX509" + val clientAuth = { + if (httpsConfig.clientAuth.toBoolean) + Some(TLSClientAuth.need) + else + Some(TLSClientAuth.none) + } + +// configure keystore + val keystorePassword = httpsConfig.keystorePassword.toCharArray + val ks: KeyStore = getCertStore(keystorePassword, httpsConfig.keystoreFlavor, httpsConfig.keystorePath) + val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(keyFactoryType) + keyManagerFactory.init(ks, keystorePassword) + +// configure truststore + val truststorePassword = httpsConfig.truststorePassword.toCharArray + val ts: KeyStore = getCertStore(truststorePassword, httpsConfig.truststoreFlavor, httpsConfig.keystorePath) + val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance(keyFactoryType) + trustManagerFactory.init(ts) + + val sslContext: SSLContext = SSLContext.getInstance("TLS") + sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom) + + ConnectionContext.https(sslContext, sslConfig, clientAuth = clientAuth) + } +} diff --git a/common/scala/src/main/scala/whisk/http/BasicHttpService.scala b/common/scala/src/main/scala/whisk/http/BasicHttpService.scala index e0c570c..ece541b 100644 --- a/common/scala/src/main/scala/whisk/http/BasicHttpService.scala +++ b/common/scala/src/main/scala/whisk/http/BasicHttpService.scala @@ -18,7 +18,7 @@ package whisk.http import scala.collection.immutable.Seq -import scala.concurrent.Await +import scala.concurrent.{Await, Future} import scala.concurrent.duration.DurationInt import akka.actor.ActorSystem import akka.event.Logging @@ -30,12 +30,8 @@ import akka.http.scaladsl.server.RouteResult.Rejected import akka.http.scaladsl.server.directives._ import akka.stream.ActorMaterializer import spray.json._ -import whisk.common.LogMarker -import whisk.common.LogMarkerToken -import whisk.common.LoggingMarkers -import whisk.common.TransactionCounter -import whisk.common.TransactionId -import whisk.common.MetricEmitter +import whisk.common._ +import whisk.core.WhiskConfig /** * This trait extends the Akka Directives and Actor with logging and transaction counting @@ -146,14 +142,31 @@ trait BasicHttpService extends Directives with TransactionCounter { object BasicHttpService { /** - * Starts an HTTP route handler on given port and registers a shutdown hook. + * Starts an HTTPS route handler on given port and registers a shutdown hook. */ - def startService(route: Route, port: Int)(implicit actorSystem: ActorSystem, - materializer: ActorMaterializer): Unit = { + def startHttpsService(route: Route, port: Int, config: WhiskConfig)(implicit actorSystem: ActorSystem, + materializer: ActorMaterializer): Unit = { + implicit val executionContext = actorSystem.dispatcher + + val httpsBinding = Http().bindAndHandle(route, "0.0.0.0", port, connectionContext = Https.connectionContext(config)) + addShutdownHook(httpsBinding) + } + + /** + * Starts an HTTP route handler on given port and registers a shutdown hook. + */ + def startHttpService(route: Route, port: Int)(implicit actorSystem: ActorSystem, + materializer: ActorMaterializer): Unit = { val httpBinding = Http().bindAndHandle(route, "0.0.0.0", port) + addShutdownHook(httpBinding) + } + + def addShutdownHook(binding: Future[Http.ServerBinding])(implicit actorSystem: ActorSystem, + materializer: ActorMaterializer): Unit = { + implicit val executionContext = actorSystem.dispatcher sys.addShutdownHook { - Await.result(httpBinding.map(_.unbind()), 30.seconds) + Await.result(binding.map(_.unbind()), 30.seconds) actorSystem.terminate() Await.result(actorSystem.whenTerminated, 30.seconds) } diff --git a/core/controller/src/main/resources/application.conf b/core/controller/src/main/resources/application.conf index e627e48..bbc298b 100644 --- a/core/controller/src/main/resources/application.conf +++ b/core/controller/src/main/resources/application.conf @@ -7,6 +7,9 @@ whisk { invoker-busy-threshold: 4 blackbox-fraction: 10% } + controller { + protocol: http + } } # http://doc.akka.io/docs/akka-http/current/scala/http/configuration.html diff --git a/core/controller/src/main/scala/whisk/core/controller/Controller.scala b/core/controller/src/main/scala/whisk/core/controller/Controller.scala index bb2086a..095a8b6 100644 --- a/core/controller/src/main/scala/whisk/core/controller/Controller.scala +++ b/core/controller/src/main/scala/whisk/core/controller/Controller.scala @@ -49,6 +49,7 @@ import whisk.http.BasicRasService import whisk.spi.SpiLoader import whisk.core.containerpool.logging.LogStoreProvider import akka.event.Logging.InfoLevel +import pureconfig.loadConfigOrThrow /** * The Controller is the service that provides the REST API for OpenWhisk. @@ -159,6 +160,8 @@ class Controller(val instance: InstanceId, */ object Controller { + protected val protocol = loadConfigOrThrow[String]("whisk.controller.protocol") + // requiredProperties is a Map whose keys define properties that must be bound to // a value, and whose values are default values. A null value in the Map means there is // no default value specified, so it must appear in the properties file @@ -233,7 +236,10 @@ object Controller { actorSystem, ActorMaterializer.create(actorSystem), logger) - BasicHttpService.startService(controller.route, port)(actorSystem, controller.materializer) + if (Controller.protocol == "https") + BasicHttpService.startHttpsService(controller.route, port, config)(actorSystem, controller.materializer) + else + BasicHttpService.startHttpService(controller.route, port)(actorSystem, controller.materializer) case Failure(t) => abort(s"Invalid runtimes manifest: $t") diff --git a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala index 7a24286..93dd988 100644 --- a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala +++ b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala @@ -34,8 +34,10 @@ import akka.http.scaladsl.server.{RequestContext, RouteResult} import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} import akka.stream.ActorMaterializer import spray.json.DefaultJsonProtocol._ +import com.typesafe.sslconfig.akka.AkkaSSLConfig +import pureconfig.loadConfigOrThrow import spray.json._ -import whisk.common.TransactionId +import whisk.common.{Https, TransactionId} import whisk.core.controller.RestApiCommons.ListLimit import whisk.core.database.CacheChangeNotification import whisk.core.entitlement.Collection @@ -56,6 +58,29 @@ trait WhiskTriggersApi extends WhiskCollectionAPI { /** Database service to CRUD triggers. */ protected val entityStore: EntityStore + /** Connection context for HTTPS */ + protected lazy val httpsConnectionContext = { + val sslConfig = AkkaSSLConfig().mapSettings { s => + s.withLoose(s.loose.withDisableHostnameVerification(true)) + } + Https.connectionContext(whiskConfig, Some(sslConfig)) + + } + + protected val controllerProtocol = loadConfigOrThrow[String]("whisk.controller.protocol") + + /** + * Sends a request either over http or https depending on the configuration + * @param request http request to send + * @return http response packed in a future + */ + private def singleRequest(request: HttpRequest): Future[HttpResponse] = { + if (controllerProtocol == "https") + Http().singleRequest(request, connectionContext = httpsConnectionContext) + else + Http().singleRequest(request) + } + /** Notification service for cache invalidation. */ protected implicit val cacheChangeNotification: Some[CacheChangeNotification] @@ -65,7 +90,7 @@ trait WhiskTriggersApi extends WhiskCollectionAPI { /** JSON response formatter. */ /** Path to Triggers REST API. */ protected val triggersPath = "triggers" - protected val url = Uri(s"http://localhost:${whiskConfig.servicePort}") + protected val url = Uri(s"${controllerProtocol}://localhost:${whiskConfig.servicePort}") protected implicit val materializer: ActorMaterializer @@ -372,7 +397,7 @@ trait WhiskTriggersApi extends WhiskCollectionAPI { headers = List(Authorization(BasicHttpCredentials(user.authkey.uuid.asString, user.authkey.key.asString))), entity = HttpEntity(MediaTypes.`application/json`, args.compactPrint)) - Http().singleRequest(request) + singleRequest(request) } /** Contains the result of invoking a rule */ @@ -395,4 +420,5 @@ trait WhiskTriggersApi extends WhiskCollectionAPI { /** Custom unmarshaller for query parameters "limit" for "list" operations. */ private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection) + } diff --git a/core/invoker/src/main/scala/whisk/core/invoker/Invoker.scala b/core/invoker/src/main/scala/whisk/core/invoker/Invoker.scala index b6bcccd..e8e1dae 100644 --- a/core/invoker/src/main/scala/whisk/core/invoker/Invoker.scala +++ b/core/invoker/src/main/scala/whisk/core/invoker/Invoker.scala @@ -189,6 +189,8 @@ object Invoker { }) val port = config.servicePort.toInt - BasicHttpService.startService(new InvokerServer().route, port)(actorSystem, ActorMaterializer.create(actorSystem)) + BasicHttpService.startHttpService(new InvokerServer().route, port)( + actorSystem, + ActorMaterializer.create(actorSystem)) } } diff --git a/tests/src/test/resources/application.conf.j2 b/tests/src/test/resources/application.conf.j2 index 7bf9421..e235bd7 100644 --- a/tests/src/test/resources/application.conf.j2 +++ b/tests/src/test/resources/application.conf.j2 @@ -45,4 +45,17 @@ whisk { WhiskActivation = {{ db.whisk.activations }} } } + + controller { + protocol = {{ controller.protocol }} + https { + keystore-flavor = "{{ controller.ssl.storeFlavor }}" + keystore-path = "{{ openwhisk_home }}/ansible/roles/controller/files/{{ controllerKeystoreName }}" + keystore-password = "{{ controller.ssl.keystore.password }}" + truststore-flavor = "{{ controller.ssl.storeFlavor }}" + truststore-path = "{{ openwhisk_home }}/ansible/roles/controller/files/{{ controllerKeystoreName }}" + truststore-password = "{{ controller.ssl.keystore.password }}" + client-auth = "{{ controller.ssl.clientAuth }}" + } + } } diff --git a/tests/src/test/scala/common/rest/WskRest.scala b/tests/src/test/scala/common/rest/WskRest.scala index 0b057ac..71ec04a 100644 --- a/tests/src/test/scala/common/rest/WskRest.scala +++ b/tests/src/test/scala/common/rest/WskRest.scala @@ -17,7 +17,7 @@ package common.rest -import java.io.File +import java.io.{File, FileInputStream} import java.time.Instant import java.util.Base64 import java.security.cert.X509Certificate @@ -78,17 +78,42 @@ import common.WskActorSystem import common.WskProps import whisk.core.entity.ByteSize import whisk.utils.retry -import javax.net.ssl.{HostnameVerifier, KeyManager, SSLContext, SSLSession, X509TrustManager} +import javax.net.ssl._ import com.typesafe.sslconfig.akka.AkkaSSLConfig import java.nio.charset.StandardCharsets +import java.security.KeyStore + +import akka.actor.ActorSystem +import pureconfig.loadConfigOrThrow +import whisk.common.Https.HttpsConfig class AcceptAllHostNameVerifier extends HostnameVerifier { override def verify(s: String, sslSession: SSLSession): Boolean = true } object SSL { - lazy val nonValidatingContext: SSLContext = { + + lazy val httpsConfig = loadConfigOrThrow[HttpsConfig]("whisk.controller.https") + + def keyManagers(clientAuth: Boolean) = { + if (clientAuth) + keyManagersForClientAuth + else + Array[KeyManager]() + } + + def keyManagersForClientAuth: Array[KeyManager] = { + val keyFactoryType = "SunX509" + val keystorePassword = httpsConfig.keystorePassword.toCharArray + val ks: KeyStore = KeyStore.getInstance(httpsConfig.keystoreFlavor) + ks.load(new FileInputStream(httpsConfig.keystorePath), httpsConfig.keystorePassword.toCharArray) + val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(keyFactoryType) + keyManagerFactory.init(ks, keystorePassword) + keyManagerFactory.getKeyManagers + } + + def nonValidatingContext(clientAuth: Boolean = false): SSLContext = { class IgnoreX509TrustManager extends X509TrustManager { def checkClientTrusted(chain: Array[X509Certificate], authType: String) = () def checkServerTrusted(chain: Array[X509Certificate], authType: String) = () @@ -96,9 +121,35 @@ object SSL { } val context = SSLContext.getInstance("TLS") - context.init(Array[KeyManager](), Array(new IgnoreX509TrustManager), null) + + context.init(keyManagers(clientAuth), Array(new IgnoreX509TrustManager), null) context } + + def httpsConnectionContext(implicit system: ActorSystem) = { + val sslConfig = AkkaSSLConfig().mapSettings { s => + s.withHostnameVerifierClass(classOf[AcceptAllHostNameVerifier].asInstanceOf[Class[HostnameVerifier]]) + } + new HttpsConnectionContext(SSL.nonValidatingContext(httpsConfig.clientAuth.toBoolean), Some(sslConfig)) + } +} + +object HttpConnection { + + /** + * Returns either the https context that is tailored for self-signed certificates on the controller, or + * a default connection context used in Http.SingleRequest + * @param protocol protocol used to communicate with controller API + * @param system actor system + * @return https connection context + */ + def getContext(protocol: String)(implicit system: ActorSystem) = { + if (protocol == "https") + SSL.httpsConnectionContext + else +// supports http + Http().defaultClientHttpsContext + } } class WskRest() extends RunWskRestCmd with BaseWsk { @@ -1170,6 +1221,7 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers with ScalaFu implicit val config = PatienceConfig(100 seconds, 15 milliseconds) implicit val materializer = ActorMaterializer() + val protocol = loadConfigOrThrow[String]("whisk.controller.protocol") val idleTimeout = 90 seconds val queueSize = 10 val maxOpenRequest = 1024 @@ -1180,7 +1232,7 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers with ScalaFu s.withHostnameVerifierClass(classOf[AcceptAllHostNameVerifier].asInstanceOf[Class[HostnameVerifier]]) } - val connectionContext = new HttpsConnectionContext(SSL.nonValidatingContext, Some(sslConfig)) + val connectionContext = new HttpsConnectionContext(SSL.nonValidatingContext(), Some(sslConfig)) def isStatusCodeExpected(expectedExitCode: Int, statusCode: Int): Boolean = { if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT)) diff --git a/tests/src/test/scala/ha/CacheInvalidationTests.scala b/tests/src/test/scala/ha/CacheInvalidationTests.scala index 59bc36d..9841a5e 100644 --- a/tests/src/test/scala/ha/CacheInvalidationTests.scala +++ b/tests/src/test/scala/ha/CacheInvalidationTests.scala @@ -19,12 +19,10 @@ package ha import scala.concurrent.Await import scala.concurrent.duration.DurationInt - import org.junit.runner.RunWith import org.scalatest.FlatSpec import org.scalatest.Matchers import org.scalatest.junit.JUnitRunner - import akka.http.scaladsl.Http import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.marshalling.Marshal @@ -36,9 +34,11 @@ import akka.stream.ActorMaterializer import common.WhiskProperties import common.WskActorSystem import common.WskTestHelpers +import common.rest.HttpConnection import spray.json._ import spray.json.DefaultJsonProtocol._ import whisk.core.WhiskConfig +import pureconfig.loadConfigOrThrow @RunWith(classOf[JUnitRunner]) class CacheInvalidationTests extends FlatSpec with Matchers with WskTestHelpers with WskActorSystem { @@ -47,11 +47,17 @@ class CacheInvalidationTests extends FlatSpec with Matchers with WskTestHelpers val hosts = WhiskProperties.getProperty("controller.hosts").split(",") + val controllerProtocol = loadConfigOrThrow[String]("whisk.controller.protocol") + val connectionContext = HttpConnection.getContext(controllerProtocol) + def ports(instance: Int) = WhiskProperties.getControllerBasePort + instance def controllerUri(instance: Int) = { require(instance >= 0 && instance < hosts.length, "Controller instance not known.") - Uri().withScheme("http").withHost(hosts(instance)).withPort(ports(instance)) + Uri() + .withScheme(controllerProtocol) + .withHost(hosts(instance)) + .withPort(ports(instance)) } def actionPath(name: String) = Uri.Path(s"/api/v1/namespaces/_/actions/$name") @@ -71,13 +77,15 @@ class CacheInvalidationTests extends FlatSpec with Matchers with WskTestHelpers val request = Marshal(body).to[RequestEntity].flatMap { entity => Http() - .singleRequest(HttpRequest( - method = HttpMethods.PUT, - uri = controllerUri(controllerInstance) - .withPath(actionPath(name)) - .withQuery(Uri.Query("overwrite" -> true.toString)), - headers = List(authHeader), - entity = entity)) + .singleRequest( + HttpRequest( + method = HttpMethods.PUT, + uri = controllerUri(controllerInstance) + .withPath(actionPath(name)) + .withQuery(Uri.Query("overwrite" -> true.toString)), + headers = List(authHeader), + entity = entity), + connectionContext = connectionContext) .flatMap { response => Unmarshal(response).to[JsObject].map { resBody => withClue(s"Error in Body: $resBody")(response.status shouldBe StatusCodes.OK) @@ -95,7 +103,8 @@ class CacheInvalidationTests extends FlatSpec with Matchers with WskTestHelpers HttpRequest( method = HttpMethods.GET, uri = controllerUri(controllerInstance).withPath(actionPath(name)), - headers = List(authHeader))) + headers = List(authHeader)), + connectionContext = connectionContext) .flatMap { response => Unmarshal(response).to[JsObject].map { resBody => withClue(s"Wrong statuscode from controller. Body is: $resBody")(response.status shouldBe expectedCode) @@ -114,7 +123,8 @@ class CacheInvalidationTests extends FlatSpec with Matchers with WskTestHelpers HttpRequest( method = HttpMethods.DELETE, uri = controllerUri(controllerInstance).withPath(actionPath(name)), - headers = List(authHeader))) + headers = List(authHeader)), + connectionContext = connectionContext) .flatMap { response => Unmarshal(response).to[JsObject].map { resBody => expectedCode.map { code => diff --git a/tests/src/test/scala/ha/ShootComponentsTests.scala b/tests/src/test/scala/ha/ShootComponentsTests.scala index a3ea2b5..e3480bc 100644 --- a/tests/src/test/scala/ha/ShootComponentsTests.scala +++ b/tests/src/test/scala/ha/ShootComponentsTests.scala @@ -33,7 +33,7 @@ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.ActorMaterializer import common._ -import common.rest.WskRest +import common.rest.{HttpConnection, WskRest} import pureconfig._ import spray.json._ import spray.json.DefaultJsonProtocol._ @@ -60,6 +60,8 @@ class ShootComponentsTests implicit val materializer = ActorMaterializer() implicit val testConfig = PatienceConfig(1.minute) + val controllerProtocol = loadConfigOrThrow[String]("whisk.controller.protocol") + // Throttle requests to the remaining controllers to avoid getting 429s. (60 req/min) val amountOfControllers = WhiskProperties.getProperty(WhiskConfig.controllerInstances).toInt val limit = WhiskProperties.getProperty(WhiskConfig.actionInvokePerMinuteLimit).toDouble @@ -79,8 +81,15 @@ class ShootComponentsTests val dbWhiskAuth = dbConfig.databases.get("WhiskAuth").get def ping(host: String, port: Int, path: String = "/") = { + + val connectionContext = HttpConnection.getContext(controllerProtocol) + val response = Try { - Http().singleRequest(HttpRequest(uri = s"http://$host:$port$path")).futureValue + Http() + .singleRequest( + HttpRequest(uri = s"$controllerProtocol://$host:$port$path"), + connectionContext = connectionContext) + .futureValue }.toOption response.map { res => diff --git a/tests/src/test/scala/services/HeadersTests.scala b/tests/src/test/scala/services/HeadersTests.scala index ab3792c..44240d7 100644 --- a/tests/src/test/scala/services/HeadersTests.scala +++ b/tests/src/test/scala/services/HeadersTests.scala @@ -21,20 +21,17 @@ import scala.concurrent.Future import scala.concurrent.duration.DurationInt import scala.language.postfixOps import scala.collection.immutable.Seq - import org.junit.runner.RunWith import org.scalatest.FlatSpec import org.scalatest.Matchers import org.scalatest.concurrent.ScalaFutures import org.scalatest.junit.JUnitRunner import org.scalatest.time.Span.convertDurationToSpan - import common.TestUtils import common.WhiskProperties -import common.rest.WskRest +import common.rest.{HttpConnection, WskRest} import common.WskProps import common.WskTestHelpers - import akka.http.scaladsl.model.Uri import akka.http.scaladsl.model.Uri.Path import akka.http.scaladsl.model.headers.BasicHttpCredentials @@ -52,8 +49,8 @@ import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.HttpMethod import akka.http.scaladsl.model.HttpHeader import akka.stream.ActorMaterializer - import common.WskActorSystem +import pureconfig.loadConfigOrThrow @RunWith(classOf[JUnitRunner]) class HeadersTests extends FlatSpec with Matchers with ScalaFutures with WskActorSystem with WskTestHelpers { @@ -62,12 +59,14 @@ class HeadersTests extends FlatSpec with Matchers with ScalaFutures with WskActo implicit val materializer = ActorMaterializer() + val controllerProtocol = loadConfigOrThrow[String]("whisk.controller.protocol") + println(loadConfigOrThrow[String]("whisk")) val whiskAuth = WhiskProperties.getBasicAuth val creds = BasicHttpCredentials(whiskAuth.fst, whiskAuth.snd) val allMethods = Some(Set(DELETE.name, GET.name, POST.name, PUT.name)) val allowOrigin = `Access-Control-Allow-Origin`.* val allowHeaders = `Access-Control-Allow-Headers`("Authorization", "Content-Type") - val url = Uri(s"http://${WhiskProperties.getBaseControllerAddress()}") + val url = Uri(s"$controllerProtocol://${WhiskProperties.getBaseControllerAddress()}") def request(method: HttpMethod, uri: Uri, headers: Option[Seq[HttpHeader]] = None): Future[HttpResponse] = { val httpRequest = headers match { @@ -75,7 +74,8 @@ class HeadersTests extends FlatSpec with Matchers with ScalaFutures with WskActo case None => HttpRequest(method, uri) } - Http().singleRequest(httpRequest) + val connectionContext = HttpConnection.getContext(controllerProtocol) + Http().singleRequest(httpRequest, connectionContext = connectionContext) } implicit val config = PatienceConfig(10 seconds, 0 milliseconds) diff --git a/tools/travis/setup.sh b/tools/travis/setup.sh index 26c5df1..ab35954 100755 --- a/tools/travis/setup.sh +++ b/tools/travis/setup.sh @@ -32,4 +32,4 @@ docker info pip install --user couchdb # Ansible -pip install --user ansible==2.3.0.0 +pip install --user ansible==2.4.2.0 -- To stop receiving notification emails like this one, please contact cbic...@apache.org.