Skip to content

Commit

Permalink
feat(proxy): A simple extension point around ProxyConfig (#1331)
Browse files Browse the repository at this point in the history
This PR supports bundling of proxy configuration alongside the
plugins that require it.
  • Loading branch information
ajordens authored Sep 9, 2020
1 parent 1b3eeda commit ce931c1
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 137 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2020 Netflix, Inc.
*
* Licensed 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 com.netflix.spinnaker.gate.api.extension;

import com.netflix.spinnaker.kork.annotations.Alpha;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;

@Data
@Alpha
public class ProxyConfig {
/** Identifier for this proxy, must be unique. */
private String id;

/** Target uri for this proxy. */
private String uri;

/** Whether ssl hostname verification should be skipped. */
private Boolean skipHostnameVerification = false;

/** Fully qualified path to keystore file. */
private String keyStore;

/** Type of keystore, defaults to {@code KeyStore.getDefaultType()}. */
private String keyStoreType = KeyStore.getDefaultType();

/**
* Plain text keystore password.
*
* <p>If keyStore is non-null, one of keyStorePassword or keyStorePasswordFile must be supplied.
*/
private String keyStorePassword;

/**
* Fully qualified path to keystore password file.
*
* <p>If keyStore is non-null, one of keyStorePassword or keyStorePasswordFile must be supplied.
*/
private String keyStorePasswordFile;

/** Fully qualified path to truststore file. */
private String trustStore;

/** Type of truststore, defaults to {@code KeyStore.getDefaultType()}. */
private String trustStoreType = KeyStore.getDefaultType();

/**
* Plain text truststore password.
*
* <p>If trustStore is non-null, one of trustStorePassword or trustStorePasswordFile must be
* supplied.
*/
private String trustStorePassword;

/**
* Fully qualified path to truststore password file.
*
* <p>If trustStore is non-null, one of trustStorePassword or trustStorePasswordFile must be
* supplied.
*/
private String trustStorePasswordFile;

/** Supported http methods for this proxy. */
private List<String> methods = new ArrayList<>();

/** Connection timeout, defaults to 30s. */
private Long connectTimeoutMs = 30_000L;

/** Read timeout, defaults to 59s. */
private Long readTimeoutMs = 59_000L;

/** Write timeout, defaults to 30s. */
private Long writeTimeoutMs = 30_000L;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2020 Netflix, Inc.
*
* Licensed 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 com.netflix.spinnaker.gate.api.extension;

import com.netflix.spinnaker.kork.annotations.Alpha;
import com.netflix.spinnaker.kork.plugins.api.internal.SpinnakerExtensionPoint;
import java.util.List;

/** Extension point for configuring proxy endpoints. */
@Alpha
public interface ProxyConfigProvider extends SpinnakerExtensionPoint {

/**
* Provides list of proxy configurations that will be exposed via the ProxyController.
*
* @return list of proxy configurations
*/
List<? extends ProxyConfig> getProxyConfigs();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,12 @@

package com.netflix.spinnaker.config

import com.squareup.okhttp.OkHttpClient
import java.io.File
import java.security.KeyStore
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.annotation.PostConstruct
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import org.slf4j.LoggerFactory
import com.netflix.spinnaker.gate.api.extension.ProxyConfig
import com.netflix.spinnaker.gate.api.extension.ProxyConfigProvider
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Component

@Configuration
@EnableConfigurationProperties(
Expand All @@ -40,120 +31,11 @@ open class ProxyConfiguration

@ConfigurationProperties
data class ProxyConfigurationProperties(var proxies: List<ProxyConfig> = mutableListOf()) {

companion object {
val logger = LoggerFactory.getLogger(ProxyConfig::class.java)
}

@PostConstruct
fun postConstruct() {
for (proxy in proxies) {
try {
// initialize the `okHttpClient` for each proxy
proxy.init()
} catch (e: Exception) {
logger.error("Failed to initialize proxy (id: ${proxy.id})", e)
}
}
}
}

data class ProxyConfig(
var id: String? = null,
var uri: String? = null,
var skipHostnameVerification: Boolean = false,
var keyStore: String? = null,
var keyStoreType: String = KeyStore.getDefaultType(),
var keyStorePassword: String? = null,
var keyStorePasswordFile: String? = null,
var trustStore: String? = null,
var trustStoreType: String = KeyStore.getDefaultType(),
var trustStorePassword: String? = null,
var trustStorePasswordFile: String? = null,
var methods: List<String> = mutableListOf(),
var connectTimeoutMs: Long = 30_000,
var readTimeoutMs: Long = 59_000,
var writeTimeoutMs: Long = 30_000
) {

companion object {
val logger = LoggerFactory.getLogger(ProxyConfig::class.java)
@Component
class DefaultProxyConfigProvider(val proxyConfigurationProperties: ProxyConfigurationProperties) : ProxyConfigProvider {
override fun getProxyConfigs(): List<ProxyConfig> {
return proxyConfigurationProperties.proxies
}

var okHttpClient = OkHttpClient()

fun init() {
okHttpClient.setConnectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
okHttpClient.setReadTimeout(readTimeoutMs, TimeUnit.MILLISECONDS)
okHttpClient.setWriteTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS)

if (skipHostnameVerification) {
this.okHttpClient = okHttpClient.setHostnameVerifier({ hostname, _ ->
logger.warn("Skipping hostname verification on request to $hostname (id: $id)")
true
})
}

if (!keyStore.isNullOrEmpty()) {
val keyStorePassword = if (!keyStorePassword.isNullOrEmpty()) {
keyStorePassword
} else if (!keyStorePasswordFile.isNullOrEmpty()) {
File(keyStorePasswordFile).readText()
} else {
throw IllegalStateException("No `keyStorePassword` or `keyStorePasswordFile` specified (id: $id)")
}

val kStore = KeyStore.getInstance(keyStoreType)

File(this.keyStore).inputStream().use {
kStore.load(it, keyStorePassword!!.toCharArray())
}

val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(kStore, keyStorePassword!!.toCharArray())

val keyManagers = kmf.keyManagers
var trustManagers: Array<TrustManager>? = null

if (!trustStore.isNullOrEmpty()) {
if (trustStore.equals("*")) {
trustManagers = arrayOf(TrustAllTrustManager())
} else {
val trustStorePassword = if (!trustStorePassword.isNullOrEmpty()) {
trustStorePassword
} else if (!trustStorePasswordFile.isNullOrEmpty()) {
File(trustStorePasswordFile).readText()
} else {
throw IllegalStateException("No `trustStorePassword` or `trustStorePasswordFile` specified (id: $id)")
}

val tStore = KeyStore.getInstance(trustStoreType)
File(this.trustStore).inputStream().use {
tStore.load(it, trustStorePassword!!.toCharArray())
}

val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
tmf.init(tStore)

trustManagers = tmf.trustManagers
}
}

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, trustManagers, null)

this.okHttpClient = okHttpClient.setSslSocketFactory(sslContext.socketFactory)
}
}
}

class TrustAllTrustManager : X509TrustManager {
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {
// do nothing
}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {
// do nothing
}

override fun getAcceptedIssuers() = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2020 Netflix, Inc.
*
* Licensed 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 com.netflix.spinnaker.gate.controllers

import com.netflix.spinnaker.gate.api.extension.ProxyConfig
import com.squareup.okhttp.OkHttpClient
import org.slf4j.LoggerFactory
import java.io.File
import java.security.KeyStore
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager

internal class Proxy(val config: ProxyConfig) {
companion object {
val logger = LoggerFactory.getLogger(ProxyConfig::class.java)
}

var okHttpClient = OkHttpClient()

fun init() {
with(config) {
okHttpClient.setConnectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
okHttpClient.setReadTimeout(readTimeoutMs, TimeUnit.MILLISECONDS)
okHttpClient.setWriteTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS)

if (skipHostnameVerification) {
okHttpClient = okHttpClient.setHostnameVerifier({ hostname, _ ->
logger.warn("Skipping hostname verification on request to $hostname (id: $id)")
true
})
}

if (!keyStore.isNullOrEmpty()) {
val keyStorePassword = if (!keyStorePassword.isNullOrEmpty()) {
keyStorePassword
} else if (!keyStorePasswordFile.isNullOrEmpty()) {
File(keyStorePasswordFile).readText()
} else {
throw IllegalStateException("No `keyStorePassword` or `keyStorePasswordFile` specified (id: $id)")
}

val kStore = KeyStore.getInstance(keyStoreType)

File(this.keyStore).inputStream().use {
kStore.load(it, keyStorePassword!!.toCharArray())
}

val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(kStore, keyStorePassword!!.toCharArray())

val keyManagers = kmf.keyManagers
var trustManagers: Array<TrustManager>? = null

if (!trustStore.isNullOrEmpty()) {
if (trustStore.equals("*")) {
trustManagers = arrayOf(TrustAllTrustManager())
} else {
val trustStorePassword = if (!trustStorePassword.isNullOrEmpty()) {
trustStorePassword
} else if (!trustStorePasswordFile.isNullOrEmpty()) {
File(trustStorePasswordFile).readText()
} else {
throw IllegalStateException("No `trustStorePassword` or `trustStorePasswordFile` specified (id: $id)")
}

val tStore = KeyStore.getInstance(trustStoreType)
File(this.trustStore).inputStream().use {
tStore.load(it, trustStorePassword!!.toCharArray())
}

val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
tmf.init(tStore)

trustManagers = tmf.trustManagers
}
}

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, trustManagers, null)

okHttpClient = okHttpClient.setSslSocketFactory(sslContext.socketFactory)
}
}
}
}

class TrustAllTrustManager : X509TrustManager {
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {
// do nothing
}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {
// do nothing
}

override fun getAcceptedIssuers() = null
}
Loading

0 comments on commit ce931c1

Please sign in to comment.