WowzaUrlSigningProvider.java

/*
 * 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.api.SecurityService;
import org.opencastproject.security.urlsigning.WowzaResourceStrategyImpl;
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.apache.commons.lang3.exception.ExceptionUtils;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

@Component(
    immediate = true,
    service = { ManagedService.class, UrlSigningProvider.class },
    property = {
        "service.description=Wowza Url Signing Provider",
        "service.pid=org.opencastproject.security.urlsigning.provider.impl.WowzaUrlSigningProvider"
    }
)
public class WowzaUrlSigningProvider extends AbstractUrlSigningProvider {

  private static final Logger logger = LoggerFactory.getLogger(WowzaUrlSigningProvider.class);

  /** The Wowza resource strategy to use to convert from the base url to a resource url. */
  private ResourceStrategy resourceStrategy = new WowzaResourceStrategyImpl();

  @Override
  public Logger getLogger() {
    return logger;
  }

  @Override
  public ResourceStrategy getResourceStrategy() {
    return resourceStrategy;
  }

  @Override
  public String toString() {
    return "Wowza URL Signing Provider";
  }

  /**
   * @param policy
   *             the policy
   * @return the signed url
   */
  @Override
  public String sign(Policy policy) throws UrlSigningException {
    if (!accepts(policy.getBaseUrl())) {
      throw UrlSigningException.urlNotSupported();
    }

    try {
      URI uri = new URI(policy.getBaseUrl());

      /*
        For backward compatibility, but i can not see how this could work.
        According to the documentation 
        "https://www.wowza.com/docs/how-to-protect-streaming-using-securetoken-in-wowza-streaming-engine" 
        if you using token v1 we need a TEA implimentation.
      */
      if ("rtmp".equals((uri.getScheme()))) {
        return super.sign(policy);
      }

        // Get the key that matches this URI since there must be one that matches as the base url has been accepted.
      Key key = getKey(policy.getBaseUrl());

      policy.setResourceStrategy(getResourceStrategy());

      if (!key.getSecret().contains("@")) {
        getLogger().error("Given key not valid. (prefix@secret)");

        throw new Exception("Given key not valid. (prefix@secret)");
      }
      String[] wowzaKeyPair = key.getSecret().split("@");
      String wowzaPrefix = wowzaKeyPair[0];
      String wowzaSecret = wowzaKeyPair[1];

      String newUri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(),
          addSignutureToRequest(policy, wowzaPrefix, wowzaSecret), null).toString();
      return newUri;
    } catch (Exception e) {
      getLogger().error("Unable to create signed URL because {}", ExceptionUtils.getStackTrace(e));
      throw new UrlSigningException(e);
    }
  }

  /**
   * @param policy
   *             the policy
   * @param encryptionKeyId
   *             the encription key id
   * @param encryptionKey
   *             the encription key
   * @return true
   *             if the url is excluded, false otherwise
   * @exception Exception
   *             if something goes bad
   */
  private String addSignutureToRequest(Policy policy, String encryptionKeyId, String encryptionKey) throws Exception  {
    final String startTime;
    final String endTime = Long.toString(policy.getValidUntil().getMillis() / 1000);
    final String ip;

    String baseUrl = policy.getBaseUrl();
    URI url = new URI(policy.getBaseUrl());
    String resource = policy.getResource();
    if (policy.getClientIpAddress().isPresent()) {
      // The ip comes with a slash: /192.168.1.2
      String ipAux = policy.getClientIpAddress().get().toString();
      ipAux = ipAux.substring(1, ipAux.length());
      ip = ipAux;
    } else {
      ip = "";
    }

    if (policy.getValidFrom().isPresent()) {
      startTime = Long.toString(policy.getValidFrom().get().getMillis() / 1000);
    } else {
      startTime = "";
    }

    String queryStringParameters = new String();

    queryStringParameters += encryptionKeyId + "endtime=" + endTime;

    if (!"".equals(startTime)) {
      queryStringParameters += "&" + encryptionKeyId + "starttime=" + startTime;
    }

    if (url.getQuery() != null) {
      String query = url.getQuery();
      String[] params = query.split("&");
      for (String param : params) {
        if (param.contains("=")) {
          String[] keyValue = param.split("=");
          queryStringParameters += "&" + encryptionKeyId + keyValue[0] + "=" + keyValue[1];
        }
      }
    }

    queryStringParameters += "&" + encryptionKeyId + "hash=" + generateHash(baseUrl,
        resource, ip, encryptionKeyId, encryptionKey, startTime, endTime);

    return queryStringParameters;
  }

  /**
   * @param baseUrl
   *             the base url
   * @param resource
   *             the resource
   * @param ip
   *             the ip
   * @param encryptionKeyId
   *             the encription key id
   * @param encryptionKey
   *             the encription key
   * @param startTime
   *             start time
   * @param endTime
   *             end time
   * @return the generated hashed
   * @exception Exception
   *             if something goes bad
   */
  private String generateHash(String baseUrl, String resource, String ip,
      String encryptionKeyId, String encryptionKey, String startTime, String endTime) throws Exception {
    String urlToHash = resource + "?";

    if (!"".equals(ip)) {
      urlToHash += ip + "&" + encryptionKey;
    } else {
      urlToHash += encryptionKey;
    }

    SortedMap<String, String> arguments = new TreeMap<>();

    arguments.put(encryptionKeyId + "endtime", endTime);

    if (!"".equals(startTime)) {
      arguments.put(encryptionKeyId + "starttime", startTime);
    }

    String query = new URI(baseUrl).getQuery();
    if (null == query) {
      query = "";
    }

    String[] params = query.split("&");
    for (String param : params) {
      if (param.contains("=")) {
        String[] keyValue = param.split("=");
        arguments.put(encryptionKeyId + keyValue[0], keyValue[1]);
      }
    }

    for (Map.Entry<String,String> entry : arguments.entrySet()) {
      String value = entry.getValue();
      String key = entry.getKey();
      urlToHash += "&" + key + "=" + value;
    }

    MessageDigest md = MessageDigest.getInstance("SHA-256");
    byte[] messageDigest = md.digest(urlToHash.getBytes());
    String base64Hash = Base64.getEncoder().encodeToString(messageDigest);

    base64Hash = base64Hash.replaceAll("\\+", "-");
    base64Hash = base64Hash.replaceAll("/", "_");

    return base64Hash;
  }

  @Reference
  @Override
  public void setSecurityService(SecurityService securityService) {
    super.setSecurityService(securityService);
  }

}