1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
98 public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
99
100
101 private static final Logger logger = LoggerFactory.getLogger(TrustedHttpClientImpl.class);
102
103
104 public static final String DIGEST_AUTH_USER_KEY = "org.opencastproject.security.digest.user";
105
106
107 public static final String DIGEST_AUTH_PASS_KEY = "org.opencastproject.security.digest.pass";
108
109
110 public static final String NONCE_TIMEOUT_RETRY_KEY = "org.opencastproject.security.digest.nonce.retries";
111
112
113 protected static final String INTERNAL_URL_SIGNING_DURATION_KEY =
114 "org.opencastproject.security.internal.url.signing.duration";
115
116
117
118
119
120 public static final String NONCE_TIMEOUT_RETRY_BASE_TIME_KEY = "org.opencastproject.security.digest.nonce.base.time";
121
122
123
124
125
126 public static final String NONCE_TIMEOUT_RETRY_MAXIMUM_VARIABLE_TIME_KEY =
127 "org.opencastproject.security.digest.nonce.variable.time";
128
129
130 public static final int DEFAULT_CONNECTION_TIMEOUT = 60 * 1000;
131
132
133 public static final int DEFAULT_SOCKET_TIMEOUT = 300 * 1000;
134
135
136 public static final int DEFAULT_NONCE_TIMEOUT_RETRIES = 12;
137
138
139 private static final int MILLISECONDS_IN_SECONDS = 1000;
140
141
142 public static final int DEFAULT_RETRY_BASE_TIME = 300;
143
144
145 public static final int DEFAULT_RETRY_MAXIMUM_VARIABLE_TIME = 300;
146
147
148
149
150
151 protected static final long DEFAULT_URL_SIGNING_EXPIRES_DURATION = 60;
152
153
154 protected String user = null;
155
156
157 protected String pass = null;
158
159
160 private int nonceTimeoutRetries = DEFAULT_NONCE_TIMEOUT_RETRIES;
161
162
163 protected Map<HttpResponse, CloseableHttpClient> responseMap = new ConcurrentHashMap<>();
164
165
166 private final Random generator = new Random();
167
168
169 private int retryBaseDelay = 300;
170
171
172 private int retryMaximumVariableTime = 300;
173
174
175 private long signedUrlExpiresDuration = DEFAULT_URL_SIGNING_EXPIRES_DURATION;
176
177
178 private ServiceRegistry serviceRegistry = null;
179
180
181 protected SecurityService securityService = null;
182
183
184 protected OrganizationDirectoryService organizationDirectoryService = null;
185
186
187 protected UrlSigningService urlSigningService = null;
188
189
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
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
237
238
239
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
251
252
253
254
255 public void unsetServiceRegistry(ServiceRegistry serviceRegistry) {
256 if (this.serviceRegistry == serviceRegistry) {
257 this.serviceRegistry = null;
258 }
259 }
260
261
262
263
264
265
266
267 @Reference
268 public void setSecurityService(SecurityService securityService) {
269 this.securityService = securityService;
270 }
271
272
273
274
275
276
277
278 @Reference
279 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
280 this.organizationDirectoryService = organizationDirectoryService;
281 }
282
283
284
285
286
287
288
289 @Reference
290 public void setUrlSigningService(UrlSigningService urlSigningService) {
291 this.urlSigningService = urlSigningService;
292 }
293
294
295
296
297
298
299
300 private void getRetryNumber(ComponentContext cc) {
301 nonceTimeoutRetries = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_KEY, DEFAULT_NONCE_TIMEOUT_RETRIES);
302 }
303
304
305
306
307
308
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
316
317
318
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
327
328
329
330
331
332
333
334
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
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
379
380
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
398 httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH);
399 }
400
401
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
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
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
442
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
455 if (response != null) {
456 responseMap.remove(response);
457 }
458
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
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
486
487
488
489
490
491
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
521
522
523
524
525
526
527
528
529
530
531
532 private HttpResponse retryAuthAndRequestAfterNonceTimeout(HttpUriRequest httpUriRequest, HttpResponse response)
533 throws TrustedHttpClientException, IOException, ClientProtocolException {
534
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
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
568
569
570
571
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
580
581
582
583
584
585
586
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
602 UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user, pass);
603
604
605 DigestScheme digestAuth = new DigestScheme();
606 digestAuth.overrideParamter("realm", realmAndNonce[0]);
607 digestAuth.overrideParamter("nonce", realmAndNonce[1]);
608
609
610 try {
611 httpUriRequest.setHeader(digestAuth.authenticate(creds, httpUriRequest));
612 } catch (Exception e) {
613
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
626
627
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
643
644
645
646
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
685
686 public int getNonceTimeoutRetries() {
687 return nonceTimeoutRetries;
688 }
689
690
691
692
693 public int getRetryBaseDelay() {
694 return retryBaseDelay;
695 }
696
697
698
699
700 public int getRetryMaximumVariableTime() {
701 return retryMaximumVariableTime;
702 }
703
704
705
706
707
708 private static final class HostCache {
709 private final Object lock = new Object();
710
711
712
713
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
747 hosts.addAll(serviceRegistry.getHostRegistrations().stream()
748 .map(HostRegistration::getBaseUrl)
749 .map(URI::create)
750 .map(URI::getHost)
751 .collect(Collectors.toSet()));
752
753
754
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 }