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.net.URI;
75 import java.util.Collection;
76 import java.util.Map;
77 import java.util.Random;
78 import java.util.Set;
79 import java.util.concurrent.ConcurrentHashMap;
80 import java.util.stream.Collectors;
81
82
83
84
85 @Component(
86 property = {
87 "service.description=Provides Trusted Http Clients (for use with digest authentication)"
88 },
89 immediate = true,
90 service = { TrustedHttpClient.class }
91 )
92 public class TrustedHttpClientImpl implements TrustedHttpClient {
93
94 public static final String AUTHORIZATION_HEADER_NAME = "Authorization";
95
96
97 private static final Logger logger = LoggerFactory.getLogger(TrustedHttpClientImpl.class);
98
99
100 public static final String DIGEST_AUTH_USER_KEY = "org.opencastproject.security.digest.user";
101
102
103 public static final String DIGEST_AUTH_PASS_KEY = "org.opencastproject.security.digest.pass";
104
105
106 public static final String NONCE_TIMEOUT_RETRY_KEY = "org.opencastproject.security.digest.nonce.retries";
107
108
109 protected static final String INTERNAL_URL_SIGNING_DURATION_KEY =
110 "org.opencastproject.security.internal.url.signing.duration";
111
112
113
114
115
116 public static final String NONCE_TIMEOUT_RETRY_BASE_TIME_KEY = "org.opencastproject.security.digest.nonce.base.time";
117
118
119
120
121
122 public static final String NONCE_TIMEOUT_RETRY_MAXIMUM_VARIABLE_TIME_KEY =
123 "org.opencastproject.security.digest.nonce.variable.time";
124
125
126 public static final int DEFAULT_CONNECTION_TIMEOUT = 60 * 1000;
127
128
129 public static final int DEFAULT_SOCKET_TIMEOUT = 300 * 1000;
130
131
132 public static final int DEFAULT_NONCE_TIMEOUT_RETRIES = 12;
133
134
135 private static final int MILLISECONDS_IN_SECONDS = 1000;
136
137
138 public static final int DEFAULT_RETRY_BASE_TIME = 300;
139
140
141 public static final int DEFAULT_RETRY_MAXIMUM_VARIABLE_TIME = 300;
142
143
144
145
146
147 protected static final long DEFAULT_URL_SIGNING_EXPIRES_DURATION = 60;
148
149
150 protected String user = null;
151
152
153 protected String pass = null;
154
155
156 private int nonceTimeoutRetries = DEFAULT_NONCE_TIMEOUT_RETRIES;
157
158
159
160
161 protected Map<HttpResponse, CloseableHttpClient> responseMap = new ConcurrentHashMap<>();
162
163
164 private final Random generator = new Random();
165
166
167 private int retryBaseDelay = 300;
168
169
170 private int retryMaximumVariableTime = 300;
171
172
173 private long signedUrlExpiresDuration = DEFAULT_URL_SIGNING_EXPIRES_DURATION;
174
175
176 private ServiceRegistry serviceRegistry = null;
177
178
179 protected SecurityService securityService = null;
180
181
182 protected OrganizationDirectoryService organizationDirectoryService = null;
183
184
185 protected UrlSigningService urlSigningService = null;
186
187
188 private HostCache hosts = null;
189
190
191 @Activate
192 public void activate(ComponentContext cc) {
193 logger.debug("activate");
194 user = cc.getBundleContext().getProperty(DIGEST_AUTH_USER_KEY);
195 pass = cc.getBundleContext().getProperty(DIGEST_AUTH_PASS_KEY);
196 if (user == null || pass == null) {
197 throw new IllegalStateException("trusted communication is not properly configured");
198 }
199
200 getRetryNumber(cc);
201 getRetryBaseTime(cc);
202 getRetryMaximumVariableTime(cc);
203
204 final Long expiration = NumberUtils.createLong(StringUtils.trimToNull(
205 cc.getBundleContext().getProperty(INTERNAL_URL_SIGNING_DURATION_KEY)));
206 if (expiration != null) {
207 signedUrlExpiresDuration = expiration;
208 } else {
209 signedUrlExpiresDuration = DEFAULT_URL_SIGNING_EXPIRES_DURATION;
210 }
211 logger.debug("Expire signed URLs in {} seconds.", signedUrlExpiresDuration);
212 }
213
214 private HostCache getHostCache() {
215 if (null == hosts) {
216 hosts = new HostCache(60000, this.organizationDirectoryService, this.serviceRegistry);
217 }
218 return hosts;
219 }
220
221
222
223
224
225
226
227 @Reference(
228 cardinality = ReferenceCardinality.OPTIONAL,
229 policy = ReferencePolicy.DYNAMIC,
230 unbind = "unsetServiceRegistry")
231 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
232 this.serviceRegistry = serviceRegistry;
233 }
234
235
236
237
238
239
240
241 public void unsetServiceRegistry(ServiceRegistry serviceRegistry) {
242 if (this.serviceRegistry == serviceRegistry) {
243 this.serviceRegistry = null;
244 }
245 }
246
247
248
249
250
251
252
253 @Reference
254 public void setSecurityService(SecurityService securityService) {
255 this.securityService = securityService;
256 }
257
258
259
260
261
262
263
264 @Reference
265 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) {
266 this.organizationDirectoryService = organizationDirectoryService;
267 }
268
269
270
271
272
273
274
275 @Reference
276 public void setUrlSigningService(UrlSigningService urlSigningService) {
277 this.urlSigningService = urlSigningService;
278 }
279
280
281
282
283
284
285
286 private void getRetryNumber(ComponentContext cc) {
287 nonceTimeoutRetries = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_KEY, DEFAULT_NONCE_TIMEOUT_RETRIES);
288 }
289
290
291
292
293
294
295
296 private void getRetryBaseTime(ComponentContext cc) {
297 retryBaseDelay = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_BASE_TIME_KEY, DEFAULT_RETRY_BASE_TIME);
298 }
299
300
301
302
303
304
305
306 private void getRetryMaximumVariableTime(ComponentContext cc) {
307 retryMaximumVariableTime = getIntFromComponentContext(cc, NONCE_TIMEOUT_RETRY_MAXIMUM_VARIABLE_TIME_KEY,
308 DEFAULT_RETRY_MAXIMUM_VARIABLE_TIME);
309 }
310
311
312
313
314
315
316
317
318
319
320
321
322 private int getIntFromComponentContext(ComponentContext cc, String key, int defaultValue) {
323 int result;
324 try {
325 String stringValue = cc.getBundleContext().getProperty(key);
326 result = Integer.parseInt(StringUtils.trimToNull(stringValue));
327 } catch (Exception e) {
328 if (cc != null && cc.getBundleContext() != null && cc.getBundleContext().getProperty(key) != null) {
329 logger.info("Unable to get property with key " + key + " with value " + cc.getBundleContext().getProperty(key)
330 + " so using default of " + defaultValue + " because of " + e.getMessage());
331 } else {
332 logger.info("Unable to get property with key " + key + " so using default of " + defaultValue + " because of "
333 + e.getMessage());
334 }
335 result = defaultValue;
336 }
337
338 return result;
339 }
340
341 @Deactivate
342 public void deactivate() {
343 logger.debug("deactivate");
344 }
345
346 public TrustedHttpClientImpl() {
347
348 }
349
350 public TrustedHttpClientImpl(String user, String pass) {
351 this.user = user;
352 this.pass = pass;
353 }
354
355
356 public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socketTimeout) {
357 RequestConfig config = RequestConfig.custom()
358 .setConnectionRequestTimeout(connectionTimeout)
359 .setSocketTimeout(socketTimeout).build();
360 return HttpClientBuilder.create().setDefaultRequestConfig(config);
361 }
362
363
364
365
366
367
368 @Override
369 public HttpResponse execute(HttpUriRequest httpUriRequest) throws TrustedHttpClientException {
370 return execute(httpUriRequest, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_SOCKET_TIMEOUT);
371 }
372
373 @Override
374 public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout, int socketTimeout)
375 throws TrustedHttpClientException {
376 if (serviceRegistry != null && serviceRegistry.getCurrentJob() != null) {
377 httpUriRequest.setHeader(CURRENT_JOB_HEADER, Long.toString(serviceRegistry.getCurrentJob().getId()));
378 }
379
380 boolean enableDigest = getHostCache().contains(httpUriRequest.getURI().getHost());
381 logger.debug("Digest auth enabled for this request: " + enableDigest);
382 if (enableDigest) {
383
384 httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH);
385 }
386
387
388 logger.debug("Adding security context to request");
389 final Organization organization = securityService.getOrganization();
390 if (organization != null) {
391 httpUriRequest.setHeader(SecurityConstants.ORGANIZATION_HEADER, organization.getId());
392 final User currentUser = securityService.getUser();
393 if (enableDigest && currentUser != null) {
394 httpUriRequest.setHeader(SecurityConstants.USER_HEADER, currentUser.getUsername());
395 }
396 }
397
398 final HttpClientBuilder clientBuilder = makeHttpClientBuilder(connectionTimeout, socketTimeout);
399 if ("GET".equalsIgnoreCase(httpUriRequest.getMethod()) || "HEAD".equalsIgnoreCase(httpUriRequest.getMethod())) {
400 final CloseableHttpClient httpClient;
401 if (enableDigest) {
402
403 CredentialsProvider provider = new BasicCredentialsProvider();
404 provider.setCredentials(
405 new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.DIGEST),
406 new UsernamePasswordCredentials(user, pass));
407 httpClient = clientBuilder.setDefaultCredentialsProvider(provider).build();
408 } else {
409 httpClient = clientBuilder.build();
410 }
411
412 try {
413 httpUriRequest = getSignedUrl(httpUriRequest);
414 HttpResponse response = new HttpResponseWrapper(httpClient.execute(httpUriRequest));
415 responseMap.put(response, httpClient);
416 return response;
417 } catch (IOException e) {
418 try {
419 httpClient.close();
420 } catch (IOException ioException) {
421 throw new TrustedHttpClientException(e);
422 }
423 throw new TrustedHttpClientException(e);
424 }
425 } else if (enableDigest) {
426 final CloseableHttpClient httpClient = clientBuilder.build();
427
428
429 manuallyHandleDigestAuthentication(httpUriRequest, httpClient);
430 HttpResponse response = null;
431 try {
432 response = new HttpResponseWrapper(httpClient.execute(httpUriRequest));
433 if (nonceTimeoutRetries > 0 && hadNonceTimeoutResponse(response)) {
434 httpClient.close();
435 response = retryAuthAndRequestAfterNonceTimeout(httpUriRequest, response);
436 }
437 responseMap.put(response, httpClient);
438 return response;
439 } catch (Exception e) {
440
441 if (response != null) {
442 responseMap.remove(response);
443 }
444
445 try {
446 httpClient.close();
447 } catch (IOException ioException) {
448 throw new TrustedHttpClientException(e);
449 }
450 throw new TrustedHttpClientException(e);
451 }
452 } else {
453 final CloseableHttpClient httpClient = clientBuilder.build();
454 HttpResponse response = null;
455 try {
456 response = httpClient.execute(httpUriRequest);
457 return response;
458 } catch (Exception e) {
459
460 try {
461 httpClient.close();
462 } catch (IOException ioException) {
463 throw new TrustedHttpClientException(e);
464 }
465 throw new TrustedHttpClientException(e);
466 }
467 }
468 }
469
470
471
472
473
474
475
476
477
478
479 protected HttpUriRequest getSignedUrl(HttpUriRequest httpUriRequest) throws TrustedHttpClientException {
480 if (("GET".equalsIgnoreCase(httpUriRequest.getMethod()) || "HEAD".equalsIgnoreCase(httpUriRequest.getMethod()))
481 && ResourceRequestUtil.isNotSigned(httpUriRequest.getURI())
482 && urlSigningService.accepts(httpUriRequest.getURI().toString())) {
483 logger.trace("Signing request with method: {} and URI: {}", httpUriRequest.getMethod(), httpUriRequest.getURI());
484 try {
485 final String signedUrl = urlSigningService
486 .sign(httpUriRequest.getURI().toString(), signedUrlExpiresDuration, null, null);
487 HttpRequestBase signedRequest;
488 if ("GET".equalsIgnoreCase(httpUriRequest.getMethod())) {
489 signedRequest = new HttpGet(signedUrl);
490 } else {
491 signedRequest = new HttpHead(signedUrl);
492 }
493 signedRequest.setProtocolVersion(httpUriRequest.getProtocolVersion());
494 for (Header header : httpUriRequest.getAllHeaders()) {
495 signedRequest.addHeader(header);
496 }
497 return signedRequest;
498 } catch (UrlSigningException e) {
499 throw new TrustedHttpClientException(e);
500 }
501 }
502 return httpUriRequest;
503 }
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518 private HttpResponse retryAuthAndRequestAfterNonceTimeout(HttpUriRequest httpUriRequest, HttpResponse response)
519 throws TrustedHttpClientException, IOException, ClientProtocolException {
520
521 httpUriRequest.removeHeaders(AUTHORIZATION_HEADER_NAME);
522
523 for (int i = 0; i < nonceTimeoutRetries; i++) {
524 CloseableHttpClient httpClient = makeHttpClientBuilder(DEFAULT_CONNECTION_TIMEOUT, DEFAULT_SOCKET_TIMEOUT)
525 .build();
526 int variableDelay = 0;
527
528 if (retryMaximumVariableTime > 0) {
529 variableDelay = generator.nextInt(retryMaximumVariableTime * MILLISECONDS_IN_SECONDS);
530 }
531
532 long totalDelay = (retryBaseDelay * MILLISECONDS_IN_SECONDS + variableDelay);
533 if (totalDelay > 0) {
534 logger.info("Sleeping " + totalDelay + "ms before trying request " + httpUriRequest.getURI()
535 + " again due to a " + response.getStatusLine());
536 try {
537 Thread.sleep(totalDelay);
538 } catch (InterruptedException e) {
539 logger.error("Suffered InteruptedException while trying to sleep until next retry.", e);
540 }
541 }
542 manuallyHandleDigestAuthentication(httpUriRequest, httpClient);
543 response = new HttpResponseWrapper(httpClient.execute(httpUriRequest));
544 if (!hadNonceTimeoutResponse(response)) {
545 responseMap.put(response, httpClient);
546 break;
547 }
548 httpClient.close();
549 }
550 return response;
551 }
552
553
554
555
556
557
558
559
560 private boolean hadNonceTimeoutResponse(HttpResponse response) {
561 return (401 == response.getStatusLine().getStatusCode())
562 && ("Nonce has expired/timed out".equals(response.getStatusLine().getReasonPhrase()));
563 }
564
565
566
567
568
569
570
571
572
573
574
575 private void manuallyHandleDigestAuthentication(HttpUriRequest httpUriRequest, CloseableHttpClient httpClient)
576 throws TrustedHttpClientException {
577 HttpRequestBase digestRequest;
578 try {
579 digestRequest = (HttpRequestBase) httpUriRequest.getClass().newInstance();
580 } catch (Exception e) {
581 throw new IllegalStateException("Can not create a new " + httpUriRequest.getClass().getName());
582 }
583 digestRequest.setURI(httpUriRequest.getURI());
584 digestRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH);
585 String[] realmAndNonce = getRealmAndNonce(digestRequest);
586
587 if (realmAndNonce != null) {
588
589 UsernamePasswordCredentials creds = new UsernamePasswordCredentials(user, pass);
590
591
592 DigestScheme digestAuth = new DigestScheme();
593 digestAuth.overrideParamter("realm", realmAndNonce[0]);
594 digestAuth.overrideParamter("nonce", realmAndNonce[1]);
595
596
597 try {
598 httpUriRequest.setHeader(digestAuth.authenticate(creds, httpUriRequest));
599 } catch (Exception e) {
600
601 try {
602 httpClient.close();
603 } catch (IOException ex) {
604 throw new TrustedHttpClientException(ex);
605 }
606 throw new TrustedHttpClientException(e);
607 }
608 }
609 }
610
611
612
613
614
615
616 @Override
617 public void close(HttpResponse response) throws IOException {
618 if (response != null) {
619 CloseableHttpClient httpClient = responseMap.remove(response);
620 if (httpClient != null) {
621 httpClient.close();
622 }
623 } else {
624 logger.debug("Can not close a null response");
625 }
626 }
627
628
629
630
631
632
633
634
635 protected String[] getRealmAndNonce(HttpRequestBase request) throws TrustedHttpClientException {
636 CloseableHttpClient httpClient = makeHttpClientBuilder(DEFAULT_CONNECTION_TIMEOUT, DEFAULT_SOCKET_TIMEOUT).build();
637 HttpResponse response;
638 try {
639 try {
640 response = new HttpResponseWrapper(httpClient.execute(request));
641 Header[] headers = response.getHeaders("WWW-Authenticate");
642 if (headers == null || headers.length == 0) {
643 logger.warn("URI {} does not support digest authentication", request.getURI());
644 return null;
645 }
646 Header authRequiredResponseHeader = headers[0];
647 String nonce = null;
648 String realm = null;
649 for (HeaderElement element : authRequiredResponseHeader.getElements()) {
650 if ("nonce".equals(element.getName())) {
651 nonce = element.getValue();
652 } else if ("Digest realm".equals(element.getName())) {
653 realm = element.getValue();
654 }
655 }
656 return new String[]{realm, nonce};
657 } finally {
658 httpClient.close();
659 }
660 } catch (IOException e) {
661 throw new TrustedHttpClientException(e);
662 }
663 }
664
665
666
667
668 public int getNonceTimeoutRetries() {
669 return nonceTimeoutRetries;
670 }
671
672
673
674
675 public int getRetryBaseDelay() {
676 return retryBaseDelay;
677 }
678
679
680
681
682 public int getRetryMaximumVariableTime() {
683 return retryMaximumVariableTime;
684 }
685
686
687
688
689
690 private static final class HostCache {
691 private final Object lock = new Object();
692
693
694
695
696 private final Set<String> hosts = set();
697 private final long refreshInterval;
698 private long lastRefresh;
699
700 private final OrganizationDirectoryService organizationDirectoryService;
701 private final ServiceRegistry serviceRegistry;
702
703 HostCache(long refreshInterval, OrganizationDirectoryService orgDirSrv, ServiceRegistry serviceReg) {
704 this.refreshInterval = refreshInterval;
705 this.organizationDirectoryService = orgDirSrv;
706 this.serviceRegistry = serviceReg;
707 invalidate();
708 }
709
710 public boolean contains(String host) {
711 synchronized (lock) {
712 try {
713 refresh();
714 } catch (ServiceRegistryException e) {
715 logger.error("Unable to update host cache due to service registry exception!", e);
716 }
717 return hosts.contains(host);
718 }
719 }
720 public void invalidate() {
721 this.lastRefresh = System.currentTimeMillis() - 2 * refreshInterval;
722 }
723
724 private void refresh() throws ServiceRegistryException {
725 final long now = System.currentTimeMillis();
726 if (now - lastRefresh > refreshInterval) {
727 hosts.clear();
728
729 hosts.addAll(serviceRegistry.getHostRegistrations().stream()
730 .map(HostRegistration::getBaseUrl)
731 .map(URI::create)
732 .map(URI::getHost)
733 .collect(Collectors.toSet()));
734
735
736
737 hosts.addAll(organizationDirectoryService.getOrganizations().stream()
738 .map(Organization::getServers)
739 .map(Map::keySet)
740 .flatMap(Collection::stream).collect(Collectors.toSet()));
741 lastRefresh = now;
742 }
743 }
744 }
745 }