/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.security.urlsigning.provider.impl; import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.provider.UrlSigningProvider; import org.opencastproject.urlsigning.common.Policy; import org.opencastproject.urlsigning.common.ResourceStrategy; import org.opencastproject.urlsigning.utils.ResourceRequestUtil; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Dictionary; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; public abstract class AbstractUrlSigningProvider implements UrlSigningProvider, ManagedService { /** The prefix in the configuration file to define the id of the key. */ public static final String ID_PREFIX = "id"; /** The prefix in the configuration file to define the encryption key. */ public static final String KEY_PREFIX = "key"; /** The prefix in the configuration file to define the matching url. */ public static final String URL_PREFIX = "url"; /** * @return The method that an implementation class will convert base urls to resource urls. */ public abstract ResourceStrategy getResourceStrategy(); /** * @return The logger to use for this signing provider. */ public abstract Logger getLogger(); /** * A class to contain the necessary key entries for url signing. */ private class KeyEntry { private final String keyId; private final String key; KeyEntry(String keyId, String key) { this.keyId = keyId; this.key = key; } public String getId() { return keyId; } public String getKey() { return key; } } /** The map to contain the list of keys, their ids and the urls they match. */ private Map<String, KeyEntry> keys = new TreeMap<String, KeyEntry>(); /** * Add a new key entry to the collection of keys. * * @param keyId * The id of the key to add. * @param url * The url that the matching will apply to. * @param key * The encryption key to use for these urls. */ public void addKeyEntry(String keyId, String url, String key) { if (keyId == null) throw new IllegalArgumentException("The key id prefix must not be null"); if (url == null) throw new IllegalArgumentException("The url matcher prefix must not be null"); if (key == null) throw new IllegalArgumentException("The key prefix must not be null"); if (keys.containsKey(url)) throw new IllegalStateException("Url matcher '" + url + "' already registered"); keys.put(url, new KeyEntry(keyId, key)); } /** * @return The current set of url beginnings this signing provider is looking for. */ public Set<String> getUris() { return Collections.unmodifiableSet(keys.keySet()); } /** * If available get a {@link KeyEntry} if there is a matching Url matcher. * * @param baseUrl * The url to check against the possible matchers. * @return The {@link KeyEntry} if it is available. */ private KeyEntry getKeyEntry(String baseUrl) { for (String uriMatcher : keys.keySet()) { if (baseUrl.startsWith(uriMatcher)) { return keys.get(uriMatcher); } } return null; } @SuppressWarnings("rawtypes") @Override public void updated(Dictionary properties) throws ConfigurationException { getLogger().info("Updating {}", this.toString()); if (properties == null) { getLogger().warn("{} is unconfigured", this.toString()); return; } // Clear the current set of keys keys.clear(); String key = null; String keyId = null; String url = null; int i = 1; while (true) { // Create the configuration prefixes key = new StringBuilder(KEY_PREFIX).append(".").append(i).toString(); keyId = new StringBuilder(ID_PREFIX).append(".").append(i).toString(); url = new StringBuilder(URL_PREFIX).append(".").append(i).toString(); getLogger().debug("Looking for configuration of {}, {}, and {}", new Object[] { key, keyId, url }); // Read the key, keyId and url values String keyValue = StringUtils.trimToNull((String) properties.get(key)); String keyIdValue = StringUtils.trimToNull((String) properties.get(keyId)); String urlValue = StringUtils.trimToNull((String) properties.get(url)); // Has the url signing provider been fully configured if (keyValue == null || keyIdValue == null || urlValue == null) { getLogger() .debug("Unable to configure key with id '{}' and url matcher '{}' because the id, key or url is missing. Stopping to look for new keys.", keyIdValue, urlValue); break; } // Store the key try { addKeyEntry(keyIdValue, urlValue, keyValue); getLogger().info("{} will handle uris that start with '{}' with the key id '{}'", new Object[] { this.toString(), urlValue, keyIdValue }); } catch (IllegalStateException e) { throw new ConfigurationException(urlValue, e.getMessage()); } i++; } // Has the rewriter been fully configured if (keys.size() == 0) { getLogger().info("{} configured to not sign any urls.", this.toString()); return; } } @Override public boolean accepts(String baseUrl) { try { new URI(baseUrl); for (String uriMatcher : keys.keySet()) { if (baseUrl.startsWith(uriMatcher)) { return true; } } return false; } catch (URISyntaxException e) { getLogger().debug("Unable to support url {} because: {}", baseUrl, ExceptionUtils.getStackTrace(e)); return false; } } @Override public String sign(Policy policy) throws UrlSigningException { if (!accepts(policy.getBaseUrl())) { throw UrlSigningException.urlNotSupported(); } // Get the key that matches this URI since there must be one that matches as the base url has been accepted. KeyEntry keyEntry = getKeyEntry(policy.getBaseUrl()); policy.setResourceStrategy(getResourceStrategy()); try { URI uri = new URI(policy.getBaseUrl()); List<NameValuePair> queryStringParameters = new ArrayList<NameValuePair>(); if (uri.getQuery() != null) { queryStringParameters = URLEncodedUtils.parse(new URI(policy.getBaseUrl()).getQuery(), StandardCharsets.UTF_8); } queryStringParameters.addAll(URLEncodedUtils.parse( ResourceRequestUtil.policyToResourceRequestQueryString(policy, keyEntry.getId(), keyEntry.getKey()), StandardCharsets.UTF_8)); return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), URLEncodedUtils.format( queryStringParameters, StandardCharsets.UTF_8), null).toString(); } catch (Exception e) { getLogger().error("Unable to create signed URL because {}", ExceptionUtils.getStackTrace(e)); throw new UrlSigningException(e); } } }