From 672465715efe6d72ec205ebcd0d64975dcc935c5 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Thu, 3 Oct 2024 14:08:43 +1000 Subject: [PATCH] Initial shot at a Ruby implementation --- ruby/README.md | 17 +++++++++++++++++ ruby/tests.rb | 45 +++++++++++++++++++++++++++++++++++++++++++++ ruby/token.rb | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 ruby/README.md create mode 100644 ruby/tests.rb create mode 100644 ruby/token.rb diff --git a/ruby/README.md b/ruby/README.md new file mode 100644 index 0000000..fb11d67 --- /dev/null +++ b/ruby/README.md @@ -0,0 +1,17 @@ +# BunnyCDN.TokenAuthentication +## Ruby +### Introduction + +This has been tested on Ruby 3.1.1, 3.2.0 and 3.3.0, 3.3.4 & 3.3.5 + +### Usage + +An example: + +```ruby +sec_key = "229248f0-f007-4bf9-ba1f-bbf1b4ad9d40" + +signed_url = sign_url("https://token-tester.b-cdn.net/300kb.jpg", sec_key: sec_key, expiration_time: 3600, path_allowed: "/300kb", countries_allowed: 'AU,NZ') + +signed_url = sign_url("https://token-tester.b-cdn.net/300kb.jpg", sec_key: sec_key, expiration_time: 3600, path_allowed: "/300kb", countries_allowed: 'AU,NZ', path_url: true) +``` diff --git a/ruby/tests.rb b/ruby/tests.rb new file mode 100644 index 0000000..fddc979 --- /dev/null +++ b/ruby/tests.rb @@ -0,0 +1,45 @@ +require_relative 'token.rb' + +# Tests Generated with the Python3 implementation, modified to allow directly setting the expired value for consistency. +# +def tests + test_url = "https://token-tester.b-cdn.net/300kb.jpg" + sec_key = "229248f0-f007-4bf9-ba1f-bbf1b4ad9d40" + expires = 1727918622 + + test_cases = [ + { user_ip: nil, path_allowed: nil, countries_allowed: nil, countries_blocked: nil, limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=fa_Vy6p0rbSWCf1CHNBdSiSku828n0HNDffUX0DFlnI&expires=1727918622/300kb.jpg"}, + + { user_ip: nil, path_allowed: nil, countries_allowed: "AU", countries_blocked: nil, limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=8ryY2_NXJ8PauusW-GvF_GSstCq6IwkV1VmeZOrFKUQ&token_countries=AU&expires=1727918622/300kb.jpg"}, + + { user_ip: "192.168.100.100", path_allowed: nil, countries_allowed: "AU", countries_blocked: nil, limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=wELdlcF6DFBiUI9daYEUf83xUdkiWiUnRs4WKjzxVXo&token_countries=AU&expires=1727918622/300kb.jpg"}, + + { user_ip: "192.168.100.100", path_allowed: "/300kb", countries_allowed: "AU", countries_blocked: nil, limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=HQHBUOFnQCUl3Z53YsDUeihCYrP6wPWVCagPZhE0vKI&token_countries=AU&token_path=%2F300kb&expires=1727918622/300kb.jpg"}, + + { user_ip: "192.168.100.100", path_allowed: "/300kb", countries_allowed: "AU", countries_blocked: "GB", limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=F0LkSqauEQ2vUfyB48WwGntGpIBQQdl0AxdX5XrTDow&token_countries=AU&token_countries_blocked=GB&token_path=%2F300kb&expires=1727918622/300kb.jpg"}, + + { user_ip: "192.168.100.100", path_allowed: "/300kb", countries_allowed: "AU,NZ", countries_blocked: "GB", limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=DEOLD1efgKnm2FWBEwlg75NohDtydbKj4PU7oEpDoro&token_countries=AU%2CNZ&token_countries_blocked=GB&token_path=%2F300kb&expires=1727918622/300kb.jpg"}, + + { user_ip: "192.168.100.100", path_allowed: "/300kb", countries_allowed: "AU, NZ", countries_blocked: "GB", limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=0XdwT0g9UACzUBh7AzyFwPhVZIsGwqj7yvHGHcY8qXo&token_countries=AU%2C%20NZ&token_countries_blocked=GB&token_path=%2F300kb&expires=1727918622/300kb.jpg"}, + + { user_ip: "192.168.100.100", path_allowed: nil, countries_allowed: nil, countries_blocked: nil, limit: nil, path_url: true, expected: "https://token-tester.b-cdn.net/bcdn_token=SmnSkK1stGqOJge706jsf-02HaCbUaVv7507ZrLP43k&expires=1727918622/300kb.jpg"}, + + { user_ip: nil, path_allowed: nil, countries_allowed: nil, countries_blocked: nil, limit: nil, expected: "https://token-tester.b-cdn.net/300kb.jpg?token=fa_Vy6p0rbSWCf1CHNBdSiSku828n0HNDffUX0DFlnI&expires=1727918622"}, + + { user_ip: "192.168.100.100", path_allowed: nil, countries_allowed: nil, countries_blocked: nil, limit: nil, expected: "https://token-tester.b-cdn.net/300kb.jpg?token=SmnSkK1stGqOJge706jsf-02HaCbUaVv7507ZrLP43k&expires=1727918622"}, + + { user_ip: "192.168.100.100", path_allowed: "/300kb", countries_allowed: nil, countries_blocked: nil, limit: nil, expected: "https://token-tester.b-cdn.net/300kb.jpg?token=jriwRWg1R2Ba_fCAFP7KnIKoBCYgBzkJg83mD8hJchA&token_path=%2F300kb&expires=1727918622"}, + + { user_ip: "192.168.100.100", path_allowed: "/300kb", path_url: false, expected: "https://token-tester.b-cdn.net/300kb.jpg?token=jriwRWg1R2Ba_fCAFP7KnIKoBCYgBzkJg83mD8hJchA&token_path=%2F300kb&expires=1727918622"} + ] + test_cases.each_with_index do |test_case, index| + expected = test_case.delete(:expected) + generated = sign_url(test_url, sec_key: sec_key, expires: expires, **test_case) + puts "Test #{index}: #{expected == generated ? "\e[32mPASSED\e[0m" : "\e[31mFAILED\e[0m"}" + puts " Expected : #{expected}" + puts " Generated: #{generated}" + end +end + +tests + diff --git a/ruby/token.rb b/ruby/token.rb new file mode 100644 index 0000000..e5f52f5 --- /dev/null +++ b/ruby/token.rb @@ -0,0 +1,33 @@ +require "uri" +require "base64" +require "digest" + +def sign_url(url, sec_key:, expiration_time: nil, expires: nil, user_ip: "", path_url: false , path_allowed: nil, countries_allowed: nil, countries_blocked: nil, limit: nil) + raise ArgumentError, "use EITHER expiration_time OR expires" if expiration_time && expires || !expiration_time && !expires + + uri = URI(url) + expires = (expires || (Time.now.to_i + expiration_time.to_i)).to_s + + parameters = URI.decode_www_form(uri.query || "").to_h + extra_parameters = {token_countries: countries_allowed, token_countries_blocked: countries_blocked, token_path: path_allowed, limit: limit}.compact + parameters.merge!(extra_parameters) unless extra_parameters.empty? + parameters = parameters.sort.to_h + + # The signature requires the non-encoded version of the query string + parameter_data = parameters.map { |k, v| "#{k}=#{v}" }.join("&") + signature_path = path_allowed || uri.path + hashable_base = sec_key + signature_path + expires + parameter_data + user_ip.to_s + + token = Base64.urlsafe_encode64(Digest::SHA256.digest(hashable_base), padding: false) + + if path_url + # Get our parameters into the order we want them, but manually add uri.path to the end. + + parameter_data_url = URI.encode_www_form({bcdn_token: token, **parameters}).gsub("+", "%20") + "&expires=#{expires + uri.path}" + "#{uri.scheme}://#{uri.host}/#{parameter_data_url}" + else + uri.query = URI.encode_www_form({token: token, **parameters, expires: expires}).gsub("+", "%20") + uri.to_s + end +end +