1 package com.atlassian.asap.core.keys.publickey;
2
3 import com.atlassian.asap.api.exception.CannotRetrieveKeyException;
4 import com.atlassian.asap.core.exception.PublicKeyNotFoundException;
5 import com.atlassian.asap.core.exception.PublicKeyRetrievalException;
6 import com.atlassian.asap.core.keys.KeyProvider;
7 import com.atlassian.asap.core.keys.PemReader;
8 import com.atlassian.asap.core.validator.ValidatedKeyId;
9 import org.apache.commons.lang3.StringUtils;
10 import org.apache.http.HttpEntity;
11 import org.apache.http.HttpHeaders;
12 import org.apache.http.HttpResponse;
13 import org.apache.http.HttpStatus;
14 import org.apache.http.client.HttpClient;
15 import org.apache.http.client.config.RequestConfig;
16 import org.apache.http.client.methods.HttpGet;
17 import org.apache.http.client.utils.HttpClientUtils;
18 import org.apache.http.entity.ContentType;
19 import org.apache.http.impl.client.DefaultRedirectStrategy;
20 import org.apache.http.impl.client.HttpClientBuilder;
21 import org.apache.http.impl.client.cache.CacheConfig;
22 import org.apache.http.impl.client.cache.CachingHttpClients;
23 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26
27 import java.io.IOException;
28 import java.io.InputStreamReader;
29 import java.net.URI;
30 import java.nio.charset.Charset;
31 import java.nio.charset.StandardCharsets;
32 import java.security.PublicKey;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.concurrent.TimeUnit;
36
37 import static com.google.common.base.Preconditions.checkArgument;
38 import static java.lang.String.format;
39
40
41
42
43 public class HttpPublicKeyProvider implements KeyProvider<PublicKey> {
44
45
46
47 static final int DEFAULT_MAX_CONNECTIONS = 20;
48 static final String PEM_MIME_TYPE = "application/x-pem-file";
49 static final String ACCEPT_HEADER_VALUE = PEM_MIME_TYPE;
50
51 private static final Logger logger = LoggerFactory.getLogger(HttpPublicKeyProvider.class);
52
53 private final HttpClient httpClient;
54 private final PemReader pemReader;
55 private final URI baseUrl;
56
57
58
59
60
61
62
63
64 public HttpPublicKeyProvider(URI baseUrl, HttpClient httpClient, PemReader pemReader) {
65 this(baseUrl, httpClient, pemReader, false);
66 }
67
68 HttpPublicKeyProvider(URI baseUrl, HttpClient httpClient, PemReader pemReader, boolean allowInsecureConnections) {
69 Objects.requireNonNull(baseUrl, "Base URL cannot be null");
70 checkArgument(baseUrl.isAbsolute(), "Base URL must be absolute");
71 checkArgument((allowInsecureConnections && "http".equals(baseUrl.getScheme()))
72 || "https".equals(baseUrl.getScheme()), "Invalid base URL scheme");
73 checkArgument(StringUtils.endsWith(baseUrl.toString(), "/"), "Base URL does not end with trailing slash: " + baseUrl);
74
75 this.baseUrl = baseUrl;
76 this.httpClient = Objects.requireNonNull(httpClient);
77 this.pemReader = Objects.requireNonNull(pemReader);
78 }
79
80 @Override
81 public PublicKey getKey(ValidatedKeyId validatedKeyId) throws CannotRetrieveKeyException {
82 URI keyUrl = baseUrl.resolve(validatedKeyId.getKeyId());
83 logger.debug("Fetching public key {}", keyUrl);
84
85 HttpResponse response = null;
86 try {
87 response = httpGetKey(keyUrl);
88 int statusCode = response.getStatusLine().getStatusCode();
89 switch (statusCode) {
90 case HttpStatus.SC_OK:
91 HttpEntity entity = response.getEntity();
92 if (entity != null) {
93 return readEntityAsPublicKey(validatedKeyId, keyUrl, entity);
94 } else {
95 logger.info("Unexpected empty HTTP response when trying to retrieve public key URL {}", keyUrl);
96 throw new PublicKeyRetrievalException("Unexpected empty response getting public key", validatedKeyId, keyUrl);
97 }
98 case HttpStatus.SC_FORBIDDEN:
99 case HttpStatus.SC_NOT_FOUND:
100 final String statusReason = response.getStatusLine().getReasonPhrase();
101
102 logger.info("Public key URL {} returned {} {}", keyUrl, statusCode, statusReason);
103 throw new PublicKeyNotFoundException(format("Encountered %s %s for public key", statusCode, statusReason), validatedKeyId, keyUrl);
104 default:
105 logger.error("Unexpected HTTP status code {} when trying to retrieve public key URL {}", statusCode, keyUrl);
106 throw new PublicKeyRetrievalException("Unexpected status code " + statusCode + " getting public key", validatedKeyId, keyUrl);
107 }
108 } catch (IOException e) {
109 logger.warn("A problem occurred when trying to retrieve public key from URL {}", keyUrl, e);
110 throw new PublicKeyRetrievalException("Error getting HTTPS public key - " + e.getMessage(), e, validatedKeyId, keyUrl);
111 } finally {
112 HttpClientUtils.closeQuietly(response);
113 }
114 }
115
116 private PublicKey readEntityAsPublicKey(ValidatedKeyId validatedKeyId, URI keyUrl, HttpEntity entity) throws IOException, CannotRetrieveKeyException {
117 try (InputStreamReader reader = new InputStreamReader(entity.getContent(), getCharset(entity))) {
118 final String mimeType = ContentType.get(entity).getMimeType();
119 if (Objects.equals(PEM_MIME_TYPE, mimeType)) {
120 return pemReader.readPublicKey(reader);
121 } else {
122 throw new PublicKeyRetrievalException(
123 format("Unexpected public key MIME type %s, expected %s", mimeType, PEM_MIME_TYPE), validatedKeyId, keyUrl);
124 }
125 }
126 }
127
128 private Charset getCharset(HttpEntity entity) {
129 return Optional.ofNullable(ContentType.getOrDefault(entity).getCharset()).orElse(StandardCharsets.US_ASCII);
130 }
131
132 private HttpResponse httpGetKey(URI keyUrl) throws IOException {
133 HttpGet httpGet = new HttpGet(keyUrl);
134 httpGet.setHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER_VALUE);
135 return httpClient.execute(httpGet);
136 }
137
138
139
140
141
142
143
144 public static HttpClient defaultHttpClient() {
145 return defaultHttpClientBuilder().build();
146 }
147
148
149
150
151
152
153
154
155 public static HttpClientBuilder defaultHttpClientBuilder() {
156 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
157 connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS);
158 connectionManager.setMaxTotal(DEFAULT_MAX_CONNECTIONS);
159
160 RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
161 requestConfigBuilder.setConnectionRequestTimeout(((int) TimeUnit.SECONDS.toMillis(5)));
162 requestConfigBuilder.setConnectTimeout(((int) TimeUnit.SECONDS.toMillis(5)));
163 requestConfigBuilder.setSocketTimeout((int) TimeUnit.SECONDS.toMillis(10));
164
165 CacheConfig cacheConfig = CacheConfig.custom()
166 .setMaxCacheEntries(128)
167 .setMaxObjectSize(2048)
168 .setHeuristicCachingEnabled(false)
169 .setSharedCache(false)
170 .setAsynchronousWorkersMax(2)
171 .build();
172
173 return CachingHttpClients.custom()
174 .setCacheConfig(cacheConfig)
175 .setDefaultRequestConfig(requestConfigBuilder.build())
176 .setConnectionManager(connectionManager)
177 .useSystemProperties()
178 .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
179
180 .setServiceUnavailableRetryStrategy(new S3ServiceUnavailableRetryStrategy(2, 100));
181 }
182
183 @Override
184 public String toString() {
185 return this.getClass().getSimpleName() + "{baseUrl=" + baseUrl + '}';
186 }
187 }