/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.security.samlp; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.function.Consumer; import org.apache.commons.lang.StringUtils; import org.apache.cxf.staxutils.StaxUtils; import org.apache.wss4j.common.ext.WSSecurityException; import org.apache.wss4j.common.saml.OpenSAMLUtil; import org.codice.ddf.configuration.PropertyResolver; import org.opensaml.core.xml.XMLObject; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import com.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpBackOffIOExceptionHandler; import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.util.ExponentialBackOff; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; public class MetadataConfigurationParser { public static final String METADATA_ROOT_FOLDER = "metadata"; public static final String ETC_FOLDER = "etc"; private static final Logger LOGGER = LoggerFactory.getLogger(MetadataConfigurationParser.class); private static final String HTTPS = "https://"; private static final String HTTP = "http://"; private static final String FILE = "file:"; private final Map<String, EntityDescriptor> entityDescriptorMap = new ConcurrentHashMap<>(); private final Consumer<EntityDescriptor> updateCallback; static { OpenSAMLUtil.initSamlEngine(); } public MetadataConfigurationParser(List<String> entityDescriptions) throws IOException { this(entityDescriptions, null); } public MetadataConfigurationParser(List<String> entityDescriptions, Consumer<EntityDescriptor> updateCallback) throws IOException { this.updateCallback = updateCallback; parseEntityDescriptions(entityDescriptions); } public Map<String, EntityDescriptor> getEntryDescriptions() { return entityDescriptorMap; } private void parseEntityDescriptions(List<String> entityDescriptions) throws IOException { String ddfHome = System.getProperty("ddf.home"); for (String entityDescription : entityDescriptions) { buildEntityDescriptor(entityDescription); } Path metadataFolder = Paths.get(ddfHome, ETC_FOLDER, METADATA_ROOT_FOLDER); try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(metadataFolder)) { for (Path path : directoryStream) { if (Files.isReadable(path)) { try (InputStream fileInputStream = Files.newInputStream(path)) { EntityDescriptor entityDescriptor = readEntityDescriptor(new InputStreamReader(fileInputStream, "UTF-8")); LOGGER.info("entityId = {}", entityDescriptor.getEntityID()); entityDescriptorMap.put(entityDescriptor.getEntityID(), entityDescriptor); if (updateCallback != null) { updateCallback.accept(entityDescriptor); } } } } } catch (NoSuchFileException e) { LOGGER.debug("IDP metadata directory is not configured.", e); } } private void buildEntityDescriptor(String entityDescription) throws IOException { EntityDescriptor entityDescriptor = null; entityDescription = entityDescription.trim(); if (entityDescription.startsWith(HTTPS) || entityDescription.startsWith(HTTP)) { if (entityDescription.startsWith(HTTP)) { LOGGER.warn( "Retrieving metadata via HTTP instead of HTTPS. The metadata configuration is unsafe!!!"); } PropertyResolver propertyResolver = new PropertyResolver(entityDescription); HttpTransport httpTransport = new NetHttpTransport(); HttpRequest httpRequest = httpTransport.createRequestFactory() .buildGetRequest(new GenericUrl(propertyResolver.getResolvedString())); httpRequest.setUnsuccessfulResponseHandler(new HttpBackOffUnsuccessfulResponseHandler( new ExponentialBackOff()).setBackOffRequired( HttpBackOffUnsuccessfulResponseHandler.BackOffRequired.ALWAYS)); httpRequest.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(new ExponentialBackOff())); ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); ListenableFuture<HttpResponse> httpResponseFuture = service.submit(httpRequest::execute); Futures.addCallback(httpResponseFuture, new FutureCallback<HttpResponse>() { @Override public void onSuccess(HttpResponse httpResponse) { if (httpResponse != null) { try { String parsedResponse = httpResponse.parseAsString(); buildEntityDescriptor(parsedResponse); } catch (IOException e) { LOGGER.info("Unable to parse metadata from: {}", httpResponse.getRequest() .getUrl() .toString(), e); } } } @Override public void onFailure(Throwable throwable) { LOGGER.info("Unable to retrieve metadata.", throwable); } }); service.shutdown(); } else if (entityDescription.startsWith(FILE + System.getProperty("ddf.home"))) { String pathStr = StringUtils.substringAfter(entityDescription, FILE); Path path = Paths.get(pathStr); if (Files.isReadable(path)) { try (InputStream fileInputStream = Files.newInputStream(path)) { entityDescriptor = readEntityDescriptor(new InputStreamReader(fileInputStream, "UTF-8")); } } } else if (entityDescription.startsWith("<") && entityDescription.endsWith(">")) { entityDescriptor = readEntityDescriptor(new StringReader(entityDescription)); } else { LOGGER.info("Skipping unknown metadata configuration value: {}", entityDescription); } if (entityDescriptor != null) { entityDescriptorMap.put(entityDescriptor.getEntityID(), entityDescriptor); if (updateCallback != null) { updateCallback.accept(entityDescriptor); } } } private EntityDescriptor readEntityDescriptor(Reader reader) { Document entityDoc; try { entityDoc = StaxUtils.read(reader); } catch (Exception ex) { throw new IllegalArgumentException("Unable to read SAMLRequest as XML."); } XMLObject entityXmlObj; try { entityXmlObj = OpenSAMLUtil.fromDom(entityDoc.getDocumentElement()); } catch (WSSecurityException ex) { throw new IllegalArgumentException( "Unable to convert EntityDescriptor document to XMLObject."); } return (EntityDescriptor) entityXmlObj; } }