elentras/dm_cloud

View on GitHub
lib/dm_cloud/signing.rb

Summary

Maintainability
A
45 mins
Test Coverage
require 'rubygems'

module DmCloud
  class Signing
    # Generate auth token for request from Media
    # Params:
    #   request: A hash of params generated from Media methods and Media::MetaData
    # Result :
    #   return a string which contain the auth token for the request
    #   <url>?auth=<expires>-<sec>-<nonce>-<md5sum>[-<pub-sec-data>]
    def self.identify(request)
      user_id  = DmCloud.config[:user_key]
      api_key  = DmCloud.config[:secret_key]

      normalized_request = normalize(request).to_s
      # puts 'identify:: normalized_values : ' + normalized_request + "\n" + '-' * 80

      params = user_id + normalized_request + api_key

      # puts 'identify:: Values before MD5 encrypt  : ' + params + "\n" + '-' * 80

      checksum = Digest::MD5.hexdigest(params)
      auth_token = user_id + ':' + checksum

      auth_token
    end
    
    # To sign a URL, the client needs a secret shared with Dailymotion Cloud.
    # This secret is call client secret and is available in the back-office interface.
    # Params:
    #   expires: An expiration timestamp.
    #   sec-level: A security level mask.
    #   url-no-query: The URL without the query-string.
    #   nonce: A 8 characters-long random alphanumeric lowercase string to make the signature unique.
    #   secret: The client secret.
    #   sec-data: If sec-level doesn’t have the DELEGATED bit activated,
    #     this component contains concatenated informations 
    #     for all activated sec levels.
    #   pub-sec-data: Some sec level data have to be passed in clear in the signature.
    #     To generate this component the parameters are serialized using x-www-form-urlencoded, compressed with gzip and encoded in base64.
    # Result :
    #   return a string which contain the signed url like 
    #   <expires>-<sec>-<nonce>-<md5sum>[-<pub-sec-data>]
    def self.sign(stream, security_datas = nil)
      raise StandardError, "missing :stream in params" unless stream
      sec_level = security(DmCloud.config[:security_level])
      sec_data = security_data(DmCloud.config[:security_level], security_datas) unless security_datas.nil?

      base = { 
        :sec_level => sec_level,
        :url_no_query => stream,
        :expires => 1.hours.from_now.to_i,
        :nonce => SecureRandom.hex(16)[0,16],
        :secret => DmCloud.config[:secret_key]
      }
      base.merge!(:sec_data => sec_data, :pub_sec_data => sec_data) unless sec_data.nil?

      digest_struct = build_digest_struct(base)
      check_sum = Digest::MD5.hexdigest(digest_struct)

      signed_url = [base[:expires], base[:sec_level], base[:nonce], check_sum].compact
      signed_url.merge!(:pub_sec_data => sec_data) unless sec_data.nil?
      
      # puts signed_url
      
      signed_url = signed_url.join('-')
      signed_url
    end
    
    # Prepare datas for signing
    # Params :
    #   base : contains media id and others for url signing
    def self.build_digest_struct(base)
      result = []
      base.each_pair { |key, value| result << value }
      result.join('')
    end

    # The client must choose a security level for the signature.
    # Security level defines the mechanism used by Dailymotion Cloud architecture
    # to ensure the signed URL will be used by a single end-user.
    # Params :
    #   type :
    #     None: The signed URL will be valid for everyone
    #     ASNUM: The signed URL will only be valid for the AS of the end-user.
    #       The ASNUM (for Autonomous System Number) stands for the network identification,
    #       each ISP have a different ASNUM for instance.
    #     IP: The signed URL will only be valid for the IP of the end-user.
    #       This security level may wrongly block some users
    #       which have their internet access load-balanced between several proxies.
    #       This is the case in some office network or some ISPs.
    #     User-Agent: Used in addition to one of the two former levels, 
    #       this level a limit on the exact user-agent of the end-user.
    #       This is more secure but in some specific condition may lead to wrongly blocked users.
    #     Use Once: The signed URL will only be usable once.
    #       Note: should not be used with stream URLs.
    #     Country: The URL can only be queried from specified countrie(s).
    #       The rule can be reversed to allow all countries except some.
    #     Referer: The URL can only be queried 
    #       if the Referer HTTP header contains a specified value.
    #       If the URL contains a Referer header with a different value,
    #       the request is refused. If the Referer header is missing,
    #       the request is accepted in order to prevent from false positives as some browsers, 
    #       anti-virus or enterprise proxies may remove this header.
    #     Delegate: This option instructs the signing algorithm 
    #       that security level information won’t be embeded into the signature
    #       but gathered and lock at the first use.
    # Result :
    #   Return a string which contain the signed url like 
    #   http://cdn.DmCloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
    def self.security(type = nil)
      type = :none unless type
      type = type.to_sym if type.class == String
      
      case type
        when :none
          0 # None
        when :delegate
          1 << 0  # None
        when :asnum
          1 << 1  # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
        when :ip
          1 << 2  # The end-user quad dotted IP address (ie: ip=195.8.215.138)
        when :user_agent
          1 << 3  # The end-user browser user-agent (parameter name is ua)
        when :use_once
          1 << 4  # None
        when :country
          1 << 5  # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
        when :referer
          1 << 6  # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
      end
    end

    def self.security_data(type, value = nil)
      type = type.to_sym if type.class == String

      case type
        when :asnum
          "as=#{value}"  # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
        when :ip
          "ip=#{value}"  # The end-user quad dotted IP address (ie: ip=195.8.215.138)
        when :user_agent
          "ua=#{value}"  # The end-user browser user-agent (parameter name is ua)
        when :country
          "cc=#{value}"  # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
        when :referer
          "rf=#{value}"  # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
        else
          nil
      end
    end

    def self.security_pub_sec_data(type, value)
      type = type.to_sym if type.class == String
      
      case type
        when :country
          "cc=#{value}"  # A list of 2 characters long country codes in lowercase by comas. If the list starts with a dash, the rule is inverted (ie: cc=fr,gb,de or cc=-fr,it). This data have to be stored in pub-sec-data component
        when :referer
          "rf=#{value}"  # A list of URL prefixes separated by spaces stored in the pub-sec-data component (ex: rf=http;//domain.com/a/+http:/domain.com/b/).
        else
          nil
      end
    end
    
    
    # This block comes from Cloudkey gem.
    # I discovered this gem far after I start this one 
    # and I will try to add file upload from http or ftp.
    # (Missing in their gem)
    def self.normalize params
          case params
          when Array
            params.collect { |element| normalize(element) }.join('')
          when Hash
            params.to_a.sort_by {|a,b| a.to_s }.collect {|array| array.first.to_s + normalize(array.last)}.join('')
          else
            params.to_s
          end
        end
    
    # def self.normalize(params)
    #       str = params.to_json.to_s
    #       str.gsub!(/[^A-Za-z0-9]/, '')
    #       str
    #     end
  end
end