/** * 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.urlsigning.utils; import org.opencastproject.urlsigning.common.Policy; import org.opencastproject.urlsigning.common.ResourceRequest; import org.opencastproject.urlsigning.common.ResourceRequest.Status; 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.apache.http.message.BasicNameValuePair; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Properties; /** * A utility class to transform ResourceRequests into query strings and back. */ public final class ResourceRequestUtil { private static final Logger logger = LoggerFactory.getLogger(ResourceRequestUtil.class); private static final DateTimeFormatter humanReadableFormat = DateTimeFormat.forPattern("yyyy-MM-dd kk:mm:ss Z").withZoneUTC(); private ResourceRequestUtil() { } /** * Get a list of all of the query string parameters and their values. * * @param queryString * The query string to process. * @return A {@link List} of {@link NameValuePair} representing the query string parameters */ protected static List<NameValuePair> parseQueryString(String queryString) { if (StringUtils.isBlank(queryString)) { return new ArrayList<NameValuePair>(); } return URLEncodedUtils.parse(queryString.replaceFirst("\\?", ""), StandardCharsets.UTF_8); } /** * Get all of the necessary query string parameters. * * @param queryParameters * The query string parameters. * @return True if all of the mandatory query string parameters were provided. */ private static boolean getQueryStringParameters(ResourceRequest resourceRequest, List<NameValuePair> queryParameters) { for (NameValuePair nameValuePair : queryParameters) { if (ResourceRequest.ENCRYPTION_ID_KEY.equals(nameValuePair.getName())) { if (StringUtils.isBlank(resourceRequest.getEncryptionKeyId())) { resourceRequest.setEncryptionKeyId(nameValuePair.getValue()); } else { resourceRequest.setStatus(Status.BadRequest); resourceRequest.setRejectionReason( String.format("The mandatory '%s' query string parameter had an empty value.", ResourceRequest.ENCRYPTION_ID_KEY)); return false; } } if (ResourceRequest.POLICY_KEY.equals(nameValuePair.getName())) { if (StringUtils.isBlank(resourceRequest.getEncodedPolicy())) { resourceRequest.setEncodedPolicy(nameValuePair.getValue()); } else { resourceRequest.setStatus(Status.BadRequest); resourceRequest.setRejectionReason( String.format("The mandatory '%s' query string parameter had an empty value.", ResourceRequest.POLICY_KEY)); return false; } } if (ResourceRequest.SIGNATURE_KEY.equals(nameValuePair.getName())) { if (StringUtils.isBlank(resourceRequest.getSignature())) { resourceRequest.setSignature(nameValuePair.getValue()); resourceRequest.setRejectionReason( String.format("The mandatory '%s' query string parameter had an empty value.", ResourceRequest.SIGNATURE_KEY)); } else { resourceRequest.setStatus(Status.BadRequest); return false; } } } if (StringUtils.isBlank(resourceRequest.getEncodedPolicy())) { resourceRequest.setStatus(Status.BadRequest); resourceRequest.setRejectionReason(String.format("The mandatory '%s' query string parameter was missing.", ResourceRequest.POLICY_KEY)); return false; } else if (StringUtils.isBlank(resourceRequest.getEncryptionKeyId())) { resourceRequest.setStatus(Status.BadRequest); resourceRequest.setRejectionReason(String.format("The mandatory '%s' query string parameter was missing.", ResourceRequest.ENCRYPTION_ID_KEY)); return false; } else if (StringUtils.isBlank(resourceRequest.getSignature())) { resourceRequest.setStatus(Status.BadRequest); resourceRequest.setRejectionReason(String.format("The mandatory '%s' query string parameter was missing.", ResourceRequest.SIGNATURE_KEY)); return false; } return true; } /** * Determine if the policy matches the encrypted signature. * * @param policy * The policy to compare to the encrypted signature. * @param signature * The encrypted policy that was sent. * @param encryptionKey * The encryption key to use to encrypt the policy. * @return If the policy encrypted matches the signature. */ protected static boolean policyMatchesSignature(Policy policy, String signature, String encryptionKey) { try { String encryptedPolicy = PolicyUtils.getPolicySignature(policy, encryptionKey); return signature.equals(encryptedPolicy); } catch (Exception e) { logger.warn("Unable to encrypt policy because {}", ExceptionUtils.getStackTrace(e)); return false; } } /** * Create a {@link ResourceRequest} from the necessary data encoded policy, encryptionKeyId and signature. * * @param encodedPolicy * The policy Base64 encoded. * @param encryptionKeyId * The id of the encryption key used. * @param signature * The policy encrypted using the key attached to the encryptionKeyId * @return A new {@link ResourceRequest} filled with the parameter data. */ public static ResourceRequest createResourceRequest(String encodedPolicy, String encryptionKeyId, String signature) { ResourceRequest resourceRequest = new ResourceRequest(); resourceRequest.setEncodedPolicy(encodedPolicy); resourceRequest.setEncryptionKeyId(encryptionKeyId); resourceRequest.setSignature(signature); return resourceRequest; } /** * Transform a {@link Policy} into a {@link ResourceRequest} query string. * * @param policy * The {@link Policy} to use in the {@link ResourceRequest} * @param encryptionKeyId * The id of the encryption key. * @param encryptionKey * The actual encryption key. * @return A query string created from the policy. * @throws Exception * Thrown if there is a problem encoding or encrypting the policy. */ public static String policyToResourceRequestQueryString(Policy policy, String encryptionKeyId, String encryptionKey) throws Exception { ResourceRequest resourceRequest = new ResourceRequest(); resourceRequest.setEncodedPolicy(PolicyUtils.toBase64EncodedPolicy(policy)); resourceRequest.setEncryptionKeyId(encryptionKeyId); resourceRequest.setSignature(PolicyUtils.getPolicySignature(policy, encryptionKey)); return resourceRequestToQueryString(resourceRequest); } /** * Transform a {@link ResourceRequest} into a query string. * * @param resourceRequest * The {@link ResourceRequest} to transform. * @return The query string version of the {@link ResourceRequest} */ public static String resourceRequestToQueryString(ResourceRequest resourceRequest) { List<NameValuePair> queryStringParameters = new ArrayList<NameValuePair>(); queryStringParameters.add(new BasicNameValuePair(ResourceRequest.POLICY_KEY, resourceRequest.getEncodedPolicy())); queryStringParameters.add(new BasicNameValuePair(ResourceRequest.ENCRYPTION_ID_KEY, resourceRequest .getEncryptionKeyId())); queryStringParameters.add(new BasicNameValuePair(ResourceRequest.SIGNATURE_KEY, resourceRequest.getSignature())); return URLEncodedUtils.format(queryStringParameters, StandardCharsets.UTF_8); } /** * @param queryString * The query string for this request to determine its validity. * @param clientIp * The IP of the client requesting the resource. * @param resourceUri * The base uri for the resource. * @param encryptionKeys * The available encryption key ids and their keys. * @param strict * If false it will only compare the path to the resource instead of the entire URL including scheme, * hostname, port etc. * @return ResourceRequest */ public static ResourceRequest resourceRequestFromQueryString(String queryString, String clientIp, String resourceUri, Properties encryptionKeys, boolean strict) { ResourceRequest resourceRequest = new ResourceRequest(); List<NameValuePair> queryParameters = parseQueryString(queryString); if (!getQueryStringParameters(resourceRequest, queryParameters)) { return resourceRequest; } // Get the encryption key by its id. String encryptionKey = encryptionKeys.getProperty(resourceRequest.getEncryptionKeyId()); if (StringUtils.isBlank(encryptionKey)) { resourceRequest.setStatus(Status.Forbidden); resourceRequest .setRejectionReason(String.format("Forbidden because unable to find an encryption key with ID '%s'.", resourceRequest.getEncryptionKeyId())); return resourceRequest; } // Get Policy Policy policy = PolicyUtils.fromBase64EncodedPolicy(resourceRequest.getEncodedPolicy()); resourceRequest.setPolicy(policy); // Check to make sure that the Policy & Signature match when encrypted using the private key. If they don't match // return a Forbidden 403. if (!policyMatchesSignature(policy, resourceRequest.getSignature(), encryptionKey)) { resourceRequest.setStatus(Status.Forbidden); try { String policySignature = PolicyUtils.getPolicySignature(policy, encryptionKey); resourceRequest .setRejectionReason(String .format("Forbidden because policy and signature do not match. Policy: '%s' created Signature from this policy '%s' and query string Signature: '%s'.", PolicyUtils.toJson(resourceRequest.getPolicy()).toJSONString(), policySignature, resourceRequest.getSignature())); } catch (Exception e) { resourceRequest .setRejectionReason(String .format("Forbidden because policy and signature do not match. Policy: '%s' and query string Signature: '%s'. Unable to sign policy because: %s", PolicyUtils.toJson(resourceRequest.getPolicy()).toJSONString(), resourceRequest.getSignature(), ExceptionUtils.getStackTrace(e))); } return resourceRequest; } // If the IP address is specified, check it against the requestor's ip, if it doesn't match return a Forbidden 403. if (policy.getClientIpAddress().isPresent() && !policy.getClientIpAddress().get().getHostAddress().equalsIgnoreCase(clientIp)) { resourceRequest.setStatus(Status.Forbidden); resourceRequest.setRejectionReason(String.format( "Forbidden because client trying to access the resource '%s' doesn't match the policy client '%s'", clientIp, resourceRequest.getPolicy().getClientIpAddress())); return resourceRequest; } // If the resource value in the policy doesn't match the requested resource return a Forbidden 403. if (strict && !policy.getResource().equals(resourceUri)) { resourceRequest.setStatus(Status.Forbidden); resourceRequest.setRejectionReason(String.format( "Forbidden because resource trying to be accessed '%s' doesn't match policy resource '%s'", resourceUri, resourceRequest.getPolicy().getBaseUrl())); return resourceRequest; } else if (!strict) { try { String requestedPath = new URI(resourceUri).getPath(); String policyPath = new URI(policy.getResource()).getPath(); if (!policyPath.equals(requestedPath)) { resourceRequest.setStatus(Status.Forbidden); resourceRequest.setRejectionReason(String.format( "Forbidden because resource trying to be accessed '%s' doesn't match policy resource '%s'", resourceUri, resourceRequest.getPolicy().getBaseUrl())); return resourceRequest; } } catch (URISyntaxException e) { resourceRequest.setStatus(Status.Forbidden); resourceRequest .setRejectionReason(String .format("Forbidden because either the policy or requested URI cannot be parsed. Policy Path: '%s' and Request Path: '%s'. Unable to sign policy because: %s", policy.getResource(), resourceUri, ExceptionUtils.getStackTrace(e))); return resourceRequest; } } // Check the dates of the policy to make sure that it is still valid. If it is no longer valid give an Gone return // value of 410. if (new DateTime(DateTimeZone.UTC).isAfter(policy.getValidUntil().getMillis())) { resourceRequest.setStatus(Status.Gone); resourceRequest.setRejectionReason( String.format("The resource is gone because now '%s' is after the expiry time of '%s'", humanReadableFormat.print(new DateTime(DateTimeZone.UTC)), humanReadableFormat.print(new DateTime(policy.getValidUntil().getMillis(), DateTimeZone.UTC)))); return resourceRequest; } if (policy.getValidFrom().isPresent() && new DateTime(DateTimeZone.UTC).isBefore(policy.getValidFrom().get().getMillis())) { resourceRequest.setStatus(Status.Gone); resourceRequest.setRejectionReason( String.format("The resource is gone because now '%s' is before the available time of ", humanReadableFormat.print(new DateTime(DateTimeZone.UTC)), humanReadableFormat.print(policy.getValidFrom().get()))); return resourceRequest; } // If all of the above conditions pass, then allow the video to be played. resourceRequest.setStatus(Status.Ok); return resourceRequest; } /** * Check to see if a {@link URI} has not been signed already. * * @param uri * The {@link URI} to check to see if it was not signed. * @return True if not signed, false if signed. */ public static boolean isNotSigned(URI uri) { return !isSigned(uri); } /** * Check to see if a {@link URI} has been signed already. * * @param uri * The {@link URI} to check to see if it was signed. * @return True if signed, false if not. */ public static boolean isSigned(URI uri) { List<NameValuePair> queryStringParameters = URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.toString()); boolean hasKeyId = false; boolean hasPolicy = false; boolean hasSignature = false; for (NameValuePair parameter : queryStringParameters) { if (parameter.getName().equals(ResourceRequest.ENCRYPTION_ID_KEY)) { hasKeyId = true; } else if (parameter.getName().equals(ResourceRequest.POLICY_KEY)) { hasPolicy = true; } else if (parameter.getName().equals(ResourceRequest.SIGNATURE_KEY)) { hasSignature = true; } } return hasKeyId && hasPolicy && hasSignature; } }