dmcloud 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ require 'rubygems'
2
+
3
+ module DmCloud
4
+ class Signing
5
+ # Generate auth token for request from Media
6
+ # Params:
7
+ # request: A hash of params generated from Media methods and Media::MetaData
8
+ # Result :
9
+ # return a string which contain the auth token for the request
10
+ # <url>?auth=<expires>-<sec>-<nonce>-<md5sum>[-<pub-sec-data>]
11
+ def self.identify(request)
12
+ user_id = DmCloud.config[:user_key]
13
+ api_key = DmCloud.config[:secret_key]
14
+
15
+ normalized_request = normalize(request).to_s
16
+ # puts 'identify:: normalized_values : ' + normalized_request + "\n" + '-' * 80
17
+
18
+ params = user_id + normalized_request + api_key
19
+
20
+ # puts 'identify:: Values before MD5 encrypt : ' + params + "\n" + '-' * 80
21
+
22
+ checksum = Digest::MD5.hexdigest(params)
23
+ auth_token = user_id + ':' + checksum
24
+
25
+ auth_token
26
+ end
27
+
28
+ # To sign a URL, the client needs a secret shared with Dailymotion Cloud.
29
+ # This secret is call client secret and is available in the back-office interface.
30
+ # Params:
31
+ # expires: An expiration timestamp.
32
+ # sec-level: A security level mask.
33
+ # url-no-query: The URL without the query-string.
34
+ # nonce: A 8 characters-long random alphanumeric lowercase string to make the signature unique.
35
+ # secret: The client secret.
36
+ # sec-data: If sec-level doesn’t have the DELEGATED bit activated,
37
+ # this component contains concatenated informations
38
+ # for all activated sec levels.
39
+ # pub-sec-data: Some sec level data have to be passed in clear in the signature.
40
+ # To generate this component the parameters are serialized using x-www-form-urlencoded, compressed with gzip and encoded in base64.
41
+ # Result :
42
+ # return a string which contain the signed url like
43
+ # <expires>-<sec>-<nonce>-<md5sum>[-<pub-sec-data>]
44
+ def self.sign(stream, security_datas = nil)
45
+ raise StandardError, "missing :stream in params" unless stream
46
+ sec_level = security(DmCloud.config[:security_level])
47
+ sec_data = security_data(DmCloud.config[:security_level], security_datas) unless security_datas.nil?
48
+
49
+ base = {
50
+ :sec_level => sec_level,
51
+ :url_no_query => stream,
52
+ :expires => 1.hours.from_now.to_i,
53
+ :nonce => SecureRandom.hex(16)[0,16],
54
+ :secret => DmCloud.config[:secret_key]
55
+ }
56
+ base.merge!(:sec_data => sec_data, :pub_sec_data => sec_data) unless sec_data.nil?
57
+
58
+ digest_struct = build_digest_struct(base)
59
+ check_sum = Digest::MD5.hexdigest(digest_struct)
60
+
61
+ signed_url = [base[:expires], base[:sec_level], base[:nonce], check_sum].compact
62
+ signed_url.merge!(:pub_sec_data => sec_data) unless sec_data.nil?
63
+
64
+ # puts signed_url
65
+
66
+ signed_url = signed_url.join('-')
67
+ signed_url
68
+ end
69
+
70
+ # Prepare datas for signing
71
+ # Params :
72
+ # base : contains media id and others for url signing
73
+ def self.build_digest_struct(base)
74
+ result = []
75
+ base.each_pair { |key, value| result << value }
76
+ result.join('')
77
+ end
78
+
79
+ # The client must choose a security level for the signature.
80
+ # Security level defines the mechanism used by Dailymotion Cloud architecture
81
+ # to ensure the signed URL will be used by a single end-user.
82
+ # Params :
83
+ # type :
84
+ # None: The signed URL will be valid for everyone
85
+ # ASNUM: The signed URL will only be valid for the AS of the end-user.
86
+ # The ASNUM (for Autonomous System Number) stands for the network identification,
87
+ # each ISP have a different ASNUM for instance.
88
+ # IP: The signed URL will only be valid for the IP of the end-user.
89
+ # This security level may wrongly block some users
90
+ # which have their internet access load-balanced between several proxies.
91
+ # This is the case in some office network or some ISPs.
92
+ # User-Agent: Used in addition to one of the two former levels,
93
+ # this level a limit on the exact user-agent of the end-user.
94
+ # This is more secure but in some specific condition may lead to wrongly blocked users.
95
+ # Use Once: The signed URL will only be usable once.
96
+ # Note: should not be used with stream URLs.
97
+ # Country: The URL can only be queried from specified countrie(s).
98
+ # The rule can be reversed to allow all countries except some.
99
+ # Referer: The URL can only be queried
100
+ # if the Referer HTTP header contains a specified value.
101
+ # If the URL contains a Referer header with a different value,
102
+ # the request is refused. If the Referer header is missing,
103
+ # the request is accepted in order to prevent from false positives as some browsers,
104
+ # anti-virus or enterprise proxies may remove this header.
105
+ # Delegate: This option instructs the signing algorithm
106
+ # that security level information won’t be embeded into the signature
107
+ # but gathered and lock at the first use.
108
+ # Result :
109
+ # Return a string which contain the signed url like
110
+ # http://cdn.DmCloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
111
+ def self.security(type = nil)
112
+ type = :none unless type
113
+ type = type.to_sym if type.class == String
114
+
115
+ case type
116
+ when :none
117
+ 0 # None
118
+ when :delegate
119
+ 1 << 0 # None
120
+ when :asnum
121
+ 1 << 1 # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
122
+ when :ip
123
+ 1 << 2 # The end-user quad dotted IP address (ie: ip=195.8.215.138)
124
+ when :user_agent
125
+ 1 << 3 # The end-user browser user-agent (parameter name is ua)
126
+ when :use_once
127
+ 1 << 4 # None
128
+ when :country
129
+ 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
130
+ when :referer
131
+ 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/).
132
+ end
133
+ end
134
+
135
+ def self.security_data(type, value = nil)
136
+ type = type.to_sym if type.class == String
137
+
138
+ case type
139
+ when :asnum
140
+ "as=#{value}" # The number part of the end-user AS prefixed by the ‘AS’ string (ie: as=AS41690)
141
+ when :ip
142
+ "ip=#{value}" # The end-user quad dotted IP address (ie: ip=195.8.215.138)
143
+ when :user_agent
144
+ "ua=#{value}" # The end-user browser user-agent (parameter name is ua)
145
+ when :country
146
+ "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
147
+ when :referer
148
+ "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/).
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
154
+ def self.security_pub_sec_data(type, value)
155
+ type = type.to_sym if type.class == String
156
+
157
+ case type
158
+ when :country
159
+ "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
160
+ when :referer
161
+ "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/).
162
+ else
163
+ nil
164
+ end
165
+ end
166
+
167
+
168
+ # This block comes from Cloudkey gem.
169
+ # I discovered this gem far after I start this one
170
+ # and I will try to add file upload from http or ftp.
171
+ # (Missing in their gem)
172
+ def self.normalize params
173
+ case params
174
+ when Array
175
+ params.collect { |element| normalize(element) }.join('')
176
+ when Hash
177
+ params.to_a.sort_by {|a,b| a.to_s }.collect {|array| array.first.to_s + normalize(array.last)}.join('')
178
+ else
179
+ params.to_s
180
+ end
181
+ end
182
+
183
+ # def self.normalize(params)
184
+ # str = params.to_json.to_s
185
+ # str.gsub!(/[^A-Za-z0-9]/, '')
186
+ # str
187
+ # end
188
+ end
189
+ end
@@ -0,0 +1,80 @@
1
+ require "time"
2
+ require "openssl"
3
+ require "base64"
4
+ require 'digest/md5'
5
+
6
+ # This module generate methods to generate video's fluxes
7
+ # before signing it and request it.
8
+ module DmCloud
9
+ class Streaming
10
+ # Default URL to get embed content ou direct url
11
+ DIRECT_STREAM = "[PROTOCOL]://cdn.dmcloud.net/route/[USER_ID]/[MEDIA_ID]/[ASSET_NAME].[ASSET_EXTENSION]".freeze
12
+ EMBED_STREAM = "[PROTOCOL]://api.dmcloud.net/embed/[USER_ID]/[MEDIA_ID]".freeze
13
+ EMBED_IFRAME = '<iframe width="[WIDTH]" height="[HEIGHT]" frameborder="0" scrolling="no" src="[EMBED_URL]"></iframe>'.freeze
14
+
15
+ # Get embeded player
16
+ # Params :
17
+ # media_id: this is the id of the media (eg: 4c922386dede830447000009)
18
+ # options:
19
+ # skin_id: (optional) the id of the custom skin for the video player
20
+ # width: (optional) the width for the video player frame
21
+ # height: (optional) the height for the video player frame
22
+ # Result :
23
+ # return a string which contain the signed url like
24
+ # <iframe width="848" height="480" frameborder="0" scrolling="no" src="http://api.DmCloud.net/embed/<user_id>/<media_id>?auth=<auth_token>&skin=<skin_id>"></iframe>
25
+ def self.embed(media_id, options = {}, security = {})
26
+ raise StandardError, "missing :media_id in params" unless media_id
27
+
28
+ skin_id = options[:skin_id] ? options[:skin_id] : 'modern1'
29
+ width = options[:width] ? options[:width].to_s : '848'
30
+ height = options[:height] ? options[:height].to_s : '480'
31
+
32
+ stream = EMBED_STREAM.dup
33
+ stream.gsub!('[PROTOCOL]', DmCloud.config[:protocol])
34
+ stream.gsub!('[USER_ID]', DmCloud.config[:user_key])
35
+ stream.gsub!('[MEDIA_ID]', media_id)
36
+ signed_url = DmCloud::Signing.sign(stream, security)
37
+ signed_url = stream + "?auth=#{signed_url}"
38
+
39
+ frame = EMBED_IFRAME.dup
40
+ frame.gsub!('[WIDTH]', width)
41
+ frame.gsub!('[HEIGHT]', height)
42
+ frame.gsub!('[EMBED_URL]', signed_url)
43
+
44
+ frame.html_safe
45
+ end
46
+
47
+ # Get media url for direct link to the file on DailyMotion Cloud
48
+ # Params :
49
+ # media_id: this is the id of the media (eg: 4c922386dede830447000009)
50
+ # asset_name: the name of the asset you want to stream (eg: mp4_h264_aac)
51
+ # asset_extension: the extension of the asset, most of the time it is the first part of the asset name (eg: mp4)
52
+ # Result :
53
+ # return a string which contain the signed url like
54
+ # http://cdn.DmCloud.net/route/<user_id>/<media_id>/<asset_name>.<asset_extension>?auth=<auth_token>
55
+ def self.url(media_id, asset_name, asset_extension = nil, security = {})
56
+ raise StandardError, "missing :media_id in params" unless media_id
57
+ raise StandardError, "missing :asset_name in params" unless asset_name
58
+ asset_extension = asset_name.split('_').first unless asset_extension
59
+
60
+ stream = DIRECT_STREAM.dup
61
+ stream.gsub!('[PROTOCOL]', DmCloud.config[:protocol])
62
+ stream.gsub!('[USER_ID]', DmCloud.config[:user_key])
63
+ stream.gsub!('[MEDIA_ID]', media_id)
64
+ stream.gsub!('[ASSET_NAME]', asset_name)
65
+ stream.gsub!('[ASSET_EXTENSION]', asset_extension)
66
+
67
+ stream += '?auth=[AUTH_TOKEN]'.gsub!('[AUTH_TOKEN]', DmCloud::Signing.sign(stream, security))
68
+ stream
69
+ end
70
+
71
+ # Gets the real URL that points to the download link on DMCloud's specific server
72
+ def self.download_url(media_id, asset_name, asset_extension = nil, security = {})
73
+ download_url = self.url(media_id, asset_name, asset_extension, security)
74
+ response = Net::HTTP.get_response(URI.parse(download_url))
75
+ download_url = response.header["location"]
76
+
77
+ download_url
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module DmCloud
2
+ VERSION = "0.0.65"
3
+ end
data/lib/dm_cloud.rb ADDED
@@ -0,0 +1,58 @@
1
+ require 'yaml'
2
+
3
+ # This gem's comments come from DailyMotion Cloud API,
4
+ # that's the better way to see changes on new version and logic.
5
+ # For parts more generals and not representating DailyMotion Cloud API,
6
+ # I add some about my own opinion.
7
+ module DmCloud
8
+
9
+ # Configuration defaults
10
+ # I used this parts from Slainer68 paybox_system gem.
11
+ # I liked the concept and how he handle this part.
12
+ # Thx Slainer68, I created my first gem,
13
+ # and next one will be an update to your paybox_system gem.
14
+ @@config = {
15
+ security_level: 'none',
16
+ protocol: 'http',
17
+ auto_call: true,
18
+ user_key: nil,
19
+ secret_key: nil
20
+ }
21
+
22
+ YAML_INITIALIZER_PATH = File.dirname(__FILE__)
23
+ @valid_config_keys = @@config.keys
24
+
25
+ # Configure through hash
26
+ def self.configure(opts = {})
27
+ opts.each {|k,v| @@config[k.to_sym] = v } # if @valid_config_keys.include? k.to_sym}
28
+ end
29
+
30
+ # Configure through yaml file
31
+ # for ruby scripting usage
32
+ def self.configure_with(yaml_file_path = nil)
33
+ yaml_file_path = YAML_INITIALIZER_PATH unless yaml_file_path
34
+ begin
35
+ config = YAML::load(IO.read(path_to_yaml_file))
36
+ rescue Errno::ENOENT
37
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
38
+ rescue Psych::SyntaxError
39
+ log(:warning, "YAML configuration file contains invalid syntax. Using defaults."); return
40
+ end
41
+
42
+ configure(config)
43
+ end
44
+
45
+ # Access to config variables (security level, user_id and api_key)
46
+ def self.config
47
+ @@config = configure unless @@config
48
+ @@config
49
+ end
50
+
51
+ # Loading classes to easier access
52
+ # NOTE: I like this way to handle my classes,
53
+ # sexiest than using require 'my_class_file' everywhere
54
+ autoload(:Streaming, 'dm_cloud/streaming')
55
+ autoload(:Media, 'dm_cloud/media')
56
+ autoload(:Request, 'dm_cloud/request')
57
+ autoload(:Signing, 'dm_cloud/signing')
58
+ end
@@ -0,0 +1,48 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ require 'dm_cloud/media'
4
+
5
+ describe DmCloud::Media do
6
+ use_vcr_cassette
7
+
8
+ context "Using test account" do
9
+ before :each do
10
+ DmCloud.configure({:user_key => TEST_USER_KEY, :secret_key => TEST_SECRET_KEY, auto_call: false})
11
+ end
12
+
13
+ context "Having a collection" do
14
+ it "should list four medias" do
15
+ subject { list(:per_page => 20)['result']['total'].should == (4) }
16
+ end
17
+
18
+ it "should get all the titles" do
19
+ result = DmCloud::Media.list(:fields => {:meta => [:title]}, :per_page => 20) #['result']['list']
20
+ result[:call].should == "media.list"
21
+ result[:params][:args][:fields].should include("meta.title")
22
+ end
23
+
24
+ it "should get page 2 with 2 records per page" do
25
+ result = DmCloud::Media.list(:per_page => 2, :page => 2)
26
+ result[:params][:page].should == 2
27
+ result[:params][:per_page].should == 2
28
+ end
29
+ end
30
+
31
+ context "Querying a single media" do
32
+ it "should have an default fields" do
33
+ result = DmCloud::Media.info('4f33ddbc94a6f6517c001577')
34
+ fields = result[:params][:args][:fields]
35
+ puts fields.to_yaml
36
+ fields.should include('embed_url')
37
+ end
38
+
39
+ # it "should have a stream url" do
40
+ # subject { stream_url('4f33ddbc94a6f6517c001577').should include("http://cdn.DmCloud.net/route/4f33d9c8f325e11c830016af/4f33ddbc94a6f6517c001577/mp4_h264_aac.mp4")
41
+ # end
42
+ #
43
+ # it "should have http detected as protocol" do
44
+ # @cloudkey.media.stream_url('4f33ddbc94a6f6517c001577', 'mp4_h264_aac',Cloudkey::SecurityPolicy.new, :download => true).should include("/http/")
45
+ # end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'dm_cloud/signing'
3
+
4
+ describe DmCloud::Signing do
5
+
6
+ before do
7
+ DmCloud.configure({ :user_key => "hello world", :secret_key => "sEcReT_KeY" })
8
+ end
9
+
10
+ it "should sign 'hello world' with sEcReT_KeY and returns 'b5d93121a6dc87562b46beb8ba809ace'" do
11
+ auth_token = subject { identify() }
12
+ auth_token.should == 'b5d93121a6dc87562b46beb8ba809ace'
13
+ end
14
+
15
+ it "it should sign an url" do
16
+ let(:signed_url) { stub(:sign).with("http://google.fr","olol") }
17
+ end
18
+
19
+ context "Normalizing" do
20
+ {
21
+ 'foo42bar' => ['foo', 42, 'bar'],
22
+ 'pink3red2yellow1' => {'yellow' => 1, 'red' => 2, 'pink' => 3},
23
+ 'foo42pink3red2yellow1bar' => ['foo', 42, {'yellow' => 1, 'red' => 2, 'pink' => 3}, 'bar'],
24
+ 'foo42pink3red2yellow1bar' => [:foo, 42, {:yellow => 1, :red => 2, :pink => 3}, :bar],
25
+ '12' => [nil, 1,2],
26
+ '' => nil,
27
+ '212345' => {2 => [nil, 1,2], 3 => nil, 4 => 5}
28
+ }.each do |normalized, original|
29
+ it "should normalize #{original.inspect} into #{normalized}" do
30
+ subject { normalize(original).should == normalized }
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'dm_cloud/streaming'
3
+
4
+
5
+ describe DmCloud::Streaming do
6
+ use_vcr_cassette
7
+
8
+ context "check " do
9
+ before :each do
10
+ DmCloud.configure({user_key: TEST_USER_KEY, secret_key: TEST_SECRET_KEY, auto_execute: false})
11
+ end
12
+
13
+ context "Querying a single media" do
14
+ it "should have an embedded url" do
15
+ DmCloud::Streaming.embed('4f33ddbc94a6f6517c001577').should include("http://api.DmCloud.net/embed/4f33d9c8f325e11c830016af/4f33ddbc94a6f6517c001577")
16
+ end
17
+
18
+ it "should have a stream url" do
19
+ DmCloud::Streaming.url('4f33ddbc94a6f6517c001577', 'source').should include("http://cdn.DmCloud.net/route/4f33d9c8f325e11c830016af/4f33ddbc94a6f6517c001577/mp4_h264_aac.mp4")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'dm_cloud'
3
+
4
+ describe DmCloud do
5
+ context "configuration" do
6
+ use_vcr_cassette
7
+
8
+ it "should provide a config on DmCloud" do
9
+ DmCloud.should respond_to :config
10
+ end
11
+
12
+ it "should be initialized with default values" do
13
+ DmCloud.configure
14
+ DmCloud.config[:security_level].should == 'none'
15
+ DmCloud.config[:protocol].should == 'http'
16
+ DmCloud.config[:auto_call].should be_true
17
+ end
18
+
19
+ context "after configuration setted" do
20
+ it "should be properly set" do
21
+ DmCloud.configure({user_key: TEST_USER_KEY, secret_key: TEST_SECRET_KEY, auto_call: false })
22
+ DmCloud.config[:user_key].should == TEST_USER_KEY
23
+ DmCloud.config[:secret_key].should == TEST_SECRET_KEY
24
+ DmCloud.config[:auto_call].should == false
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '/../', 'lib'))
3
+ require 'rubygems'
4
+ require 'rspec'
5
+ require 'rspec/autorun'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
+
11
+ # def self.behavior(obj)
12
+ # if @methods
13
+ # @methods = @methods.select{|met| @methods.member? met }
14
+ # else
15
+ # @methods = obj.public_methods
16
+ # end
17
+ # end
18
+ # puts "Common Methods: #{@methods.sort.join(', ')}" if @methods
19
+
20
+ module Compare
21
+ def self.type(obj)
22
+ @objects ||= []
23
+ @objects << obj
24
+ end
25
+
26
+ def self.report
27
+ puts "Object Types: #{@objects.collect{|o| o.class}.join(', ')}" if @objects
28
+ end
29
+ end
30
+
31
+ class Object
32
+ def put_methods(regex=/.*/)
33
+ puts self.methods.grep(regex)
34
+ end
35
+ end
36
+
37
+ RSpec.configure do |config|
38
+ config.after(:suite) do
39
+ Compare.report
40
+ end
41
+ end
42
+
43
+ # Hook on http requests
44
+ require 'vcr'
45
+
46
+ TEST_USER_KEY = "my_user_key"
47
+ TEST_SECRET_KEY = "my_secret_key"
48
+
49
+ VCR.configure do |c|
50
+ c.cassette_library_dir = 'spec/cassettes'
51
+ # c.stub_with :fakeweb
52
+ c.default_cassette_options = { :record => :new_episodes }
53
+ end
54
+
55
+ RSpec.configure do |c|
56
+ c.extend VCR::RSpec::Macros
57
+ end