• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2017 Google Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 //
15 ////////////////////////////////////////////////////////////////////////////////
16 
17 package com.google.crypto.tink.util;
18 
19 import com.google.api.client.http.GenericUrl;
20 import com.google.api.client.http.HttpHeaders;
21 import com.google.api.client.http.HttpRequest;
22 import com.google.api.client.http.HttpResponse;
23 import com.google.api.client.http.HttpStatusCodes;
24 import com.google.api.client.http.HttpTransport;
25 import com.google.api.client.http.javanet.NetHttpTransport;
26 import com.google.errorprone.annotations.CanIgnoreReturnValue;
27 import java.io.BufferedReader;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.io.InputStreamReader;
31 import java.io.Reader;
32 import java.net.MalformedURLException;
33 import java.net.URL;
34 import java.nio.charset.Charset;
35 import java.util.Locale;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.Executors;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 import javax.annotation.concurrent.GuardedBy;
41 
42 /**
43  * Thread-safe downloader.
44  *
45  * <p>This class can be used to download keys from a remote HTTPS server.
46  *
47  * <h3>Usage</h3>
48  *
49  * <p>Use {@link KeysDownloader.Builder} to construct an instance and keep it as a singleton in a
50  * static final variable across requests.
51  *
52  * <p>When initializing your server, we also recommend that you call {@link #refreshInBackground()}
53  * to proactively fetch the data.
54  *
55  * @since 1.1.0
56  * @deprecated This is not supported by Tink, as it incurs a dependency on <code>
57  *     com.google.api.client.http</code>. If you need this, please copy it into your codebase.
58  */
59 @Deprecated
60 public class KeysDownloader {
61   private static final Charset UTF_8 = Charset.forName("UTF-8");
62 
63   /** Default HTTP transport used by this class. */
64   private static final NetHttpTransport DEFAULT_HTTP_TRANSPORT =
65       new NetHttpTransport.Builder().build();
66 
67   private static final Executor DEFAULT_BACKGROUND_EXECUTOR = Executors.newCachedThreadPool();
68 
69   /** Pattern for the max-age header element of Cache-Control. */
70   private static final Pattern MAX_AGE_PATTERN = Pattern.compile("\\s*max-age\\s*=\\s*(\\d+)\\s*");
71 
72   private final Executor backgroundExecutor;
73   private final HttpTransport httpTransport;
74   private final Object fetchDataLock;
75   private final Object instanceStateLock;
76   private final String url;
77 
78   @GuardedBy("instanceStateLock")
79   private Runnable pendingRefreshRunnable;
80 
81   @GuardedBy("instanceStateLock")
82   private String cachedData;
83 
84   @GuardedBy("instanceStateLock")
85   private long cachedTimeInMillis;
86 
87   @GuardedBy("instanceStateLock")
88   private long cacheExpirationDurationInMillis;
89 
KeysDownloader(Executor backgroundExecutor, HttpTransport httpTransport, String url)90   public KeysDownloader(Executor backgroundExecutor, HttpTransport httpTransport, String url) {
91     validate(url);
92     this.backgroundExecutor = backgroundExecutor;
93     this.httpTransport = httpTransport;
94     this.instanceStateLock = new Object();
95     this.fetchDataLock = new Object();
96     this.url = url;
97     this.cachedTimeInMillis = Long.MIN_VALUE;
98     this.cacheExpirationDurationInMillis = 0;
99   }
100 
101   /**
102    * Returns a string containing a JSON with the Google public signing keys.
103    *
104    * <p>Meant to be called by {@link PaymentMethodTokenRecipient}.
105    */
download()106   public String download() throws IOException {
107     synchronized (instanceStateLock) {
108       // Checking and using the cache if required.
109       if (hasNonExpiredDataCached()) {
110         // Proactively triggering a refresh if we are close to the cache expiration.
111         if (shouldProactivelyRefreshDataInBackground()) {
112           refreshInBackground();
113         }
114         return cachedData;
115       }
116     }
117 
118     // Acquiring the fetch lock so we don't have multiple threads trying to fetch from the
119     // server at the same time.
120     synchronized (fetchDataLock) {
121       // It is possible that some other thread performed the fetch already and we don't need
122       // to fetch anymore, so double checking a fetch is still required.
123       synchronized (instanceStateLock) {
124         if (hasNonExpiredDataCached()) {
125           return cachedData;
126         }
127       }
128       // No other thread fetched, so it is up to this thread to fetch.
129       return fetchAndCacheData();
130     }
131   }
132 
getHttpTransport()133   public HttpTransport getHttpTransport() {
134     return httpTransport;
135   }
136 
getUrl()137   public String getUrl() {
138     return url;
139   }
140 
141   @GuardedBy("instanceStateLock")
hasNonExpiredDataCached()142   private boolean hasNonExpiredDataCached() {
143     long currentTimeInMillis = getCurrentTimeInMillis();
144     boolean cachedInFuture = cachedTimeInMillis > currentTimeInMillis;
145     boolean cacheExpired =
146         cachedTimeInMillis + cacheExpirationDurationInMillis <= currentTimeInMillis;
147     return !cacheExpired && !cachedInFuture;
148   }
149 
150   @GuardedBy("instanceStateLock")
shouldProactivelyRefreshDataInBackground()151   private boolean shouldProactivelyRefreshDataInBackground() {
152     // At half expiration duration, we should try to refresh.
153     return cachedTimeInMillis + (cacheExpirationDurationInMillis / 2) <= getCurrentTimeInMillis();
154   }
155 
156   /**
157    * Returns the current time in milliseconds since epoch.
158    *
159    * <p>Visible so tests can override it in subclasses.
160    */
getCurrentTimeInMillis()161   long getCurrentTimeInMillis() {
162     return System.currentTimeMillis();
163   }
164 
165   @GuardedBy("fetchDataLock")
166   @CanIgnoreReturnValue
fetchAndCacheData()167   private String fetchAndCacheData() throws IOException {
168     long currentTimeInMillis = getCurrentTimeInMillis();
169     HttpRequest httpRequest =
170         httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(url));
171     HttpResponse httpResponse = httpRequest.execute();
172     if (httpResponse.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
173       throw new IOException("Unexpected status code = " + httpResponse.getStatusCode());
174     }
175     String data;
176     InputStream contentStream = httpResponse.getContent();
177     try {
178       InputStreamReader reader = new InputStreamReader(contentStream, UTF_8);
179       data = readerToString(reader);
180     } finally {
181       contentStream.close();
182     }
183     synchronized (instanceStateLock) {
184       this.cachedTimeInMillis = currentTimeInMillis;
185       this.cacheExpirationDurationInMillis =
186           getExpirationDurationInSeconds(httpResponse.getHeaders()) * 1000;
187       this.cachedData = data;
188     }
189     return data;
190   }
191 
192   /** Reads the contents of a {@link Reader} into a {@link String}. */
readerToString(Reader reader)193   private static String readerToString(Reader reader) throws IOException {
194     reader = new BufferedReader(reader);
195     StringBuilder stringBuilder = new StringBuilder();
196     int c;
197     while ((c = reader.read()) != -1) {
198       stringBuilder.append((char) c);
199     }
200     return stringBuilder.toString();
201   }
202 
203   /**
204    * Gets the cache TimeInMillis in seconds. "max-age" in "Cache-Control" header and "Age" header
205    * are considered.
206    *
207    * @param httpHeaders the http header of the response
208    * @return the cache TimeInMillis in seconds or zero if the response should not be cached
209    */
getExpirationDurationInSeconds(HttpHeaders httpHeaders)210   long getExpirationDurationInSeconds(HttpHeaders httpHeaders) {
211     long expirationDurationInSeconds = 0;
212     if (httpHeaders.getCacheControl() != null) {
213       for (String arg : httpHeaders.getCacheControl().split(",")) {
214         Matcher m = MAX_AGE_PATTERN.matcher(arg);
215         if (m.matches()) {
216           expirationDurationInSeconds = Long.valueOf(m.group(1));
217           break;
218         }
219       }
220     }
221     if (httpHeaders.getAge() != null) {
222       expirationDurationInSeconds -= httpHeaders.getAge();
223     }
224     return Math.max(0, expirationDurationInSeconds);
225   }
226 
227   /** Fetches keys in the background. */
refreshInBackground()228   public void refreshInBackground() {
229     Runnable refreshRunnable = newRefreshRunnable();
230     synchronized (instanceStateLock) {
231       if (pendingRefreshRunnable != null) {
232         return;
233       }
234       pendingRefreshRunnable = refreshRunnable;
235     }
236     try {
237       backgroundExecutor.execute(refreshRunnable);
238     } catch (Throwable e) {
239       synchronized (instanceStateLock) {
240         // Clearing if we were still the pending runnable.
241         if (pendingRefreshRunnable == refreshRunnable) {
242           pendingRefreshRunnable = null;
243         }
244       }
245       throw e;
246     }
247   }
248 
newRefreshRunnable()249   private Runnable newRefreshRunnable() {
250     return new Runnable() {
251       @Override
252       public void run() {
253         synchronized (fetchDataLock) {
254           try {
255             fetchAndCacheData();
256           } catch (IOException e) {
257             // Failed to fetch the data. Ok as this was just from the background.
258           } finally {
259             synchronized (instanceStateLock) {
260               // Clearing if we were still the pending runnable.
261               if (pendingRefreshRunnable == this) {
262                 pendingRefreshRunnable = null;
263               }
264             }
265           }
266         }
267       }
268     };
269   }
270 
271   private static void validate(String url) {
272     try {
273       URL tmp = new URL(url);
274       if (!tmp.getProtocol().toLowerCase(Locale.US).equals("https")) {
275         throw new IllegalArgumentException("url must point to a HTTPS server");
276       }
277     } catch (MalformedURLException ex) {
278       throw new IllegalArgumentException(ex);
279     }
280   }
281 
282   /** Builder for {@link KeysDownloader}. */
283   public static class Builder {
284     private HttpTransport httpTransport = DEFAULT_HTTP_TRANSPORT;
285     private Executor executor = DEFAULT_BACKGROUND_EXECUTOR;
286     private String url;
287 
288     /** Sets the url which must point to a HTTPS server. */
289     @CanIgnoreReturnValue
290     public Builder setUrl(String val) {
291       this.url = val;
292       return this;
293     }
294 
295     /** Sets the background executor. */
296     @CanIgnoreReturnValue
297     public Builder setExecutor(Executor val) {
298       this.executor = val;
299       return this;
300     }
301 
302     /**
303      * Sets the HTTP transport.
304      *
305      * <p>You generally should not need to set a custom transport as the default transport {@link
306      * KeysDownloader#DEFAULT_HTTP_TRANSPORT} should be suited for most use cases.
307      */
308     @CanIgnoreReturnValue
309     public Builder setHttpTransport(HttpTransport httpTransport) {
310       this.httpTransport = httpTransport;
311       return this;
312     }
313 
314     public KeysDownloader build() {
315       if (url == null) {
316         throw new IllegalArgumentException("must provide a url with {#setUrl}");
317       }
318       return new KeysDownloader(executor, httpTransport, url);
319     }
320   }
321 }
322