tinypass 0.0.1

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.
Files changed (85) hide show
  1. checksums.yaml +15 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +32 -0
  8. data/Rakefile +19 -0
  9. data/lib/.DS_Store +0 -0
  10. data/lib/tinypass.rb +48 -0
  11. data/lib/tinypass/.DS_Store +0 -0
  12. data/lib/tinypass/builder.rb +7 -0
  13. data/lib/tinypass/builder/client_builder.rb +40 -0
  14. data/lib/tinypass/builder/client_parser.rb +48 -0
  15. data/lib/tinypass/builder/cookie_parser.rb +25 -0
  16. data/lib/tinypass/builder/json_msg_builder.rb +115 -0
  17. data/lib/tinypass/builder/open_encoder.rb +11 -0
  18. data/lib/tinypass/builder/secure_encoder.rb +15 -0
  19. data/lib/tinypass/builder/security_utils.rb +67 -0
  20. data/lib/tinypass/gateway.rb +104 -0
  21. data/lib/tinypass/offer.rb +23 -0
  22. data/lib/tinypass/policies.rb +5 -0
  23. data/lib/tinypass/policies/discount_policy.rb +25 -0
  24. data/lib/tinypass/policies/policy.rb +25 -0
  25. data/lib/tinypass/policies/pricing_policy.rb +13 -0
  26. data/lib/tinypass/policies/restriction_policy.rb +14 -0
  27. data/lib/tinypass/price_option.rb +66 -0
  28. data/lib/tinypass/resource.rb +17 -0
  29. data/lib/tinypass/token.rb +6 -0
  30. data/lib/tinypass/token/access_token.rb +163 -0
  31. data/lib/tinypass/token/access_token_list.rb +71 -0
  32. data/lib/tinypass/token/access_token_store.rb +59 -0
  33. data/lib/tinypass/token/meter.rb +76 -0
  34. data/lib/tinypass/token/meter_helper.rb +82 -0
  35. data/lib/tinypass/token/token_data.rb +72 -0
  36. data/lib/tinypass/ui.rb +2 -0
  37. data/lib/tinypass/ui/html_widget.rb +29 -0
  38. data/lib/tinypass/ui/purchase_request.rb +34 -0
  39. data/lib/tinypass/utils.rb +34 -0
  40. data/lib/tinypass/version.rb +3 -0
  41. data/spec/.DS_Store +0 -0
  42. data/spec/acceptance/basic_workflow_spec.rb +81 -0
  43. data/spec/integration/.DS_Store +0 -0
  44. data/spec/integration/cases/.DS_Store +0 -0
  45. data/spec/integration/cases/basic_spec.rb +53 -0
  46. data/spec/integration/cases/combo_spec.rb +43 -0
  47. data/spec/integration/cases/metered_reminder_spec.rb +42 -0
  48. data/spec/integration/cases/metered_strict_spec.rb +54 -0
  49. data/spec/integration/cases/metered_views_spec.rb +92 -0
  50. data/spec/integration/client_builder_and_parser_spec.rb +21 -0
  51. data/spec/spec_helper.rb +33 -0
  52. data/spec/support/.DS_Store +0 -0
  53. data/spec/support/acceptance.rb +13 -0
  54. data/spec/support/tinypass_factories.rb +25 -0
  55. data/spec/unit/.DS_Store +0 -0
  56. data/spec/unit/builder/.DS_Store +0 -0
  57. data/spec/unit/builder/client_builder_spec.rb +23 -0
  58. data/spec/unit/builder/client_parser_spec.rb +27 -0
  59. data/spec/unit/builder/cookie_parser_spec.rb +39 -0
  60. data/spec/unit/builder/json_msg_builder_spec.rb +73 -0
  61. data/spec/unit/builder/open_encoder_spec.rb +15 -0
  62. data/spec/unit/builder/secure_encoder_spec.rb +23 -0
  63. data/spec/unit/builder/security_utils_spec.rb +42 -0
  64. data/spec/unit/gateway_spec.rb +80 -0
  65. data/spec/unit/offer_spec.rb +31 -0
  66. data/spec/unit/policies/.DS_Store +0 -0
  67. data/spec/unit/policies/discount_policy_spec.rb +40 -0
  68. data/spec/unit/policies/pricing_policy_spec.rb +23 -0
  69. data/spec/unit/policies/restriction_policy_spec.rb +35 -0
  70. data/spec/unit/price_option_spec.rb +109 -0
  71. data/spec/unit/resource_spec.rb +24 -0
  72. data/spec/unit/tinypass_spec.rb +51 -0
  73. data/spec/unit/token/.DS_Store +0 -0
  74. data/spec/unit/token/access_token_list_spec.rb +94 -0
  75. data/spec/unit/token/access_token_spec.rb +267 -0
  76. data/spec/unit/token/access_token_store_spec.rb +93 -0
  77. data/spec/unit/token/meter_helper_spec.rb +103 -0
  78. data/spec/unit/token/meter_spec.rb +66 -0
  79. data/spec/unit/token/token_data_spec.rb +66 -0
  80. data/spec/unit/ui/.DS_Store +0 -0
  81. data/spec/unit/ui/html_widget_spec.rb +89 -0
  82. data/spec/unit/ui/purchase_request_spec.rb +46 -0
  83. data/spec/unit/utils_spec.rb +57 -0
  84. data/tinypass.gemspec +35 -0
  85. metadata +330 -0
