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
164
165 protected Map<HttpResponse, CloseableHttpClient> responseMap = new ConcurrentHashMap<>();
166
167
168 private final Random generator = new Random();
169
170
171 private int retryBaseDelay = 300;
172
173
174 private int retryMaximumVariableTime = 300;
175
176
177 private long signedUrlExpiresDuration = DEFAULT_URL_SIGNING_EXPIRES_DURATION;
178
179
180 private ServiceRegistry serviceRegistry = null;
181
182
183 protected SecurityService securityService = null;
184
185
186 protected OrganizationDirectoryService organizationDirectoryService = null;
187
188
189 protected UrlSigningService urlSigningService = null;
190
191
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
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
240
241
242
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
254
255
256
257
258 public void unsetServiceRegistry(ServiceRegistry serviceRegistry) {
259 if (this.serviceRegistry == serviceRegistry) {
260 this.serviceRegistry = null;
261 }
262 }
263
264
265
266
267
268
269
270 @Reference
271 public void setSecurityService(SecurityService securityService) {
272 this.securityService = securityService;
273 }
274
275
276
277
278
279
280
281 @Reference
282 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
283 this.organizationDirectoryService = organizationDirectoryService;
284 }
285
286
287
288
289
290
291
292 @Reference
293 public void setUrlSigningService(UrlSigningService urlSigningService) {
294 this.urlSigningService = urlSigningService;
295 }
296
297
298
299
300
301
302
303 private void getRetryNumber(ComponentContext cc) {
304 nonceTimeoutRetries = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_KEY, DEFAULT_NONCE_TIMEOUT_RETRIES);
305 }
306
307
308
309
310
311
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
319
320
321
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
330
331
332
333
334
335
336
337
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
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
382
383
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
401 httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH);
402 }
403
404
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
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
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
445
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
458 if (response != null) {
459 responseMap.remove(response);
460 }
461
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
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
489
490
491
492
493
494
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
524
525
526
527
528
529
530
531
532
533
534
535 private HttpResponse retryAuthAndRequestAfterNonceTimeout(HttpUriRequest httpUriRequest, HttpResponse response)
536 throws TrustedHttpClientException, IOException, ClientProtocolException {
537
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
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
572
573
574
575
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
584
585
586
587
588
589
590
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
606 UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user, pass);
607
608
609 DigestScheme digestAuth = new DigestScheme();
610 digestAuth.overrideParamter("realm", realmAndNonce[0]);
611 digestAuth.overrideParamter("nonce", realmAndNonce[1]);
612
613
614 try {
615 httpUriRequest.setHeader(digestAuth.authenticate(creds, httpUriRequest));
616 } catch (Exception e) {
617
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
630
631
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
647
648
649
650
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
689
690 public int getNonceTimeoutRetries() {
691 return nonceTimeoutRetries;
692 }
693
694
695
696
697 public int getRetryBaseDelay() {
698 return retryBaseDelay;
699 }
700
701
702
703
704 public int getRetryMaximumVariableTime() {
705 return retryMaximumVariableTime;
706 }
707
708
709
710
711
712 private static final class HostCache {
713 private final Object lock = new Object();
714
715
716
717
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
751 hosts.addAll(serviceRegistry.getHostRegistrations().stream()
752 .map(HostRegistration::getBaseUrl)
753 .map(URI::create)
754 .map(URI::getHost)
755 .collect(Collectors.toSet()));
756
757
758
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 }