View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.kernel.security;
23  
24  import static org.opencastproject.kernel.rest.CurrentJobFilter.CURRENT_JOB_HEADER;
25  import static org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint.DIGEST_AUTH;
26  import static org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER;
27  import static org.opencastproject.util.data.Collections.set;
28  
29  import org.opencastproject.security.api.Organization;
30  import org.opencastproject.security.api.OrganizationDirectoryService;
31  import org.opencastproject.security.api.SecurityConstants;
32  import org.opencastproject.security.api.SecurityService;
33  import org.opencastproject.security.api.TrustedHttpClient;
34  import org.opencastproject.security.api.TrustedHttpClientException;
35  import org.opencastproject.security.api.User;
36  import org.opencastproject.security.urlsigning.exception.UrlSigningException;
37  import org.opencastproject.security.urlsigning.service.UrlSigningService;
38  import org.opencastproject.security.util.HttpResponseWrapper;
39  import org.opencastproject.serviceregistry.api.HostRegistration;
40  import org.opencastproject.serviceregistry.api.ServiceRegistry;
41  import org.opencastproject.serviceregistry.api.ServiceRegistryException;
42  import org.opencastproject.urlsigning.utils.ResourceRequestUtil;
43  
44  import org.apache.commons.lang3.StringUtils;
45  import org.apache.commons.lang3.math.NumberUtils;
46  import org.apache.http.Header;
47  import org.apache.http.HeaderElement;
48  import org.apache.http.HttpResponse;
49  import org.apache.http.auth.AuthScope;
50  import org.apache.http.auth.UsernamePasswordCredentials;
51  import org.apache.http.client.ClientProtocolException;
52  import org.apache.http.client.CredentialsProvider;
53  import org.apache.http.client.config.AuthSchemes;
54  import org.apache.http.client.config.RequestConfig;
55  import org.apache.http.client.methods.HttpGet;
56  import org.apache.http.client.methods.HttpHead;
57  import org.apache.http.client.methods.HttpRequestBase;
58  import org.apache.http.client.methods.HttpUriRequest;
59  import org.apache.http.impl.auth.DigestScheme;
60  import org.apache.http.impl.client.BasicCredentialsProvider;
61  import org.apache.http.impl.client.CloseableHttpClient;
62  import org.apache.http.impl.client.HttpClientBuilder;
63  import org.osgi.service.component.ComponentContext;
64  import org.osgi.service.component.annotations.Activate;
65  import org.osgi.service.component.annotations.Component;
66  import org.osgi.service.component.annotations.Deactivate;
67  import org.osgi.service.component.annotations.Reference;
68  import org.osgi.service.component.annotations.ReferenceCardinality;
69  import org.osgi.service.component.annotations.ReferencePolicy;
70  import org.slf4j.Logger;
71  import org.slf4j.LoggerFactory;
72  
73  import java.io.IOException;
74  import java.net.URI;
75  import java.util.Collection;
76  import java.util.Map;
77  import java.util.Random;
78  import java.util.Set;
79  import java.util.concurrent.ConcurrentHashMap;
80  import java.util.stream.Collectors;
81  
82  /**
83   * An http client that executes secure (though not necessarily encrypted) http requests.
84   */
85  @Component(
86      property = {
87          "service.description=Provides Trusted Http Clients (for use with digest authentication)"
88      },
89      immediate = true,
90      service = { TrustedHttpClient.class }
91  )
92  public class TrustedHttpClientImpl implements TrustedHttpClient {
93    /** Header name used to request a new nonce from a server a request is sent to. */
94    public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
95  
96    /** The logger */
97    private static final Logger logger = LoggerFactory.getLogger(TrustedHttpClientImpl.class);
98  
99    /** The configuration property specifying the digest authentication user */
100   public static final String DIGEST_AUTH_USER_KEY = "org.opencastproject.security.digest.user";
101 
102   /** The configuration property specifying the digest authentication password */
103   public static final String DIGEST_AUTH_PASS_KEY = "org.opencastproject.security.digest.pass";
104 
105   /** The configuration property specifying the number of times to retry after the nonce timesouts on a request. */
106   public static final String NONCE_TIMEOUT_RETRY_KEY = "org.opencastproject.security.digest.nonce.retries";
107 
108   /** The configuration property specifying the duration a signed url will remain valid for. */
109   protected static final String INTERNAL_URL_SIGNING_DURATION_KEY =
110       "org.opencastproject.security.internal.url.signing.duration";
111 
112   /**
113    * The configuration property specifying the minimum amount of time in seconds wait before retrying a request after a
114    * nonce timeout.
115    */
116   public static final String NONCE_TIMEOUT_RETRY_BASE_TIME_KEY = "org.opencastproject.security.digest.nonce.base.time";
117 
118   /**
119    * The configuration property specifying the maximum for a random amount of time in seconds above the base time to
120    * wait.
121    */
122   public static final String NONCE_TIMEOUT_RETRY_MAXIMUM_VARIABLE_TIME_KEY =
123       "org.opencastproject.security.digest.nonce.variable.time";
124 
125   /** The default time until a connection attempt fails */
126   public static final int DEFAULT_CONNECTION_TIMEOUT = 60 * 1000;
127 
128   /** The default time between packets that causes a connection to fail */
129   public static final int DEFAULT_SOCKET_TIMEOUT = 300 * 1000;
130 
131   /** The default number of times to attempt a request after it has failed due to a nonce expiring. */
132   public static final int DEFAULT_NONCE_TIMEOUT_RETRIES = 12;
133 
134   /** The number of milliseconds in a single second. */
135   private static final int MILLISECONDS_IN_SECONDS = 1000;
136 
137   /** The default amount of time to wait after a nonce timeout. */
138   public static final int DEFAULT_RETRY_BASE_TIME = 300;
139 
140   /** Default maximum amount of time in a random range between 0 and this value to add to the base time. */
141   public static final int DEFAULT_RETRY_MAXIMUM_VARIABLE_TIME = 300;
142 
143   /**
144    * The default time before a piece of signed content expires. 1 Minute. These are internal calls to another server, if
145    * we can't make the request in under a minute something has gone horribly wrong.
146    */
147   protected static final long DEFAULT_URL_SIGNING_EXPIRES_DURATION = 60;
148 
149   /** The configured username to send as part of the digest authenticated request */
150   protected String user = null;
151 
152   /** The configured password to send as part of the digest authenticated request */
153   protected String pass = null;
154 
155   /** The number of times to retry a request after a nonce timeout. */
156   private int nonceTimeoutRetries = DEFAULT_NONCE_TIMEOUT_RETRIES;
157 
158   /**
159    * The map of open responses to their http clients, which need to be closed after we are finished with the response
160    */
161   protected Map<HttpResponse, CloseableHttpClient> responseMap = new ConcurrentHashMap<>();
162 
163   /** Used to add a random amount of time up to retryMaximumVariableTime to retry a request after a nonce timeout. */
164   private final Random generator = new Random();
165 
166   /** The amount of time in seconds to wait until trying the request again. */
167   private int retryBaseDelay = 300;
168 
169   /** The maximum amount of time in seconds to wait in addition to the RETRY_BASE_DELAY. */
170   private int retryMaximumVariableTime = 300;
171 
172   /** The duration a signed url will remain valid for. */
173   private long signedUrlExpiresDuration = DEFAULT_URL_SIGNING_EXPIRES_DURATION;
174 
175   /** The service registry */
176   private ServiceRegistry serviceRegistry = null;
177 
178   /** The security service */
179   protected SecurityService securityService = null;
180 
181   /** The organization directory service */
182   protected OrganizationDirectoryService organizationDirectoryService = null;
183 
184   /** The url signing service */
185   protected UrlSigningService urlSigningService = null;
186 
187   /** A regularly emptying cache of hosts in the cluster */
188   private HostCache hosts = null;
189 
190 
191   @Activate
192   public void activate(ComponentContext cc) {
193     logger.debug("activate");
194     user = cc.getBundleContext().getProperty(DIGEST_AUTH_USER_KEY);
195     pass = cc.getBundleContext().getProperty(DIGEST_AUTH_PASS_KEY);
196     if (user == null || pass == null) {
197       throw new IllegalStateException("trusted communication is not properly configured");
198     }
199 
200     getRetryNumber(cc);
201     getRetryBaseTime(cc);
202     getRetryMaximumVariableTime(cc);
203 
204     final Long expiration = NumberUtils.createLong(StringUtils.trimToNull(
205         cc.getBundleContext().getProperty(INTERNAL_URL_SIGNING_DURATION_KEY)));
206     if (expiration != null) {
207       signedUrlExpiresDuration = expiration;
208     } else {
209       signedUrlExpiresDuration = DEFAULT_URL_SIGNING_EXPIRES_DURATION;
210     }
211     logger.debug("Expire signed URLs in {} seconds.", signedUrlExpiresDuration);
212   }
213 
214   private HostCache getHostCache() {
215     if (null == hosts) {
216       hosts = new HostCache(60000, this.organizationDirectoryService, this.serviceRegistry);
217     }
218     return hosts;
219   }
220 
221   /**
222    * Sets the service registry.
223    *
224    * @param serviceRegistry
225    *         the serviceRegistry to set
226    */
227   @Reference(
228       cardinality = ReferenceCardinality.OPTIONAL,
229       policy =  ReferencePolicy.DYNAMIC,
230       unbind = "unsetServiceRegistry")
231   public void setServiceRegistry(ServiceRegistry serviceRegistry) {
232     this.serviceRegistry = serviceRegistry;
233   }
234 
235   /**
236    * Unsets the service registry.
237    *
238    * @param serviceRegistry
239    *         the serviceRegistry to unset (unused, but needed for OSGI)
240    */
241   public void unsetServiceRegistry(ServiceRegistry serviceRegistry) {
242     if (this.serviceRegistry == serviceRegistry) {
243       this.serviceRegistry = null;
244     }
245   }
246 
247   /**
248    * Sets the security service.
249    *
250    * @param securityService
251    *         the security service
252    */
253   @Reference
254   public void setSecurityService(SecurityService securityService) {
255     this.securityService = securityService;
256   }
257 
258   /**
259    * Sets the organization directory service.
260    *
261    * @param organizationDirectoryService
262    *         the organization directory service
263    */
264   @Reference
265   public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
266     this.organizationDirectoryService = organizationDirectoryService;
267   }
268 
269   /**
270    * Sets the url signing service.
271    *
272    * @param urlSigningService
273    *        The signing service to sign urls with.
274    */
275   @Reference
276   public void setUrlSigningService(UrlSigningService urlSigningService) {
277     this.urlSigningService = urlSigningService;
278   }
279 
280   /**
281    * Extracts the number of times to retry a request after a nonce timeout.
282    *
283    * @param cc
284    *         The ComponentContent to extract this property from.
285    */
286   private void getRetryNumber(ComponentContext cc) {
287     nonceTimeoutRetries = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_KEY, DEFAULT_NONCE_TIMEOUT_RETRIES);
288   }
289 
290   /**
291    * Extracts the minimum amount of time in seconds to wait if there is a nonce timeout before retrying.
292    *
293    * @param cc
294    *         The ComponentContent to extract this property from.
295    */
296   private void getRetryBaseTime(ComponentContext cc) {
297     retryBaseDelay = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_BASE_TIME_KEY, DEFAULT_RETRY_BASE_TIME);
298   }
299 
300   /**
301    * Extracts the maximum amount of time in seconds that is added to the base time after a nonce timeout.
302    *
303    * @param cc
304    *         The ComponentContent to extract this property from.
305    */
306   private void getRetryMaximumVariableTime(ComponentContext cc) {
307     retryMaximumVariableTime = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_MAXIMUM_VARIABLE_TIME_KEY,
308                                                           DEFAULT_RETRY_MAXIMUM_VARIABLE_TIME);
309   }
310 
311   /**
312    * Gets a property from the ComponentContext that is the base type int.
313    *
314    * @param cc
315    *         The ComponentContext to get the property from.
316    * @param key
317    *         The key to search the properties for to get the value back.
318    * @param defaultValue
319    *         The default value to set if the property is malformed or non-existant.
320    * @return The int property either as the value from the properties collection or the default value.
321    */
322   private int getIntFromComponentContext(ComponentContext cc, String key, int defaultValue) {
323     int result;
324     try {
325       String stringValue = cc.getBundleContext().getProperty(key);
326       result = Integer.parseInt(StringUtils.trimToNull(stringValue));
327     } catch (Exception e) {
328       if (cc != null && cc.getBundleContext() != null && cc.getBundleContext().getProperty(key) != null) {
329         logger.info("Unable to get property with key " + key + " with value " + cc.getBundleContext().getProperty(key)
330                             + " so using default of " + defaultValue + " because of " + e.getMessage());
331       } else {
332         logger.info("Unable to get property with key " + key + " so using default of " + defaultValue + " because of "
333                             + e.getMessage());
334       }
335       result = defaultValue;
336     }
337 
338     return result;
339   }
340 
341   @Deactivate
342   public void deactivate() {
343     logger.debug("deactivate");
344   }
345 
346   public TrustedHttpClientImpl() {
347 
348   }
349 
350   public TrustedHttpClientImpl(String user, String pass) {
351     this.user = user;
352     this.pass = pass;
353   }
354 
355   /** Creates a new HttpClientBuilder to use for making requests. */
356   public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socketTimeout) {
357     RequestConfig config = RequestConfig.custom()
358         .setConnectionRequestTimeout(connectionTimeout)
359         .setSocketTimeout(socketTimeout).build();
360     return HttpClientBuilder.create().setDefaultRequestConfig(config);
361   }
362 
363   /**
364    * {@inheritDoc}
365    *
366    * @see org.opencastproject.security.api.TrustedHttpClient#execute(org.apache.http.client.methods.HttpUriRequest)
367    */
368   @Override
369   public HttpResponse execute(HttpUriRequest httpUriRequest) throws TrustedHttpClientException {
370     return execute(httpUriRequest, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_SOCKET_TIMEOUT);
371   }
372 
373   @Override
374   public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout, int socketTimeout)
375           throws TrustedHttpClientException {
376     if (serviceRegistry != null && serviceRegistry.getCurrentJob() != null) {
377       httpUriRequest.setHeader(CURRENT_JOB_HEADER, Long.toString(serviceRegistry.getCurrentJob().getId()));
378     }
379 
380     boolean enableDigest = getHostCache().contains(httpUriRequest.getURI().getHost());
381     logger.debug("Digest auth enabled for this request: " + enableDigest);
382     if (enableDigest) {
383       // Add the request header to elicit a digest auth response
384       httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH);
385     }
386 
387     // If a security service has been set, use it to pass the current security context on
388     logger.debug("Adding security context to request");
389     final Organization organization = securityService.getOrganization();
390     if (organization != null) {
391       httpUriRequest.setHeader(SecurityConstants.ORGANIZATION_HEADER, organization.getId());
392       final User currentUser = securityService.getUser();
393       if (enableDigest && currentUser != null) {
394         httpUriRequest.setHeader(SecurityConstants.USER_HEADER, currentUser.getUsername());
395       }
396     }
397 
398     final HttpClientBuilder clientBuilder = makeHttpClientBuilder(connectionTimeout, socketTimeout);
399     if ("GET".equalsIgnoreCase(httpUriRequest.getMethod()) || "HEAD".equalsIgnoreCase(httpUriRequest.getMethod())) {
400       final CloseableHttpClient httpClient;
401       if (enableDigest) {
402         // Set the user/pass
403         CredentialsProvider provider = new BasicCredentialsProvider();
404         provider.setCredentials(
405             new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.DIGEST),
406             new UsernamePasswordCredentials(user, pass));
407         httpClient = clientBuilder.setDefaultCredentialsProvider(provider).build();
408       } else {
409         httpClient = clientBuilder.build();
410       }
411       // Run the request (the http client handles the multiple back-and-forth requests)
412       try {
413         httpUriRequest = getSignedUrl(httpUriRequest);
414         HttpResponse response = new HttpResponseWrapper(httpClient.execute(httpUriRequest));
415         responseMap.put(response, httpClient);
416         return response;
417       } catch (IOException e) {
418         try {
419           httpClient.close();
420         } catch (IOException ioException) {
421           throw new TrustedHttpClientException(e);
422         }
423         throw new TrustedHttpClientException(e);
424       }
425     } else if (enableDigest) {
426       final CloseableHttpClient httpClient = clientBuilder.build();
427       // HttpClient doesn't handle the request dynamics for other verbs (especially when sending a streamed multipart
428       // request), so we need to handle the details of the digest auth back-and-forth manually
429       manuallyHandleDigestAuthentication(httpUriRequest, httpClient);
430       HttpResponse response = null;
431       try {
432         response = new HttpResponseWrapper(httpClient.execute(httpUriRequest));
433         if (nonceTimeoutRetries > 0 && hadNonceTimeoutResponse(response)) {
434           httpClient.close();
435           response = retryAuthAndRequestAfterNonceTimeout(httpUriRequest, response);
436         }
437         responseMap.put(response, httpClient);
438         return response;
439       } catch (Exception e) {
440         // if we have a response, remove it from the map
441         if (response != null) {
442           responseMap.remove(response);
443         }
444         // close the http connection(s)
445         try {
446           httpClient.close();
447         } catch (IOException ioException) {
448           throw new TrustedHttpClientException(e);
449         }
450         throw new TrustedHttpClientException(e);
451       }
452     } else {
453       final CloseableHttpClient httpClient = clientBuilder.build();
454       HttpResponse response = null;
455       try {
456         response = httpClient.execute(httpUriRequest);
457         return response;
458       } catch (Exception e) {
459         // close the http connection(s)
460         try {
461           httpClient.close();
462         } catch (IOException ioException) {
463           throw new TrustedHttpClientException(e);
464         }
465         throw new TrustedHttpClientException(e);
466       }
467     }
468   }
469 
470   /**
471    * If the request is a GET, sign the URL and return a new {@link HttpUriRequest} that is signed.
472    *
473    * @param httpUriRequest
474    *          The possible URI to sign.
475    * @return HttpUriRequest if the request is a GET and is configured to be signed.
476    * @throws TrustedHttpClientException
477    *           Thrown if there is a problem signing the URL.
478    */
479   protected HttpUriRequest getSignedUrl(HttpUriRequest httpUriRequest) throws TrustedHttpClientException {
480     if (("GET".equalsIgnoreCase(httpUriRequest.getMethod()) || "HEAD".equalsIgnoreCase(httpUriRequest.getMethod()))
481             && ResourceRequestUtil.isNotSigned(httpUriRequest.getURI())
482             && urlSigningService.accepts(httpUriRequest.getURI().toString())) {
483       logger.trace("Signing request with method: {} and URI: {}", httpUriRequest.getMethod(), httpUriRequest.getURI());
484       try {
485         final String signedUrl = urlSigningService
486             .sign(httpUriRequest.getURI().toString(), signedUrlExpiresDuration, null, null);
487         HttpRequestBase signedRequest;
488         if ("GET".equalsIgnoreCase(httpUriRequest.getMethod())) {
489           signedRequest = new HttpGet(signedUrl);
490         } else {
491           signedRequest = new HttpHead(signedUrl);
492         }
493         signedRequest.setProtocolVersion(httpUriRequest.getProtocolVersion());
494         for (Header header : httpUriRequest.getAllHeaders()) {
495           signedRequest.addHeader(header);
496         }
497         return signedRequest;
498       } catch (UrlSigningException e) {
499         throw new TrustedHttpClientException(e);
500       }
501     }
502     return httpUriRequest;
503   }
504 
505   /**
506    * Retries a request if the nonce timed out during the request.
507    *
508    * @param httpUriRequest
509    *         The request to be made that isn't a GET, those are handled automatically.
510    * @param response
511    *         The response with the bad nonce timeout in it.
512    * @return A new response for the request if it was successful without the nonce timing out again or just the same
513    * response it got if it ran out of attempts.
514    * @throws TrustedHttpClientException
515    * @throws IOException
516    * @throws ClientProtocolException
517    */
518   private HttpResponse retryAuthAndRequestAfterNonceTimeout(HttpUriRequest httpUriRequest, HttpResponse response)
519           throws TrustedHttpClientException, IOException, ClientProtocolException {
520     // Get rid of old security headers with the old nonce.
521     httpUriRequest.removeHeaders(AUTHORIZATION_HEADER_NAME);
522 
523     for (int i = 0; i < nonceTimeoutRetries; i++) {
524       CloseableHttpClient httpClient = makeHttpClientBuilder(DEFAULT_CONNECTION_TIMEOUT, DEFAULT_SOCKET_TIMEOUT)
525           .build();
526       int variableDelay = 0;
527       // Make sure that we have a variable delay greater than 0.
528       if (retryMaximumVariableTime > 0) {
529         variableDelay = generator.nextInt(retryMaximumVariableTime * MILLISECONDS_IN_SECONDS);
530       }
531 
532       long totalDelay = (retryBaseDelay * MILLISECONDS_IN_SECONDS + variableDelay);
533       if (totalDelay > 0) {
534         logger.info("Sleeping " + totalDelay + "ms before trying request " + httpUriRequest.getURI()
535                             + " again due to a " + response.getStatusLine());
536         try {
537           Thread.sleep(totalDelay);
538         } catch (InterruptedException e) {
539           logger.error("Suffered InteruptedException while trying to sleep until next retry.", e);
540         }
541       }
542       manuallyHandleDigestAuthentication(httpUriRequest, httpClient);
543       response = new HttpResponseWrapper(httpClient.execute(httpUriRequest));
544       if (!hadNonceTimeoutResponse(response)) {
545         responseMap.put(response, httpClient);
546         break;
547       }
548       httpClient.close();
549     }
550     return response;
551   }
552 
553   /**
554    * Determines if the nonce has timed out before a request could be performed.
555    *
556    * @param response
557    *         The response to test to see if it has timed out.
558    * @return true if it has time out, false if it hasn't
559    */
560   private boolean hadNonceTimeoutResponse(HttpResponse response) {
561     return (401 == response.getStatusLine().getStatusCode())
562             && ("Nonce has expired/timed out".equals(response.getStatusLine().getReasonPhrase()));
563   }
564 
565   /**
566    * Handles the necessary handshake for digest authenticaion in the case where it isn't a GET operation.
567    *
568    * @param httpUriRequest
569    *         The request location to get the digest authentication for.
570    * @param httpClient
571    *         The client to send the request through.
572    * @throws TrustedHttpClientException
573    *         Thrown if the client cannot be shutdown.
574    */
575   private void manuallyHandleDigestAuthentication(HttpUriRequest httpUriRequest, CloseableHttpClient httpClient)
576           throws TrustedHttpClientException {
577     HttpRequestBase digestRequest;
578     try {
579       digestRequest = (HttpRequestBase) httpUriRequest.getClass().newInstance();
580     } catch (Exception e) {
581       throw new IllegalStateException("Can not create a new " + httpUriRequest.getClass().getName());
582     }
583     digestRequest.setURI(httpUriRequest.getURI());
584     digestRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH);
585     String[] realmAndNonce = getRealmAndNonce(digestRequest);
586 
587     if (realmAndNonce != null) {
588       // Set the user/pass
589       UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user, pass);
590 
591       // Set up the digest authentication with the required values
592       DigestScheme digestAuth = new DigestScheme();
593       digestAuth.overrideParamter("realm", realmAndNonce[0]);
594       digestAuth.overrideParamter("nonce", realmAndNonce[1]);
595 
596       // Add the authentication header
597       try {
598         httpUriRequest.setHeader(digestAuth.authenticate(creds, httpUriRequest));
599       } catch (Exception e) {
600         // close the http connection(s)
601         try {
602           httpClient.close();
603         } catch (IOException ex) {
604           throw new TrustedHttpClientException(ex);
605         }
606         throw new TrustedHttpClientException(e);
607       }
608     }
609   }
610 
611   /**
612    * {@inheritDoc}
613    *
614    * @see org.opencastproject.security.api.TrustedHttpClient#close(org.apache.http.HttpResponse)
615    */
616   @Override
617   public void close(HttpResponse response) throws IOException {
618     if (response != null) {
619       CloseableHttpClient httpClient = responseMap.remove(response);
620       if (httpClient != null) {
621         httpClient.close();
622       }
623     } else {
624       logger.debug("Can not close a null response");
625     }
626   }
627 
628   /**
629    * Perform a request, and extract the realm and nonce values
630    *
631    * @param request
632    *         The request to execute in order to obtain the realm and nonce
633    * @return A String[] containing the {realm, nonce}
634    */
635   protected String[] getRealmAndNonce(HttpRequestBase request) throws TrustedHttpClientException {
636     CloseableHttpClient httpClient = makeHttpClientBuilder(DEFAULT_CONNECTION_TIMEOUT, DEFAULT_SOCKET_TIMEOUT).build();
637     HttpResponse response;
638     try {
639       try {
640         response = new HttpResponseWrapper(httpClient.execute(request));
641         Header[] headers = response.getHeaders("WWW-Authenticate");
642         if (headers == null || headers.length == 0) {
643           logger.warn("URI {} does not support digest authentication", request.getURI());
644           return null;
645         }
646         Header authRequiredResponseHeader = headers[0];
647         String nonce = null;
648         String realm = null;
649         for (HeaderElement element : authRequiredResponseHeader.getElements()) {
650           if ("nonce".equals(element.getName())) {
651             nonce = element.getValue();
652           } else if ("Digest realm".equals(element.getName())) {
653             realm = element.getValue();
654           }
655         }
656         return new String[]{realm, nonce};
657       } finally {
658         httpClient.close();
659       }
660     } catch (IOException e) {
661       throw new TrustedHttpClientException(e);
662     }
663   }
664 
665   /**
666    * @return Returns the number of times the TrustedHttpClient will retry a request if nonce timeouts are occuring.
667    */
668   public int getNonceTimeoutRetries() {
669     return nonceTimeoutRetries;
670   }
671 
672   /**
673    * @return The minimum amount of time to wait in seconds after a nonce timeout before retrying.
674    */
675   public int getRetryBaseDelay() {
676     return retryBaseDelay;
677   }
678 
679   /**
680    * @return The maximum amount of time to wait in seconds after a nonce timeout in addition to the base delay.
681    */
682   public int getRetryMaximumVariableTime() {
683     return retryMaximumVariableTime;
684   }
685 
686   /**
687    * Very simple cache that does a <em>complete</em> refresh after a given interval. This type of cache is only suitable
688    * for small sets.
689    */
690   private static final class HostCache {
691     private final Object lock = new Object();
692 
693     // A simple hash map is sufficient here.
694     // No need to deal with soft references or an LRU map since the number of organizations
695     // will be quite low.
696     private final Set<String> hosts = set();
697     private final long refreshInterval;
698     private long lastRefresh;
699 
700     private final OrganizationDirectoryService organizationDirectoryService;
701     private final ServiceRegistry serviceRegistry;
702 
703     HostCache(long refreshInterval, OrganizationDirectoryService orgDirSrv, ServiceRegistry serviceReg) {
704       this.refreshInterval = refreshInterval;
705       this.organizationDirectoryService = orgDirSrv;
706       this.serviceRegistry = serviceReg;
707       invalidate();
708     }
709 
710     public boolean contains(String host) {
711       synchronized (lock) {
712         try {
713           refresh();
714         } catch (ServiceRegistryException e) {
715           logger.error("Unable to update host cache due to service registry exception!", e);
716         }
717         return hosts.contains(host);
718       }
719     }
720     public void invalidate() {
721       this.lastRefresh = System.currentTimeMillis() - 2 * refreshInterval;
722     }
723 
724     private void refresh() throws ServiceRegistryException {
725       final long now = System.currentTimeMillis();
726       if (now - lastRefresh > refreshInterval) {
727         hosts.clear();
728         //The hosts from the SR come back as protocol://hostname:port, use the URI class to get just the host
729         hosts.addAll(serviceRegistry.getHostRegistrations().stream()
730             .map(HostRegistration::getBaseUrl)
731             .map(URI::create)
732             .map(URI::getHost)
733             .collect(Collectors.toSet()));
734         //This is the *org's* server urls, as defined by in the org config with
735         // something like prop.org.opencastproject.host.$SERVER_URL=$TENANT_URL
736         // You're getting just the hostname of the tenant URL
737         hosts.addAll(organizationDirectoryService.getOrganizations().stream()
738             .map(Organization::getServers)
739             .map(Map::keySet)
740             .flatMap(Collection::stream).collect(Collectors.toSet()));
741         lastRefresh = now;
742       }
743     }
744   }
745 }