@@ -0,0 +1,67 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Tinypass
5
+ module SecurityUtils
6
+ extend self
7
+
8
+ DELIM = '~~~'
9
+
10
+ def encrypt(key, data)
11
+ original_key = key
12
+ key = prepare_key(key)
13
+
14
+ cipher = OpenSSL::Cipher.new('AES-256-ECB')
15
+ cipher.encrypt
16
+ cipher.key = key
17
+ encrypted = cipher.update(data) + cipher.final
18
+
19
+ safe = url_ensafe(encrypted)
20
+ safe + DELIM + hash_hmac_sha256(original_key, safe)
21
+ end
22
+
23
+ def decrypt(key, data)
24
+ cipher_text, hmac_text = data.split(DELIM)
25
+ check_hmac!(key, cipher_text, hmac_text) if hmac_text
26
+ key = prepare_key(key)
27
+ cipher_text = url_desafe(cipher_text)
28
+
29
+ cipher = OpenSSL::Cipher.new('AES-256-ECB')
30
+ cipher.decrypt
31
+ cipher.key = key
32
+ cipher.update(cipher_text) + cipher.final
33
+ end
34
+
35
+ def hash_hmac_sha256(key, data)
36
+ digest = OpenSSL::Digest::Digest.new('sha256')
37
+ hmac = OpenSSL::HMAC.digest(digest, key, data)
38
+ url_ensafe(hmac)
39
+ end
40
+
41
+ private
42
+
43
+ def url_ensafe(data)
44
+ base64 = Base64.urlsafe_encode64(data)
45
+ base64.sub!(/(=+)$/, '')
46
+ base64
47
+ end
48
+
49
+ def url_desafe(data)
50
+ modulus = data.length % 4
51
+ data << '=' * (4 - modulus) if modulus != 0
52
+ Base64.urlsafe_decode64(data)
53
+ end
54
+
55
+ def prepare_key(key)
56
+ key = key.slice(0, 32) if key.length > 32
57
+ key = key.ljust(32, 'X') if key.length < 32
58
+ key
59
+ end
60
+
61
+ def check_hmac!(key, cipher_text, hmac_text)
62
+ if hash_hmac_sha256(key, cipher_text) != hmac_text
63
+ raise ArgumentError.new('Could not parse message invalid hmac')
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,104 @@
1
+ require 'rest_client'
2
+ require 'uri'
3
+ require 'ostruct'
4
+
5
+ module Tinypass
6
+ module Gateway
7
+ extend self
8
+
9
+ def fetch_access_detail(rid, user_ref)
10
+ params = { rid: rid, user_ref: user_ref }
11
+ response = get('access', params)
12
+
13
+ return unless response
14
+
15
+ AccessDetails.new(MultiJson.load(response))
16
+ end
17
+
18
+ def fetch_access_details(params)
19
+ pagesize = params.delete(:pagesize) || params.delete("pagesize") || 500
20
+ params[:pagesize] = pagesize
21
+ response = get('access/search', params)
22
+
23
+ return [] unless response
24
+
25
+ PagedList.new(MultiJson.load(response))
26
+ end
27
+
28
+ def fetch_subscription_details(params)
29
+ response = get('subscription/search', params)
30
+
31
+ MultiJson.load(response)
32
+ end
33
+
34
+ def cancel_subscription(params)
35
+ post('subscription/cancel', params)
36
+ end
37
+
38
+ def grant_access(params)
39
+ post('access/grant', params)
40
+ end
41
+
42
+ def revoke_access(params)
43
+ post('access/revoke', params)
44
+ end
45
+
46
+ private
47
+
48
+ def get(action, params)
49
+ url = build_url(action, params)
50
+ headers = build_authenticated_headers('GET', url)
51
+
52
+ full_url = Config.endpoint + url
53
+
54
+ RestClient.get(full_url, headers)
55
+ rescue RestClient::ResourceNotFound
56
+ nil
57
+ end
58
+
59
+ def post(action, params)
60
+ url = build_url(action, params)
61
+ headers = build_authenticated_headers('POST', url)
62
+
63
+ full_url = Config.endpoint + url
64
+
65
+ RestClient.post(full_url, {}, headers)
66
+ end
67
+
68
+ def build_url(url, params)
69
+ "#{ Config::REST_CONTEXT }/#{ url }?#{ URI.encode_www_form(params) }"
70
+ end
71
+
72
+ def build_authenticated_headers(http_method, url)
73
+ request_definition = "#{ http_method.upcase } #{ url }"
74
+ signature = "#{ Tinypass.aid }:#{ SecurityUtils.hash_hmac_sha256(Tinypass.private_key, request_definition) }"
75
+
76
+ { authorization: signature }
77
+ end
78
+
79
+ class AccessDetails < OpenStruct
80
+ def access_granted?
81
+ expires.nil? || expires.to_i >= Time.now.to_i
82
+ end
83
+ end
84
+
85
+ class PagedList < OpenStruct
86
+ include Enumerable
87
+
88
+ def initialize(parsed_response)
89
+ @list = []
90
+ raw_access_details = parsed_response.delete('data')
91
+
92
+ raw_access_details.each do |d|
93
+ @list << AccessDetails.new(d)
94
+ end
95
+
96
+ super(parsed_response)
97
+ end
98
+
99
+ def each(&block)
100
+ @list.each(&block)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,23 @@
1
+ module Tinypass
2
+ class Offer
3
+ attr_reader :resource, :pricing, :policies, :tags
4
+
5
+ def initialize(resource, *price_options_or_policy)
6
+ raise ArgumentError.new("Can't initialize offer without price options or policy") if price_options_or_policy.empty?
7
+
8
+ @resource = resource
9
+ @policies = []
10
+ @tags = []
11
+
12
+ if price_options_or_policy.first.kind_of?(PricingPolicy)
13
+ @pricing = price_options_or_policy.first
14
+ else
15
+ @pricing = PricingPolicy.new(price_options_or_policy)
16
+ end
17
+ end
18
+
19
+ def has_active_prices?
20
+ pricing.has_active_options?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ require 'tinypass/policies/policy'
2
+
3
+ require 'tinypass/policies/discount_policy'
4
+ require 'tinypass/policies/pricing_policy'
5
+ require 'tinypass/policies/restriction_policy'
@@ -0,0 +1,25 @@
1
+ module Tinypass
2
+ class DiscountPolicy < Policy
3
+ def self.on_total_spend_in_period(amount, within_period, discount)
4
+ policy = new
5
+
6
+ policy[POLICY_TYPE] = DISCOUNT_TOTAL_IN_PERIOD
7
+ policy["amount"] = amount
8
+ policy["withinPeriod"] = within_period
9
+ policy["discount"] = discount
10
+
11
+ policy
12
+ end
13
+
14
+ def self.previous_purchased(rids, discount)
15
+ rids = Array(rids)
16
+ policy = new
17
+
18
+ policy[POLICY_TYPE] = DISCOUNT_PREVIOUS_PURCHASE
19
+ policy["rids"] = rids
20
+ policy["discount"] = discount
21
+
22
+ policy
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module Tinypass
2
+ class Policy
3
+ DISCOUNT_TOTAL_IN_PERIOD = "d1"
4
+ DISCOUNT_PREVIOUS_PURCHASE = "d2"
5
+ STRICT_METER_BY_TIME = "sm1"
6
+ REMINDER_METER_BY_TIME = "rm1"
7
+ REMINDER_METER_BY_COUNT = "rm2"
8
+ RESTRICT_MAX_PURCHASES = "r1"
9
+
10
+ POLICY_TYPE = "type"
11
+
12
+ def initialize
13
+ @map = {}
14
+ end
15
+
16
+ def []=(key,value)
17
+ key = key.to_s
18
+ @map[key] = value
19
+ end
20
+
21
+ def to_hash
22
+ @map
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module Tinypass
2
+ class PricingPolicy < Policy
3
+ attr_reader :price_options
4
+
5
+ def initialize(price_options)
6
+ @price_options = Array(price_options)
7
+ end
8
+
9
+ def has_active_options?
10
+ price_options.any? { |price_option| price_option.active? }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Tinypass
2
+ class RestrictionPolicy < Policy
3
+ def self.limit_purchases_in_period_by_amount(amount, within_period, link_with_details = nil)
4
+ policy = new
5
+
6
+ policy[POLICY_TYPE] = RESTRICT_MAX_PURCHASES
7
+ policy['amount'] = amount
8
+ policy['withinPeriod'] = within_period
9
+ policy['linkWithDetails'] = link_with_details if link_with_details
10
+
11
+ policy
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,66 @@
1
+ module Tinypass
2
+ class PriceOption
3
+ attr_accessor :price, :access_period, :caption, :start_date_in_secs, :end_date_in_secs
4
+ attr_reader :split_pays
5
+
6
+ def initialize(price, access_period = nil, start_date_in_secs = nil, end_date_in_secs = nil)
7
+ @split_pays = {}
8
+
9
+ @price, @access_period = price, access_period
10
+
11
+ @start_date_in_secs = TokenData.convert_to_epoch_seconds(start_date_in_secs) if start_date_in_secs
12
+ @end_date_in_secs = TokenData.convert_to_epoch_seconds(end_date_in_secs) if end_date_in_secs
13
+ end
14
+
15
+ def access_period_in_msecs
16
+ Utils.parse_loose_period_in_msecs(@access_period)
17
+ end
18
+
19
+ def access_period_in_secs
20
+ access_period_in_msecs / 1000
21
+ end
22
+
23
+ def active?(timestamp = nil)
24
+ timestamp ||= Time.now.to_i
25
+ timestamp = TokenData.convert_to_epoch_seconds(timestamp)
26
+
27
+ return false if start_date_in_secs && timestamp < start_date_in_secs
28
+ return false if end_date_in_secs && timestamp > end_date_in_secs
29
+ return true
30
+ end
31
+
32
+ def caption=(value)
33
+ value = value[0...50] if value
34
+ @caption = value
35
+ end
36
+
37
+ def add_split_pay(email, amount)
38
+ amount = amount[0..-2].to_f / 100.0 if amount.end_with?('%')
39
+ amount = amount.to_f
40
+
41
+ @split_pays[email] = amount
42
+ end
43
+
44
+ def to_s
45
+ string = "Price:#{ price }\tPeriod:#{ access_period }\tTrial Period:#{ access_period }"
46
+
47
+ if start_date_in_secs
48
+ string << "\tStart:#{ start_date_in_secs }:#{ Time.at(start_date_in_secs).strftime('%a, %d %b %Y %H %M %S') }"
49
+ end
50
+
51
+ if end_date_in_secs
52
+ string << "\tEnd:#{ end_date_in_secs }:#{ Time.at(end_date_in_secs).strftime('%a, %d %b %Y %H %M %S') }"
53
+ end
54
+
55
+ string << "\tCaption:#{ caption }" if caption
56
+
57
+ if @split_pays.any?
58
+ @split_pays.each do |email, amount|
59
+ string << "\tSplit:#{ email }:#{ amount }"
60
+ end
61
+ end
62
+
63
+ string
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ module Tinypass
2
+ class Resource
3
+ attr_accessor :name, :url
4
+
5
+ def initialize(rid = nil, name = nil, url = nil)
6
+ @rid, @name, @url = rid, name, url
7
+ end
8
+
9
+ def rid
10
+ @rid.to_s
11
+ end
12
+
13
+ def rid=(value)
14
+ @rid = value.to_s
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ require 'tinypass/token/access_token'
2
+ require 'tinypass/token/access_token_list'
3
+ require 'tinypass/token/access_token_store'
4
+ require 'tinypass/token/meter'
5
+ require 'tinypass/token/meter_helper'
6
+ require 'tinypass/token/token_data'
@@ -0,0 +1,163 @@
1
+ module Tinypass
2
+ class AccessToken
3
+ attr_accessor :access_state, :token_data
4
+
5
+ def initialize(rid_or_token_data, expiration_in_seconds = nil, early_expiration_in_seconds = nil)
6
+ if rid_or_token_data.kind_of?(TokenData)
7
+ self.token_data = rid_or_token_data
8
+ return
9
+ end
10
+
11
+ expiration_in_seconds ||= 0
12
+ early_expiration_in_seconds ||= 0
13
+
14
+ token_data = TokenData.new
15
+ token_data[TokenData::RID] = rid_or_token_data.to_s
16
+ token_data[TokenData::EX] = TokenData.convert_to_epoch_seconds(expiration_in_seconds)
17
+ token_data[TokenData::EARLY_EX] = TokenData.convert_to_epoch_seconds(early_expiration_in_seconds)
18
+ self.token_data = token_data
19
+ end
20
+
21
+ def rid
22
+ token_data.rid
23
+ end
24
+
25
+ def access_id
26
+ token_data[TokenData::ACCESS_ID]
27
+ end
28
+
29
+ def uid
30
+ token_data.fetch(TokenData::UID, 0)
31
+ end
32
+
33
+ def expiration_in_seconds
34
+ token_data.fetch(TokenData::EX, 0)
35
+ end
36
+ alias_method :expiration_in_secs, :expiration_in_seconds
37
+
38
+ def early_expiration_in_seconds
39
+ token_data.fetch(TokenData::EARLY_EX, 0)
40
+ end
41
+ alias_method :early_expiration_in_secs, :early_expiration_in_seconds
42
+
43
+ def trial_end_time_secs
44
+ token_data.fetch(TokenData::METER_TRIAL_ENDTIME, 0)
45
+ end
46
+
47
+ def lockout_end_time_secs
48
+ token_data.fetch(TokenData::METER_LOCKOUT_ENDTIME, 0)
49
+ end
50
+
51
+ def trial_view_count
52
+ token_data.fetch(TokenData::METER_TRIAL_ACCESS_ATTEMPTS, 0)
53
+ end
54
+
55
+ def trial_view_limit
56
+ token_data.fetch(TokenData::METER_TRIAL_MAX_ACCESS_ATTEMPTS, 0)
57
+ end
58
+
59
+ def metered?
60
+ meter_type != 0
61
+ end
62
+
63
+ def meter_view_based?
64
+ metered? && token_data[TokenData::METER_TRIAL_MAX_ACCESS_ATTEMPTS]
65
+ end
66
+
67
+ def meter_type
68
+ token_data.fetch(TokenData::METER_TYPE, 0)
69
+ end
70
+
71
+ def ips
72
+ token_data.fetch(TokenData::IPS, [])
73
+ end
74
+
75
+ def access_state
76
+ access_granted? if @access_state.nil?
77
+ @access_state
78
+ end
79
+
80
+ def access_granted?(client_ip = nil)
81
+ if expiration_in_seconds == -1
82
+ # special case. RID_NOT_FOUND
83
+ @access_state = AccessState::RID_NOT_FOUND if @access_state != AccessState::NO_TOKENS_FOUND
84
+ return false
85
+ end
86
+
87
+ if Utils::valid_ip?(client_ip) && ips.any? && !ips.include?(client_ip)
88
+ @access_state = AccessState::CLIENT_IP_DOES_NOT_MATCH_TOKEN
89
+ return false
90
+ end
91
+
92
+ if metered?
93
+ if trial_period_active?
94
+ @access_state = AccessState::METERED_IN_TRIAL
95
+ return true
96
+ end
97
+
98
+ if lockout_period_active?
99
+ @access_state = AccessState::METERED_IN_LOCKOUT
100
+ else
101
+ @access_state = AccessState::METERED_TRIAL_DEAD
102
+ end
103
+
104
+ return false
105
+ end
106
+
107
+ if expired?
108
+ @access_state = AccessState::EXPIRED
109
+ return false
110
+ end
111
+
112
+ @access_state = AccessState::ACCESS_GRANTED
113
+ true
114
+ end
115
+
116
+ def trial_period_active?
117
+ return false unless metered?
118
+
119
+ if meter_type == TokenData::METER_STRICT
120
+ return Time.now.to_i <= trial_end_time_secs
121
+ end
122
+
123
+ if meter_view_based?
124
+ return trial_view_count <= trial_view_limit && Time.now.to_i <= trial_end_time_secs
125
+ end
126
+
127
+ # unknown meter
128
+ return trial_end_time_secs == 0 || Time.now.to_i <= trial_end_time_secs
129
+ end
130
+
131
+ def lockout_period_active?
132
+ return false unless metered?
133
+ return false if trial_period_active?
134
+
135
+ return Time.now.to_i <= lockout_end_time_secs
136
+ end
137
+
138
+ def expired?
139
+ expiration = early_expiration_in_seconds
140
+ expiration = expiration_in_seconds if expiration == 0
141
+
142
+ return false if expiration == 0
143
+
144
+ expiration <= Time.now.to_i
145
+ end
146
+
147
+ def trial_dead?
148
+ !lockout_period_active? && !trial_period_active?
149
+ end
150
+ end
151
+
152
+ module AccessState
153
+ ACCESS_GRANTED = 100;
154
+ CLIENT_IP_DOES_NOT_MATCH_TOKEN = 200
155
+ RID_NOT_FOUND = 201
156
+ NO_TOKENS_FOUND = 202
157
+ METERED_IN_TRIAL = 203
158
+ EXPIRED = 204
159
+ NO_ACTIVE_PRICES = 205
160
+ METERED_IN_LOCKOUT = 206
161
+ METERED_TRIAL_DEAD = 207
162
+ end
163
+ end