diff --git a/.travis.yml b/.travis.yml index c7863ad..6b11d42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ rvm: - 2.1.1 - jruby-18mode - jruby-19mode - - jruby-head - ree before_script: - export QINIU_ACCESS_KEY=LGRqJsjX7LAvtdtX6hhyxs861lS57HwnOQmJsOuX diff --git a/CHANGELOG.md b/CHANGELOG.md index 41a0446..5935def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## CHANGE LOG +### v6.2.0 + +- 重写与授权相关的函数并归入Qiniu::Auth空间,原授权凭证生成类维持不变。 + +- 添加Qiniu::Storage::PutPolicy类和Qiniu::Storage#upload_with_put_policy方法,并推荐使用两者组合实现单文件上传。 + ### v6.1.0 - Qiniu::Storage所有上传接口返回第三个值raw_headers,类型为Hash,包含已解析的HTTP响应报文中的所有Header信息。 diff --git a/Gemfile.lock b/Gemfile.lock index e546604..9ce9541 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - qiniu (6.1.0) + qiniu (6.2.0) json (~> 1.7) mime-types (~> 1.19) rest-client (~> 1.6) diff --git a/lib/qiniu/auth.rb b/lib/qiniu/auth.rb index 1d3075c..5b2e531 100755 --- a/lib/qiniu/auth.rb +++ b/lib/qiniu/auth.rb @@ -1,17 +1,146 @@ # -*- encoding: utf-8 -*- +# vim: sw=2 ts=2 + +require 'hmac-sha1' +require 'uri' require 'qiniu/exceptions' module Qiniu module Auth + DEFAULT_AUTH_SECONDS = 3600 + + class << self + def calculate_deadline(expires_in, deadline = nil) + ### 授权期计算 + if expires_in.is_a?(Integer) && expires_in > 0 then + # 指定相对时间,单位:秒 + return Time.now.to_i + expires_in + elsif deadline.is_a?(Integer) then + # 指定绝对时间,常用于调试和单元测试 + return deadline + end + + # 默认授权期1小时 + return Time.now.to_i + DEFAULT_AUTH_SECONDS + end # calculate_deadline + end # class << self + + class PutPolicy + private + def initialize(bucket, + key = nil, + expires_in = DEFAULT_AUTH_SECONDS, + deadline = nil) + ### 设定scope参数(必填项目) + self.scope!(bucket, key) + + ### 设定deadline参数(必填项目) + @expires_in = expires_in + @deadline = Auth.calculate_deadline(expires_in, deadline) + end # initialize + + PARAMS = { + # 字符串类型参数 + :scope => "scope" , + :save_key => "saveKey" , + :end_user => "endUser" , + :return_url => "returnUrl" , + :return_body => "returnBody" , + :callback_url => "callbackUrl" , + :callback_body => "callbackBody" , + :persistent_ops => "persistentOps" , + :persistent_notify_url => "persistentNotifyUrl" , + :transform => "transform" , + + # 数值类型参数 + :deadline => "deadline" , + :insert_only => "insertOnly" , + :fsize_limit => "fsizeLimit" , + :detect_mime => "detectMime" , + :mime_limit => "mimeLimit" , + :fop_timeout => "fopTimeout" + } # PARAMS + + public + attr_reader :bucket, :key + + def scope!(bucket, key = nil) + @bucket = bucket + @key = key + + if key.nil? then + # 新增语义,文件已存在则失败 + @scope = bucket + else + # 覆盖语义,文件已存在则直接覆盖 + @scope = "#{bucket}:#{key}" + end + end # scope! + + def expires_in!(seconds) + if !seconds.nil? then + return @expires_in + end + + @epires_in = seconds + @deadline = Auth.calculate_deadline(seconds) + + return @expires_in + end # expires_in! + + def expires_in=(seconds) + return expires_in!(seconds) + end # expires_in= + + def expires_in + return @expires_in + end # expires_in + + def allow_mime_list! (list) + @mime_limit = list + end # allow_mime_list! + + def deny_mime_list! (list) + @mime_limit = "!#{list}" + end # deny_mime_list! + + def insert_only! + @insert_only = 1 + end # insert_only! + + def detect_mime! + @detect_mime = 1 + end # detect_mime! + + def to_json + args = {} + + PARAMS.each_pair do |key, fld| + val = self.__send__(key) + if !val.nil? then + args[fld] = val + end + end + + return args.to_json + end # to_json + + PARAMS.each_pair do |key, fld| + attr_accessor key + end + end # class PutPolicy class << self include Utils def call_with_signature(url, data, retry_times = 0, options = {}) - code, data, raw_headers = http_request url, data, options.merge({:qbox_signature_token => generate_qbox_signature(url, data, options[:mime])}) - [code, data, raw_headers] + return Utils.http_request( + url, + data, + options.merge({:qbox_signature_token => generate_acctoken(url, data)}) + ) end # call_with_signature def request(url, data = nil, options = {}) @@ -19,6 +148,90 @@ module Qiniu [code, data, raw_headers] end # request + EMPTY_ARGS = {} + + ### 生成下载授权URL + def authorize_download_url(url, args = EMPTY_ARGS) + ### 提取AK/SK信息 + access_key = Config.settings[:access_key] + secret_key = Config.settings[:secret_key] + + ### 授权期计算 + e = Auth.calculate_deadline(args[:expires_in], args[:deadline]) + + ### URL变换:追加授权期参数 + if url.index('?').is_a?(Fixnum) then + # 已有参数 + download_url = "#{url}&e=#{e}" + else + # 尚无参数 + download_url = "#{url}?e=#{e}" + end + + ### 生成数字签名 + sign = HMAC::SHA1.new(secret_key).update(download_url).digest + encoded_sign = Utils.urlsafe_base64_encode(sign) + + ### 生成下载授权凭证 + dntoken = "#{access_key}:#{encoded_sign}" + + ### 返回下载授权URL + return "#{download_url}&token=#{dntoken}" + end # authorize_download_url + + def generate_acctoken(url, body = '') + ### 提取AK/SK信息 + access_key = Config.settings[:access_key] + secret_key = Config.settings[:secret_key] + + ### 解析URL,生成待签名字符串 + uri = URI.parse(url) + signing_str = uri.path + + # 如有QueryString部分,则需要加上 + query_string = uri.query + if query_string.is_a?(String) && !query_string.empty? + signing_str += '?' + query_string + end + + # 追加换行符 + signing_str += "\n" + + # 如果有Body,则也加上 + # (仅限于mime == "application/x-www-form-urlencoded"的情况) + if body.is_a?(String) && !body.empty? + signing_str += body + end + + ### 生成数字签名 + sign = HMAC::SHA1.new(secret_key).update(signing_str).digest + encoded_sign = Utils.urlsafe_base64_encode(sign) + + ### 生成管理授权凭证 + acctoken = "#{access_key}:#{encoded_sign}" + + ### 返回管理授权凭证 + return acctoken + end # generate_acctoken + + def generate_uptoken(put_policy) + ### 提取AK/SK信息 + access_key = Config.settings[:access_key] + secret_key = Config.settings[:secret_key] + + ### 生成待签名字符串 + encoded_put_policy = Utils.urlsafe_base64_encode(put_policy.to_json) + + ### 生成数字签名 + sign = HMAC::SHA1.new(secret_key).update(encoded_put_policy).digest + encoded_sign = Utils.urlsafe_base64_encode(sign) + + ### 生成上传授权凭证 + uptoken = "#{access_key}:#{encoded_sign}:#{encoded_put_policy}" + + ### 返回上传授权凭证 + return uptoken + end # generate_uptoken end # class << self end # module Auth diff --git a/lib/qiniu/management.rb b/lib/qiniu/management.rb index 1d6d500..6797da2 100755 --- a/lib/qiniu/management.rb +++ b/lib/qiniu/management.rb @@ -9,10 +9,21 @@ module Qiniu Auth.request Config.settings[:rs_host] + '/buckets' end # buckets - def mkbucket(bucket_name) - Auth.request Config.settings[:rs_host] + '/mkbucket/' + bucket_name + PRIVATE_BUCKET = 0 + PUBLIC_BUCKET = 1 + + def mkbucket(bucket_name, is_public = PUBLIC_BUCKET) + Auth.request Config.settings[:rs_host] + '/mkbucket2/' + bucket_name + '/public/' + is_public.to_s end # mkbucket + def make_a_private_bucket(bucket_name) + return mkbucket(bucket_name, PRIVATE_BUCKET) + end # make_a_private_bucket + + def make_a_public_bucket(bucket_name) + return mkbucket(bucket_name, PUBLIC_BUCKET) + end # make_a_public_bucket + def stat(bucket, key) Auth.request Config.settings[:rs_host] + '/stat/' + encode_entry_uri(bucket, key) end # stat diff --git a/lib/qiniu/upload.rb b/lib/qiniu/upload.rb index faede91..ecb3743 100755 --- a/lib/qiniu/upload.rb +++ b/lib/qiniu/upload.rb @@ -1,4 +1,5 @@ # -*- encoding: utf-8 -*- +# vim: sw=2 ts=2 module Qiniu module Storage @@ -61,6 +62,27 @@ module Qiniu Utils.http_request url, post_data end # upload_with_token_2 + ### 授权举例 + # put_policy.bucket | put_policy.key | key | 语义 | 授权 + # :---------------- | :------------- | :------ | :--- | :--- + # trivial_bucket | | | 新增 | 允许,最终key为1)使用put_policy.save_key生成的值或2)资源内容的Hash值 + # trivial_bucket | | foo.txt | 新增 | 允许 + # trivial_bucket | | bar.jpg | 新增 | 允许 + # trivial_bucket | foo.txt | | 覆盖 | 允许,由SDK将put_policy.key赋值给key实现 + # trivial_bucket | foo.txt | foo.txt | 覆盖 | 允许 + # trivial_bucket | foo.txt | bar.jpg | 覆盖 | 禁止,put_policy.key与key不一致 + def upload_with_put_policy(put_policy, + local_file, + key = nil, + x_vars = nil) + uptoken = Auth.generate_uptoken(put_policy) + if key.nil? then + key = put_policy.key + end + + return upload_with_token_2(uptoken, local_file, key, x_vars) + end # upload_with_put_policy + private def _generate_action_params(local_file, bucket, diff --git a/lib/qiniu/utils.rb b/lib/qiniu/utils.rb index 3dd3005..4b1f024 100755 --- a/lib/qiniu/utils.rb +++ b/lib/qiniu/utils.rb @@ -128,26 +128,5 @@ module Qiniu File.open(filepath, "rb") { |f| Zlib.crc32 f.read } end - def generate_qbox_signature(url, params, mime = nil) - access_key = Config.settings[:access_key] - secret_key = Config.settings[:secret_key] - uri = URI.parse(url) - signature = uri.path - query_string = uri.query - signature += '?' + query_string if !query_string.nil? && !query_string.empty? - signature += "\n" - if params.is_a?(Hash) - params_string = generate_query_string(params) - signature += params_string - end - if mime.is_a?(String) && mime == "application/x-www-form-urlencoded" && params.is_a?(String) - signature += params - end - hmac = HMAC::SHA1.new(secret_key) - hmac.update(signature) - encoded_digest = urlsafe_base64_encode(hmac.digest) - %Q(#{access_key}:#{encoded_digest}) - end - end # module Utils end # module Qiniu diff --git a/lib/qiniu/version.rb b/lib/qiniu/version.rb index fa24356..820edea 100755 --- a/lib/qiniu/version.rb +++ b/lib/qiniu/version.rb @@ -3,7 +3,7 @@ module Qiniu module Version MAJOR = 6 - MINOR = 1 + MINOR = 2 PATCH = 0 # Returns a version string by joining MAJOR, MINOR, and PATCH with '.' # diff --git a/spec/qiniu/auth_spec.rb b/spec/qiniu/auth_spec.rb new file mode 100755 index 0000000..afb54bc --- /dev/null +++ b/spec/qiniu/auth_spec.rb @@ -0,0 +1,75 @@ +# -*- encoding: utf-8 -*- + +require 'spec_helper' +require 'qiniu/auth' +require 'qiniu/storage' +require 'digest/sha1' + +module Qiniu + module Auth + describe Auth do + + before :all do + @bucket = 'RubySDK-Test-Private' + @bucket = make_unique_bucket(@bucket) + + ### 尝试创建Bucket + result = Qiniu::Storage.make_a_private_bucket(@bucket) + puts result.inspect + end + + after :all do + ### 不删除Bucket以备下次使用 + end + + ### 测试私有资源下载 + context ".download_private_file" do + it "should works" do + ### 生成Key + key = 'a_private_file' + key = make_unique_key_in_bucket(key) + puts "key=#{key}" + + ### 上传测试文件 + pp = Auth::PutPolicy.new(@bucket, key) + code, data, raw_headers = Qiniu::Storage.upload_with_put_policy( + pp, + __FILE__ + ) + code.should == 200 + puts data.inspect + puts raw_headers.inspect + + ### 获取下载地址 + code, data = Qiniu::Storage.get(@bucket, key) + code.should == 200 + puts data.inspect + + url = data['url'] + + ### 授权下载地址(不带参数) + download_url = Qiniu::Auth.authorize_download_url(url) + puts "download_url=#{download_url}" + + result = RestClient.get(download_url) + result.code.should == 200 + result.body.should_not be_empty + + ### 授权下载地址(带参数) + download_url = Qiniu::Auth.authorize_download_url(url + '?download/a.m3u8') + puts "download_url=#{download_url}" + + result = RestClient.get(download_url) + result.code.should == 200 + result.body.should_not be_empty + + ### 删除文件 + code, data = Qiniu::Storage.delete(@bucket, key) + code.should == 200 + puts data.inspect + end + end + + end + end # module Storage +end # module Qiniu diff --git a/spec/qiniu/management_spec.rb b/spec/qiniu/management_spec.rb index 7023cc0..abd1fff 100755 --- a/spec/qiniu/management_spec.rb +++ b/spec/qiniu/management_spec.rb @@ -1,4 +1,5 @@ # -*- encoding: utf-8 -*- +# vim: sw=2 ts=2 require 'digest/sha1' require 'spec_helper' @@ -29,15 +30,12 @@ module Qiniu end ### 准备数据 - context ".upload_with_token_2" do + context ".prepare_file" do it "should works" do - upopts = {:scope => @bucket, :expires_in => 3600, :endUser => "why404@gmail.com"} - uptoken = Qiniu.generate_upload_token(upopts) - - code, data, raw_headers = Qiniu::Storage.upload_with_token_2( - uptoken, - __FILE__, - @key + pp = Auth::PutPolicy.new(@bucket, @key) + code, data, raw_headers = Qiniu::Storage.upload_with_put_policy( + pp, + __FILE__ ) code.should == 200 puts data.inspect diff --git a/spec/qiniu/upload_spec.rb b/spec/qiniu/upload_spec.rb index e344c21..f345c8e 100755 --- a/spec/qiniu/upload_spec.rb +++ b/spec/qiniu/upload_spec.rb @@ -1,4 +1,5 @@ # -*- encoding: utf-8 -*- +# vim: sw=2 ts=2 require 'spec_helper' require 'qiniu/auth' @@ -126,6 +127,49 @@ module Qiniu end end + context ".upload_with_put_policy" do + it "should works" do + pp = Qiniu::Auth::PutPolicy.new(@bucket, @key) + pp.end_user = "why404@gmail.com" + puts 'put_policy=' + pp.to_json + + code, data, raw_headers = Qiniu::Storage.upload_with_put_policy( + pp, + __FILE__, + @key + '-not-equal' + ) + code.should_not == 200 + puts data.inspect + puts raw_headers.inspect + + code, data, raw_headers = Qiniu::Storage.upload_with_put_policy( + pp, + __FILE__, + @key + ) + + code.should == 200 + puts data.inspect + puts raw_headers.inspect + end + end # .upload_with_put_policy + + context ".stat" do + it "should exists" do + code, data = Qiniu::Storage.stat(@bucket, @key) + puts data.inspect + code.should == 200 + end + end + + context ".delete" do + it "should works" do + code, data = Qiniu::Storage.delete(@bucket, @key) + puts data.inspect + code.should == 200 + end + end + ### 测试断点续上传 context ".resumable_upload_with_token" do it "should works" do