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