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