From 753db0ed224af3e634fe9d29cb0dfd7f71205650 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 4 May 2016 17:00:31 -0600 Subject: [PATCH] Implement VaultEnvironmentRepository. This is an alternative to VCS or file based repository implementations. All properties are stored ecrypted in vault, regardless of whether or not that actaully need to be encrypted. Supports properties style (this.is.a.property) or vault nested properties. Adds a watch to client and server, if enabled, the client will long poll the server and wait for changes rather than relying on a message broker to send change events. Adds a server side watch for consul keys, this is useful when vault is backed by consul (and currently the only use case, since there is not a consul repository implementation). Closes gh-397 --- .mvn/wrapper/maven-wrapper.properties | 2 +- docs/pom.xml | 2 +- pom.xml | 2 +- spring-cloud-config-client/pom.xml | 2 +- .../client/ConfigClientAutoConfiguration.java | 13 + .../config/client/ConfigClientProperties.java | 17 +- .../client/ConfigClientStateHolder.java | 24 + .../config/client/ConfigClientWatch.java | 82 ++ .../ConfigServiceBootstrapConfiguration.java | 4 +- .../ConfigServicePropertySourceLocator.java | 716 ++---------------- .../cloud/config/environment/Environment.java | 29 +- .../config/client/ConfigClientWatchTests.java | 38 + ...nfigServicePropertySourceLocatorTests.java | 6 +- spring-cloud-config-dependencies/pom.xml | 10 +- spring-cloud-config-monitor/pom.xml | 2 +- spring-cloud-config-sample/pom.xml | 2 +- .../src/main/java/sample/Application.java | 14 + spring-cloud-config-server/pom.xml | 2 +- .../config/ConfigServerMvcConfiguration.java | 9 +- .../EnvironmentRepositoryConfiguration.java | 33 + .../ResourceRepositoryConfiguration.java | 2 + .../CipherEnvironmentEncryptor.java | 3 +- .../environment/ConsulEnvironmentWatch.java | 126 +++ .../environment/EnvironmentCleaner.java | 3 +- .../environment/EnvironmentController.java | 4 +- ...ronmentEncryptorEnvironmentRepository.java | 2 +- .../server/environment/EnvironmentWatch.java | 16 + .../NativeEnvironmentRepository.java | 2 +- .../PassthruEnvironmentRepository.java | 2 +- .../VaultEnvironmentRepository.java | 247 ++++++ .../VaultEnvironmentRepositoryTests.java | 14 + spring-cloud-starter-config/pom.xml | 4 +- 32 files changed, 742 insertions(+), 692 deletions(-) create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientStateHolder.java create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientWatch.java create mode 100644 spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientWatchTests.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/ConsulEnvironmentWatch.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentWatch.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 6637cedb..c954cec9 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip \ No newline at end of file +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip diff --git a/docs/pom.xml b/docs/pom.xml index 8f4c8d57..77888e4b 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT pom Spring Cloud Config Docs diff --git a/pom.xml b/pom.xml index 7c8fc5a5..cc88c775 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT pom Spring Cloud Config Spring Cloud Config diff --git a/spring-cloud-config-client/pom.xml b/spring-cloud-config-client/pom.xml index 2cd1ca6f..efb362a0 100644 --- a/spring-cloud-config-client/pom.xml +++ b/spring-cloud-config-client/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT .. diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java index f1bb6102..ed950f76 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -80,4 +81,16 @@ public class ConfigClientAutoConfiguration { } } + @Configuration + @ConditionalOnClass(ContextRefresher.class) + @ConditionalOnBean(ContextRefresher.class) + @ConditionalOnProperty(value = "spring.cloud.config.watch.enabled") + protected static class ConfigClientWatchConfiguration { + + @Bean + public ConfigClientWatch configClientWatch(ContextRefresher contextRefresher) { + return new ConfigClientWatch(contextRefresher); + } + } + } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java index 689a39be..843b6d6a 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java @@ -34,6 +34,8 @@ import org.springframework.web.util.UriComponentsBuilder; public class ConfigClientProperties { public static final String PREFIX = "spring.cloud.config"; + public static final String TOKEN_HEADER = "X-Config-Token"; + public static final String STATE_HEADER = "X-Config-State"; /** * Flag to say that remote configuration is enabled. Default true; @@ -83,6 +85,11 @@ public class ConfigClientProperties { */ private boolean failFast = false; + /** + * Security Token passed thru to underlying environment repository. + */ + private String token; + private ConfigClientProperties() { } @@ -170,6 +177,14 @@ public class ConfigClientProperties { this.failFast = failFast; } + public String getToken() { + return this.token; + } + + public void setToken(String token) { + this.token = token; + } + private String[] extractCredentials() { String[] result = new String[3]; String uri = this.uri; @@ -272,7 +287,7 @@ public class ConfigClientProperties { + (this.label == null ? "" : this.label) + ", username=" + this.username + ", password=" + this.password + ", uri=" + this.uri + ", discovery.enabled=" + this.discovery.enabled + ", failFast=" - + this.failFast + "]"; + + this.failFast + ", token=" + this.token + "]"; } } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientStateHolder.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientStateHolder.java new file mode 100644 index 00000000..ca4d8a2b --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientStateHolder.java @@ -0,0 +1,24 @@ +package org.springframework.cloud.config.client; + +/** + * @author Spencer Gibb + */ +public class ConfigClientStateHolder { + private static ThreadLocal state = new ThreadLocal<>(); + + public static void resetState() { + state.remove(); + } + + public static void setState(String newState) { + if (newState == null) { + resetState(); + return; + } + state.set(newState); + } + + public static String getState() { + return state.get(); + } +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientWatch.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientWatch.java new file mode 100644 index 00000000..95e999bb --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientWatch.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * 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 org.springframework.cloud.config.client; + +import java.io.Closeable; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.context.refresh.ContextRefresher; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.Scheduled; + +import static org.springframework.util.StringUtils.hasText; + +/** + * @author Spencer Gibb + */ +public class ConfigClientWatch implements Closeable, EnvironmentAware { + + private static Log log = LogFactory + .getLog(ConfigServicePropertySourceLocator.class); + + private final AtomicBoolean running = new AtomicBoolean(false); + private final ContextRefresher refresher; + private Environment environment; + + public ConfigClientWatch(ContextRefresher refresher) { + this.refresher = refresher; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @PostConstruct + public void start() { + this.running.compareAndSet(false, true); + } + + @Scheduled(initialDelayString = "${spring.cloud.config.watch.initialDelay:180000}", fixedDelayString = "${spring.cloud.config.watch.delay:500}") + public void watchConfigServer() { + if (this.running.get()) { + String newState = this.environment.getProperty("config.client.state"); + String oldState = ConfigClientStateHolder.getState(); + + // only refresh if state has changed + if (stateChanged(oldState, newState)) { + ConfigClientStateHolder.setState(newState); + this.refresher.refresh(); + } + } + } + + /* for testing */ boolean stateChanged(String oldState, String newState) { + return (!hasText(oldState) && hasText(newState)) + || (hasText(oldState) && !oldState.equals(newState)); + } + + @Override + public void close() { + this.running.compareAndSet(true, false); + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java index 95fdd1ee..909d4070 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java @@ -51,9 +51,9 @@ public class ConfigServiceBootstrapConfiguration { @Bean @ConditionalOnProperty(value = "spring.cloud.config.enabled", matchIfMissing = true) - public ConfigServicePropertySourceLocator configServicePropertySource() { + public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) { ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator( - configClientProperties()); + properties); return locator; } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index b00d55e4..caf2b8f1 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -18,6 +18,7 @@ package org.springframework.cloud.config.client; import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; import java.util.Map; import org.apache.commons.logging.Log; @@ -29,6 +30,7 @@ import org.springframework.core.annotation.Order; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.MapPropertySource; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; @@ -37,12 +39,17 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.retry.annotation.Retryable; +import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; +import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; +import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER; + /** * @author Dave Syer * @@ -54,36 +61,40 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator .getLog(ConfigServicePropertySourceLocator.class); private RestTemplate restTemplate; - private ConfigClientProperties defaults; + private ConfigClientProperties defaultProperties; - public ConfigServicePropertySourceLocator(ConfigClientProperties defaults) { - this.defaults = defaults; + public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) { + this.defaultProperties = defaultProperties; } @Override @Retryable(interceptor = "configServerRetryInterceptor") public org.springframework.core.env.PropertySource locate( org.springframework.core.env.Environment environment) { - ConfigClientProperties client = this.defaults.override(environment); + ConfigClientProperties properties = this.defaultProperties.override(environment); CompositePropertySource composite = new CompositePropertySource("configService"); - RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(client) + RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties) : this.restTemplate; Exception error = null; String errorBody = null; - logger.info("Fetching config from server at: " + client.getRawUri()); + logger.info("Fetching config from server at: " + properties.getRawUri()); try { - String[] labels = new String[]{""}; - if (StringUtils.hasText(client.getLabel())) { - labels = StringUtils.commaDelimitedListToStringArray(client.getLabel()); + String[] labels = new String[] { "" }; + if (StringUtils.hasText(properties.getLabel())) { + labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel()); } + + String state = ConfigClientStateHolder.getState(); + // Try all the labels until one works for (String label : labels) { - Environment result = getRemoteEnvironment(restTemplate, client.getRawUri(), client.getName(), client.getProfile(), label.trim()); + Environment result = getRemoteEnvironment(restTemplate, + properties, label.trim(), state); if (result != null) { - logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s", + logger.info(String.format("Located environment: name=%s, profiles=%s, label=%s, version=%s, state=%s", result.getName(), result.getProfiles() == null ? "" : Arrays.asList(result.getProfiles()), - result.getLabel(), result.getVersion())); + result.getLabel(), result.getVersion(), result.getState())); for (PropertySource source : result.getPropertySources()) { @SuppressWarnings("unchecked") @@ -92,6 +103,13 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator composite.addPropertySource(new MapPropertySource(source .getName(), map)); } + + if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) { + HashMap map = new HashMap<>(); + putValue(map, "config.client.state", result.getState()); + putValue(map, "config.client.version", result.getVersion()); + composite.addFirstPropertySource(new MapPropertySource("configClient", map)); + } return composite; } } @@ -106,7 +124,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator catch (Exception e) { error = e; } - if (client != null && client.isFailFast()) { + if (properties.isFailFast()) { throw new IllegalStateException( "Could not locate PropertySource and the fail fast property is set, failing", error); @@ -117,8 +135,20 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator } - private Environment getRemoteEnvironment(RestTemplate restTemplate, String uri, String name, String profile, String label) { + private void putValue(HashMap map, String key, String value) { + if (StringUtils.hasText(value)) { + map.put(key, value); + } + } + + private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties, + String label, String state) { String path = "/{name}/{profile}"; + String name = properties.getName(); + String profile = properties.getProfile(); + String token = properties.getToken(); + String uri = properties.getRawUri(); + Object[] args = new String[] { name, profile }; if (StringUtils.hasText(label)) { args = new String[] { name, profile, label }; @@ -127,16 +157,24 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator ResponseEntity response = null; try { - response = restTemplate.exchange(uri + path, - HttpMethod.GET, new HttpEntity((Void) null), - Environment.class, args); - } catch (HttpClientErrorException e) { - if(e.getStatusCode() != HttpStatus.NOT_FOUND ) { + HttpHeaders headers = new HttpHeaders(); + if (StringUtils.hasText(token)) { + headers.add(TOKEN_HEADER, token); + } + if (StringUtils.hasText(state)) { //TODO: opt in to sending state? + headers.add(STATE_HEADER, state); + } + final HttpEntity entity = new HttpEntity<>((Void) null, headers); + response = restTemplate.exchange(uri + path, HttpMethod.GET, + entity, Environment.class, args); + } + catch (HttpClientErrorException e) { + if (e.getStatusCode() != HttpStatus.NOT_FOUND) { throw e; } } - if (response==null || response.getStatusCode()!=HttpStatus.OK) { + if (response == null || response.getStatusCode() != HttpStatus.OK) { return null; } Environment result = response.getBody(); @@ -148,7 +186,9 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator } private RestTemplate getSecureRestTemplate(ConfigClientProperties client) { - RestTemplate template = new RestTemplate(); + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setReadTimeout((60 * 1000 * 3) + 5000); //TODO 3m5s, make configurable? + RestTemplate template = new RestTemplate(requestFactory); String password = client.getPassword(); if (password != null) { template.setInterceptors(Arrays @@ -173,8 +213,7 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { - byte[] token = Base64 - .encode((this.username + ":" + this.password).getBytes()); + byte[] token = Base64Utils.encode((this.username + ":" + this.password).getBytes()); request.getHeaders().add("Authorization", "Basic " + new String(token)); return execution.execute(request, body); } @@ -182,634 +221,3 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator } } - -/** - * Base64 encoder which is a reduced version of Robert Harder's public domain - * implementation (version 2.3.7). See http://iharder.net/base64 for more information. - *

- * For internal use only. - * - * @author Luke Taylor - * @since 3.0 - */ -final class Base64 { - - /** No options specified. Value is zero. */ - public final static int NO_OPTIONS = 0; - - /** Specify encoding in first bit. Value is one. */ - public final static int ENCODE = 1; - - /** Specify decoding in first bit. Value is zero. */ - public final static int DECODE = 0; - - /** Do break lines when encoding. Value is 8. */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described in - * Section 4 of RFC3548: http://www.faqs - * .org/rfcs/rfc3548.html. It is important to note that data encoded this way is - * not officially valid Base64, or at the very least should not be called - * Base64 without also specifying that is was encoded using the URL- and Filename-safe - * dialect. - */ - public final static int URL_SAFE = 16; - - /** - * Encode using the special "ordered" dialect of Base64 described here: http://www.faqs.org/qa/rfcc-1940.html. - */ - public final static int ORDERED = 32; - - /** Maximum line length (76) of Base64 output. */ - private final static int MAX_LINE_LENGTH = 76; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte) '\n'; - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding - - /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** The 64 valid Base64 values. */ - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private final static byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B', - (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', - (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', - (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', - (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', - (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', - (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', - (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) '+', (byte) '/' }; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value or a negative - * number indicating some other meaning. - **/ - private final static byte[] _STANDARD_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, - -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" - * and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { (byte) 'A', (byte) 'B', - (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', - (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', - (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', - (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', - (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', - (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', - (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) '-', (byte) '_' }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, - -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but someone requested it, and it is - * described here: http://www.faqs.org/ - * qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { (byte) '-', (byte) '0', (byte) '1', - (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', - (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', - (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', - (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', - (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a', - (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', - (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', - (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z' }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, - -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' - 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' - 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 - , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 - }; - - public static byte[] decode(byte[] bytes) { - return decode(bytes, 0, bytes.length, NO_OPTIONS); - } - - public static byte[] encode(byte[] bytes) { - return encodeBytesToBytes(bytes, 0, bytes.length, NO_OPTIONS); - } - - public static boolean isBase64(byte[] bytes) { - try { - decode(bytes); - } - catch (InvalidBase64CharacterException e) { - return false; - } - return true; - } - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options - * specified. It's possible, though silly, to specify ORDERED and URLSAFE in - * which case one of them will be picked, though there is no guarantee as to which one - * will be picked. - */ - private static byte[] getAlphabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } - else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } - else { - return _STANDARD_ALPHABET; - } - } - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on the options - * specified. It's possible, though silly, to specify ORDERED and URL_SAFE in which - * case one of them will be picked, though there is no guarantee as to which one will - * be picked. - */ - private static byte[] getDecodabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } - else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } - else { - return _STANDARD_DECODABET; - } - } - - /* ******** E N C O D I N G M E T H O D S ******** */ - - /** - *

- * Encodes up to three bytes of the array source and writes the resulting - * four Base64 bytes to destination. The source and destination arrays can - * be manipulated anywhere along their length by specifying srcOffset and - * destOffset. This method does not check to make sure your arrays are - * large enough to accomodate srcOffset + 3 for the source array - * or destOffset + 4 for the destination array. The actual - * number of significant bytes in your array is given by numSigBytes. - *

- *

- * This is the lowest level of the encoding methods with all possible parameters. - *

- * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options) { - - byte[] ALPHABET = getAlphabet(options); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; - return destination; - - case 2: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - case 1: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - default: - return destination; - } - } - - /** - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @see Base64#DO_BREAK_LINES - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 2.3.1 - */ - private static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) { - - if (source == null) { - throw new NullPointerException("Cannot serialize a null array."); - } // end if: null - - if (off < 0) { - throw new IllegalArgumentException("Cannot have negative offset: " + off); - } // end if: off < 0 - - if (len < 0) { - throw new IllegalArgumentException("Cannot have length offset: " + len); - } // end if: len < 0 - - if (off + len > source.length) { - throw new IllegalArgumentException(String.format( - "Cannot have offset of %d and length of %d with array of length %d", - off, len, source.length)); - } // end if: off < 0 - - boolean breakLines = (options & DO_BREAK_LINES) > 0; - - // int len43 = len * 4 / 3; - // byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual - // encoding - if (breakLines) { - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters - } - byte[] outBuff = new byte[encLen]; - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - encode3to4(source, d + off, 3, outBuff, e, options); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, options); - e += 4; - } // end if: some padding needed - - // Only resize array if we didn't guess it right. - if (e <= outBuff.length - 1) { - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff, 0, finalOut, 0, e); - // System.err.println("Having to resize array from " + outBuff.length + " to " - // + e ); - return finalOut; - } - else { - // System.err.println("No need to resize array."); - return outBuff; - } - } - - /* ******** D E C O D I N G M E T H O D S ******** */ - - /** - * Decodes four bytes from array source and writes the resulting bytes (up - * to three of them) to destination. The source and destination arrays can - * be manipulated anywhere along their length by specifying srcOffset and - * destOffset. This method does not check to make sure your arrays are - * large enough to accomodate srcOffset + 4 for the source array - * or destOffset + 3 for the destination array. This method - * returns the actual number of bytes that were converted from the Base64 encoding. - *

- * This is the lowest level of the decoding methods with all possible parameters. - *

- * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted - * @throws NullPointerException if source or destination arrays are null - * @throws IllegalArgumentException if srcOffset or destOffset are invalid or there is - * not enough room in the array. - * @since 1.3 - */ - private static int decode4to3(final byte[] source, final int srcOffset, - final byte[] destination, final int destOffset, final int options) { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Source array was null."); - } // end if - if (destination == null) { - throw new NullPointerException("Destination array was null."); - } // end if - if (srcOffset < 0 || srcOffset + 3 >= source.length) { - throw new IllegalArgumentException( - String.format( - "Source array with length %d cannot have offset of %d and still process four bytes.", - source.length, srcOffset)); - } // end if - if (destOffset < 0 || destOffset + 2 >= destination.length) { - throw new IllegalArgumentException( - String.format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", - destination.length, destOffset)); - } // end if - - byte[] DECODABET = getDecodabet(options); - - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } - - // Example: DkL= - else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) - | ((DECODABET[source[srcOffset + 3]] & 0xFF)); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - - return 3; - } - } - - /** - * Low-level access to decoding ASCII characters in the form of a byte array. - * Ignores GUNZIP option, if it's set. This is not generally a - * recommended method, although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, if you need more speed - * and reduced memory footprint (and aren't gzipping), consider this method. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param options Can specify options such as alphabet type to use - * @return decoded data - * @throws IllegalArgumentException If bogus characters exist in source data - */ - private static byte[] decode(final byte[] source, final int off, final int len, - final int options) { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Cannot decode null source array."); - } // end if - if (off < 0 || off + len > source.length) { - throw new IllegalArgumentException( - String.format( - "Source array with length %d cannot have offset of %d and process %d bytes.", - source.length, off, len)); - } // end if - - if (len == 0) { - return new byte[0]; - } - else if (len < 4) { - throw new IllegalArgumentException( - "Base64-encoded string must have at least four characters, but length specified was " - + len); - } // end if - - byte[] DECODABET = getDecodabet(options); - - int len34 = len * 3 / 4; // Estimate on array size - byte[] outBuff = new byte[len34]; // Upper limit on size of output - int outBuffPosn = 0; // Keep track of where we're writing - - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiDecode = 0; // Special value from DECODABET - - for (i = off; i < off + len; i++) { // Loop through source - - sbiDecode = DECODABET[source[i] & 0xFF]; - - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if (sbiDecode >= WHITE_SPACE_ENC) { - if (sbiDecode >= EQUALS_SIGN_ENC) { - b4[b4Posn++] = source[i]; // Save non-whitespace - if (b4Posn > 3) { // Time to decode? - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if (source[i] == EQUALS_SIGN) { - break; - } - } - } - } - else { - // There's a bad input character in the Base64 stream. - throw new InvalidBase64CharacterException(String.format( - "Bad Base64 input character decimal %d in array position %d", - (source[i]) & 0xFF, i)); - } - } - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } -} - -@SuppressWarnings("serial") -class InvalidBase64CharacterException extends IllegalArgumentException { - - InvalidBase64CharacterException(String message) { - super(message); - } -} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/environment/Environment.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/environment/Environment.java index ebad1143..c8794f7b 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/environment/Environment.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/environment/Environment.java @@ -40,24 +40,36 @@ public class Environment { private String label; - private List propertySources = new ArrayList(); + private List propertySources = new ArrayList<>(); private String version; + private String state; + public Environment(String name, String... profiles) { - this(name, profiles, "master", null); + this(name, profiles, "master", null, null); + } + + /** + * Copies all fields except propertySources + * @param env + */ + public Environment(Environment env) { + this(env.getName(), env.getProfiles(), env.getLabel(), env.getVersion(), env.getState()); } @JsonCreator public Environment(@JsonProperty("name") String name, @JsonProperty("profiles") String[] profiles, @JsonProperty("label") String label, - @JsonProperty("version") String version) { + @JsonProperty("version") String version, + @JsonProperty("state") String state) { super(); this.name = name; this.profiles = profiles; this.label = label; this.version = version; + this.state = state; } public void add(PropertySource propertySource) { @@ -104,11 +116,20 @@ public class Environment { this.version = version; } + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + @Override public String toString() { return "Environment [name=" + name + ", profiles=" + Arrays.asList(profiles) + ", label=" + label + ", propertySources=" + propertySources - + ", version=" + version+ "]"; + + ", version=" + version + + ", state=" + state + "]"; } } diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientWatchTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientWatchTests.java new file mode 100644 index 00000000..09850703 --- /dev/null +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigClientWatchTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2016 the original author or authors. + * + * 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 org.springframework.cloud.config.client; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +/** + * @author Spencer Gibb + */ +public class ConfigClientWatchTests { + + @Test + public void stateChangedWorks() { + ConfigClientWatch watch = new ConfigClientWatch(null); + assertThat(watch.stateChanged(null, "1"), is(true)); + assertThat(watch.stateChanged("1", "2"), is(true)); + assertThat(watch.stateChanged("1", null), is(true)); + assertThat(watch.stateChanged("1", "1"), is(false)); + } +} diff --git a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java index ec6cc36a..58dfcca3 100644 --- a/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java +++ b/spring-cloud-config-client/src/test/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocatorTests.java @@ -44,7 +44,7 @@ public class ConfigServicePropertySourceLocatorTests { @Test public void sunnyDay() { Environment body = new Environment("app", "master"); - mockRequestResponseWithoutLabel(new ResponseEntity(body, + mockRequestResponseWithoutLabel(new ResponseEntity<>(body, HttpStatus.OK)); this.locator.setRestTemplate(this.restTemplate); assertNotNull(this.locator.locate(this.environment)); @@ -54,7 +54,7 @@ public class ConfigServicePropertySourceLocatorTests { public void sunnyDayWithLabel() { Environment body = new Environment("app", "master"); mockRequestResponseWithLabel( - new ResponseEntity(body, HttpStatus.OK), "v1.0.0"); + new ResponseEntity<>(body, HttpStatus.OK), "v1.0.0"); this.locator.setRestTemplate(this.restTemplate); EnvironmentTestUtils.addEnvironment(this.environment, "spring.cloud.config.label:v1.0.0"); @@ -71,7 +71,7 @@ public class ConfigServicePropertySourceLocatorTests { @Test public void failsQuietly() { - mockRequestResponseWithoutLabel(new ResponseEntity("Wah!", + mockRequestResponseWithoutLabel(new ResponseEntity<>("Wah!", HttpStatus.INTERNAL_SERVER_ERROR)); this.locator.setRestTemplate(this.restTemplate); assertNull(this.locator.locate(this.environment)); diff --git a/spring-cloud-config-dependencies/pom.xml b/spring-cloud-config-dependencies/pom.xml index 0ab93cb2..8a7e6a39 100644 --- a/spring-cloud-config-dependencies/pom.xml +++ b/spring-cloud-config-dependencies/pom.xml @@ -9,7 +9,7 @@ spring-cloud-config-dependencies - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT pom spring-cloud-config-dependencies Spring Cloud Config Dependencies @@ -28,22 +28,22 @@ org.springframework.cloud spring-cloud-starter-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT org.springframework.cloud spring-cloud-config-client - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT org.springframework.cloud spring-cloud-config-server - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT org.springframework.cloud spring-cloud-config-monitor - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT org.eclipse.jgit diff --git a/spring-cloud-config-monitor/pom.xml b/spring-cloud-config-monitor/pom.xml index 28a3ae68..e2cb6def 100644 --- a/spring-cloud-config-monitor/pom.xml +++ b/spring-cloud-config-monitor/pom.xml @@ -5,7 +5,7 @@ org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT .. spring-cloud-config-monitor diff --git a/spring-cloud-config-sample/pom.xml b/spring-cloud-config-sample/pom.xml index 3c8a0768..69eea2f5 100644 --- a/spring-cloud-config-sample/pom.xml +++ b/spring-cloud-config-sample/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT .. diff --git a/spring-cloud-config-sample/src/main/java/sample/Application.java b/spring-cloud-config-sample/src/main/java/sample/Application.java index c00219ea..3f5bda63 100644 --- a/spring-cloud-config-sample/src/main/java/sample/Application.java +++ b/spring-cloud-config-sample/src/main/java/sample/Application.java @@ -1,12 +1,26 @@ package sample; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +@RestController @SpringBootApplication public class Application { + @Autowired + private Environment environment; + + @RequestMapping("/") + public String query(@RequestParam("q") String q) { + return environment.getProperty(q); + } + public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index 4bd08b1b..b896d3c3 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT .. diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java index 836e8dc3..19c27209 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ConfigServerMvcConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.cloud.config.server.config; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; import org.springframework.cloud.config.server.environment.EnvironmentController; @@ -42,9 +43,6 @@ public class ConfigServerMvcConfiguration extends WebMvcConfigurerAdapter { @Autowired private EnvironmentRepository repository; - @Autowired - private ResourceRepository resources; - @Autowired private ConfigServerProperties server; @@ -69,8 +67,9 @@ public class ConfigServerMvcConfiguration extends WebMvcConfigurerAdapter { } @Bean - public ResourceController resourceController() { - ResourceController controller = new ResourceController(this.resources, + @ConditionalOnBean(ResourceRepository.class) + public ResourceController resourceController(ResourceRepository repository) { + ResourceController controller = new ResourceController(repository, encrypted()); return controller; } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java index e5cf3a75..79a34643 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java @@ -15,14 +15,19 @@ */ package org.springframework.cloud.config.server.config; +import javax.servlet.http.HttpServletRequest; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.server.environment.ConsulEnvironmentWatch; import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.cloud.config.server.environment.EnvironmentWatch; import org.springframework.cloud.config.server.environment.MultipleJGitEnvironmentRepository; import org.springframework.cloud.config.server.environment.NativeEnvironmentRepository; import org.springframework.cloud.config.server.environment.SvnKitEnvironmentRepository; +import org.springframework.cloud.config.server.environment.VaultEnvironmentRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -96,4 +101,32 @@ public class EnvironmentRepositoryConfiguration { } } + @Configuration + @Profile("vault") + protected static class VaultConfiguration { + @Bean + public EnvironmentRepository environmentRepository(HttpServletRequest request, EnvironmentWatch watch) { + return new VaultEnvironmentRepository(request, watch); + } + } + + @Configuration + @ConditionalOnProperty(value = "spring.cloud.config.server.consul.watch.enabled") + protected static class ConsulEnvironmentWatchConfiguration { + + @Bean + public EnvironmentWatch environmentWatch() { + return new ConsulEnvironmentWatch(); + } + } + + @Configuration + @ConditionalOnMissingBean(EnvironmentWatch.class) + protected static class DefaultEnvironmentWatch { + + @Bean + public EnvironmentWatch environmentWatch() { + return new EnvironmentWatch.Default(); + } + } } \ No newline at end of file diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceRepositoryConfiguration.java index 7475d7b7..182694d7 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/ResourceRepositoryConfiguration.java @@ -15,6 +15,7 @@ */ package org.springframework.cloud.config.server.config; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.config.server.environment.SearchPathLocator; @@ -33,6 +34,7 @@ import org.springframework.context.annotation.Configuration; public class ResourceRepositoryConfiguration { @Bean + @ConditionalOnBean(SearchPathLocator.class) public ResourceRepository resourceRepository(SearchPathLocator service) { return new GenericResourceRepository(service); } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java index 0bba7992..a9f9a32f 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/encryption/CipherEnvironmentEncryptor.java @@ -57,8 +57,7 @@ public class CipherEnvironmentEncryptor implements EnvironmentEncryptor { } private Environment decrypt(Environment environment, TextEncryptorLocator encryptor) { - Environment result = new Environment(environment.getName(), - environment.getProfiles(), environment.getLabel(), environment.getVersion()); + Environment result = new Environment(environment); for (PropertySource source : environment.getPropertySources()) { Map map = new LinkedHashMap( source.getSource()); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/ConsulEnvironmentWatch.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/ConsulEnvironmentWatch.java new file mode 100644 index 00000000..cafb973b --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/ConsulEnvironmentWatch.java @@ -0,0 +1,126 @@ +package org.springframework.cloud.config.server.environment; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.NotNull; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +/** + * @author Spencer Gibb + */ +@ConfigurationProperties("spring.cloud.config.server.consul.watch") +public class ConsulEnvironmentWatch implements EnvironmentWatch { + + public static final String CONSUL_INDEX = "X-Consul-Index"; + public static final String CONSUL_TOKEN = "X-Consul-Token"; + + private static Log LOG = LogFactory.getLog(ConsulEnvironmentWatch.class); + + private static final String WATCH_URL = "{scheme}://{host}:{port}/v1/kv/{path}?keys&recurse&wait={wait}&index={index}"; + + public static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference>() { + }; + + private RestTemplate restTemplate = new RestTemplate(); + + /** Consul agent scheme. Defaults to 'http'. */ + @NotNull + private String scheme = "http"; + + /** Consul agent hostname. Defaults to 'localhost'. */ + @NotNull + private String host = "localhost"; + + /** Consul agent port. Defaults to '8500'. */ + @NotNull + private int port = 8500; + + /** Path to watch in consul key/value store. */ + @NotNull + private String path; + + /** Consul wait value (eg, 3m or 30s). */ + @NotNull + private String wait = "3m"; + + /** Consul ACL token. */ + private String token; + + @Override + public String watch(String state) { + ArrayList params = new ArrayList<>(); + params.add(this.scheme); + params.add(this.host); + params.add(String.valueOf(this.port)); + params.add(this.path); + params.add(this.wait); + params.add (StringUtils.hasText(state) ? state : ""); + + try { + HttpHeaders headers = new HttpHeaders(); + if (StringUtils.hasText(token)) { + headers.add(CONSUL_TOKEN, token); + } + HttpEntity request = new HttpEntity<>(headers); + ResponseEntity> response = this.restTemplate.exchange(WATCH_URL, + HttpMethod.GET, request, RESPONSE_TYPE, params.toArray()); + + if (response.getStatusCode().is2xxSuccessful()) { + String consulIndex = response.getHeaders().getFirst(CONSUL_INDEX); + return consulIndex; + } + } + catch (HttpStatusCodeException e) { + if (!e.getStatusCode().equals(HttpStatus.NOT_FOUND)) { + LOG.error("Unable to watch consul path " + this.path, e); + return null; + } + } + // TODO: error handling? + + return null; + } + + public void setRestTemplate(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public void setHost(String host) { + this.host = host; + } + + public void setPort(int port) { + this.port = port; + } + + public void setPath(String path) { + this.path = path; + if (this.path.startsWith("/")) { + this.path = this.path.substring(1); + } + } + + public void setWait(String wait) { + this.wait = wait; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentCleaner.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentCleaner.java index 5f834470..9cb144f6 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentCleaner.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentCleaner.java @@ -25,8 +25,7 @@ import org.springframework.cloud.config.environment.PropertySource; public class EnvironmentCleaner { public Environment clean(Environment value, String workingDir, String uri) { - Environment result = new Environment(value.getName(), value.getProfiles(), - value.getLabel(), value.getVersion()); + Environment result = new Environment(value); for (PropertySource source : value.getPropertySources()) { String name = source.getName().replace(workingDir, ""); name = name.replace("applicationConfig: [", ""); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java index 0b61c29f..8f520a9a 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentController.java @@ -277,7 +277,7 @@ public class EnvironmentController { String[] keys = StringUtils.delimitedListToStringArray(stem, "."); for (int i = 0; i < keys.length - 1; i++) { if (current.get(keys[i]) == null) { - LinkedHashMap map = new LinkedHashMap(); + LinkedHashMap map = new LinkedHashMap<>(); current.put(keys[i], map); current = map; } @@ -290,7 +290,7 @@ public class EnvironmentController { } String name = keys[keys.length - 1]; if (current.get(name) == null) { - current.put(name, new ArrayList()); + current.put(name, new ArrayList<>()); } @SuppressWarnings("unchecked") List value = (List) current.get(name); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentEncryptorEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentEncryptorEnvironmentRepository.java index 6af680f9..c653a75f 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentEncryptorEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentEncryptorEnvironmentRepository.java @@ -64,7 +64,7 @@ public class EnvironmentEncryptorEnvironmentRepository implements EnvironmentRep * @param overrides the overrides to set */ public void setOverrides(Map overrides) { - this.overrides = new HashMap(overrides); + this.overrides = new HashMap<>(overrides); for (String key : overrides.keySet()) { if (overrides.get(key).contains("\\{")) { this.overrides.put(key, overrides.get(key).replace("\\{", "{")); diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentWatch.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentWatch.java new file mode 100644 index 00000000..6a624969 --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/EnvironmentWatch.java @@ -0,0 +1,16 @@ +package org.springframework.cloud.config.server.environment; + +/** + * @author Spencer Gibb + */ +public interface EnvironmentWatch { + String watch(String state); + + class Default implements EnvironmentWatch { + + @Override + public String watch(String state) { + return null; + } + } +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java index 2b6b8aca..83f7c494 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/NativeEnvironmentRepository.java @@ -178,7 +178,7 @@ public class NativeEnvironmentRepository protected Environment clean(Environment value) { Environment result = new Environment(value.getName(), value.getProfiles(), - value.getLabel(), this.version); + value.getLabel(), this.version, value.getState()); for (PropertySource source : value.getPropertySources()) { String name = source.getName(); if (this.environment.getPropertySources().contains(name)) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/PassthruEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/PassthruEnvironmentRepository.java index 740f2f37..7f688aa9 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/PassthruEnvironmentRepository.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/PassthruEnvironmentRepository.java @@ -60,7 +60,7 @@ public class PassthruEnvironmentRepository implements EnvironmentRepository { @Override public Environment findOne(String application, String env, String label) { - Environment result = new Environment(application, StringUtils.commaDelimitedListToStringArray(env), label, null); + Environment result = new Environment(application, StringUtils.commaDelimitedListToStringArray(env), label, null, null); for (org.springframework.core.env.PropertySource source : this.environment.getPropertySources()) { String name = source.getName(); if (!this.standardSources.contains(name) && source instanceof MapPropertySource) { diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java new file mode 100644 index 00000000..57b4243f --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepository.java @@ -0,0 +1,247 @@ +package org.springframework.cloud.config.server.environment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.annotation.JsonRawValue; +import com.fasterxml.jackson.databind.JsonNode; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.environment.PropertySource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import static org.springframework.cloud.config.client.ConfigClientProperties.STATE_HEADER; +import static org.springframework.cloud.config.client.ConfigClientProperties.TOKEN_HEADER; + +/** + * @author Spencer Gibb + */ +@ConfigurationProperties("spring.cloud.config.server.vault") +public class VaultEnvironmentRepository implements EnvironmentRepository { + + public static final String VAULT_TOKEN = "X-Vault-Token"; + + /** Vault host. Defaults to 127.0.0.1. */ + @NotEmpty + private String host = "127.0.0.1"; + + /** Vault port. Defaults to 8200. */ + @Range(min = 1, max = 65535) + private int port = 8200; + + /** Vault scheme. Defaults to http. */ + private String scheme = "http"; + + /** Vault backend. Defaults to secret. */ + @NotEmpty + private String backend = "secret"; + + /** The key in vault shared by all applications. Defaults to application. Set to empty to disable. */ + private String defaultKey = "application"; + + /** Vault profile separator. Defaults to comma. */ + @NotEmpty + private String profileSeparator = ","; + + private RestTemplate rest = new RestTemplate(); + + //TODO: move to watchState:String on findOne? + private HttpServletRequest request; + + private EnvironmentWatch watch; + + public VaultEnvironmentRepository(HttpServletRequest request, EnvironmentWatch watch) { + this.request = request; + this.watch = watch; + } + + @Override + public Environment findOne(String application, String profile, String label) { + + String state = request.getHeader(STATE_HEADER); + String newState = this.watch.watch(state); + + String[] profiles = StringUtils.commaDelimitedListToStringArray(profile); + List scrubbedProfiles = scrubProfiles(profiles); + + List keys = findKeys(application, scrubbedProfiles); + + Environment environment = new Environment(application, profiles, label, null, newState); + + for (String key : keys) { + // read raw 'data' key from vault + String data = read(key); + // data is in json format of which, yaml is a superset, so parse + final YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); + yaml.setResources(new ByteArrayResource(data.getBytes())); + Properties properties = yaml.getObject(); + + if (!properties.isEmpty()) { + environment.add(new PropertySource("vault:"+key, properties)); + } + } + + return environment; + } + + private List findKeys(String application, List profiles) { + List keys = new ArrayList<>(); + + if (StringUtils.hasText(this.defaultKey)) { + keys.add(this.defaultKey); + addProfiles(keys, this.defaultKey, profiles); + } + + keys.add(application); + addProfiles(keys, application, profiles); + + Collections.reverse(keys); + return keys; + } + + private List scrubProfiles(String[] profiles) { + List scrubbedProfiles = new ArrayList<>(Arrays.asList(profiles)); + if (scrubbedProfiles.contains("default")) { + scrubbedProfiles.remove("default"); + } + return scrubbedProfiles; + } + + private void addProfiles(List contexts, String baseContext, + List profiles) { + for (String profile : profiles) { + contexts.add(baseContext + this.profileSeparator + profile); + } + } + + String read(String key) { + String url = String.format("%s://%s:%s/v1/{backend}/{key}", this.scheme, + this.host, this.port); + + HttpHeaders headers = new HttpHeaders(); + + String token = request.getHeader(TOKEN_HEADER); + if (!StringUtils.hasLength(token)) { + throw new IllegalArgumentException("Missing required header: "+TOKEN_HEADER); + } + headers.add(VAULT_TOKEN, token); + try { + ResponseEntity response = this.rest.exchange(url, + HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class, + this.backend, key); + + HttpStatus status = response.getStatusCode(); + if (status == HttpStatus.OK) { + return response.getBody().getData(); + } + } + catch (HttpStatusCodeException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + return null; + } + throw e; + } + + return null; + } + + public void setHost(String host) { + this.host = host; + } + + public void setPort(int port) { + this.port = port; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + public void setBackend(String backend) { + this.backend = backend; + } + + public void setDefaultKey(String defaultKey) { + this.defaultKey = defaultKey; + } + + public void setProfileSeparator(String profileSeparator) { + this.profileSeparator = profileSeparator; + } + + static class VaultResponse { + private String auth; + + private Object data; + + @JsonProperty("lease_duration") + private long leaseDuration; + + @JsonProperty("lease_id") + private String leaseId; + + private boolean renewable; + + public VaultResponse() { + } + + public String getAuth() { + return auth; + } + + public void setAuth(String auth) { + this.auth = auth; + } + + @JsonRawValue + public String getData() { + return data == null ? null : data.toString(); + } + + public void setData(JsonNode data) { + this.data = data; + } + + public long getLeaseDuration() { + return leaseDuration; + } + + public void setLeaseDuration(long leaseDuration) { + this.leaseDuration = leaseDuration; + } + + public String getLeaseId() { + return leaseId; + } + + public void setLeaseId(String leaseId) { + this.leaseId = leaseId; + } + + public boolean isRenewable() { + return renewable; + } + + public void setRenewable(boolean renewable) { + this.renewable = renewable; + } + } +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java new file mode 100644 index 00000000..d213f1b7 --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/VaultEnvironmentRepositoryTests.java @@ -0,0 +1,14 @@ +package org.springframework.cloud.config.server.environment; + +import org.junit.Test; + +/** + * @author Spencer Gibb + */ +public class VaultEnvironmentRepositoryTests { + + @Test + public void testFindOne() { + //TODO: implement testFindOne + } +} diff --git a/spring-cloud-starter-config/pom.xml b/spring-cloud-starter-config/pom.xml index 2659c727..6dde973b 100644 --- a/spring-cloud-starter-config/pom.xml +++ b/spring-cloud-starter-config/pom.xml @@ -5,10 +5,10 @@ org.springframework.cloud spring-cloud-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT spring-cloud-starter-config - 1.1.3.BUILD-SNAPSHOT + 1.1.2.vault.BUILD-SNAPSHOT spring-cloud-starter-config Spring Cloud Starter http://projects.spring.io/spring-cloud