dmcloud 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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