paypal-rest 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bee5f5d7151ee8a98165bb500f97a9e0d553026d330eb2ce76dabc39cef1286f
4
+ data.tar.gz: 8fcb2b0980ccdaf75e201c505d0ae22f244380f7c2ae7bb39af22c524faddd97
5
+ SHA512:
6
+ metadata.gz: 692661c0bed877069c25404f9e4ad672d7f25a7587101ee123c073eaf575a952f4458fcb9b87d4d4b630679d35546df804b54877ddb0ee9a67339ff74e49379f
7
+ data.tar.gz: 901482d2ca9335fd41a12213db7e94356fa3e0b45151f89c2270a6d62cd3e0826b55b906057d5181f870371f600b2760f720daf322c1dc62d5cee1838bea5b13
@@ -0,0 +1,13 @@
1
+ version: 2.1
2
+ jobs:
3
+ build:
4
+ docker:
5
+ - image: ruby:3.1
6
+ steps:
7
+ - checkout
8
+ - run:
9
+ name: Run the default task
10
+ command: |
11
+ gem install bundler -v 2.3.10
12
+ bundle install
13
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,226 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+ Exclude:
4
+ - bin/**/*
5
+ <% `git status --ignored --porcelain`.scan(/^!!\s+(.*)$/).each do |match| %>
6
+ - <%= match[0] %>**/*
7
+ <% end %>
8
+
9
+ # Extensions
10
+ require:
11
+ - rubocop-rake
12
+ - rubocop-rspec
13
+
14
+ # New rules
15
+ Lint/DuplicateBranch: # (new in 1.3)
16
+ Enabled: true
17
+ Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1)
18
+ Enabled: true
19
+ Lint/EmptyBlock: # (new in 1.1)
20
+ Enabled: true
21
+ Lint/EmptyClass: # (new in 1.3)
22
+ Enabled: true
23
+ Lint/NoReturnInBeginEndBlocks: # (new in 1.2)
24
+ Enabled: true
25
+ Lint/ToEnumArguments: # (new in 1.1)
26
+ Enabled: true
27
+ Lint/UnexpectedBlockArity: # (new in 1.5)
28
+ Enabled: true
29
+ Lint/UnmodifiedReduceAccumulator: # (new in 1.1)
30
+ Enabled: true
31
+ Style/ArgumentsForwarding: # (new in 1.1)
32
+ Enabled: true
33
+ Style/CollectionCompact: # (new in 1.2)
34
+ Enabled: true
35
+ Style/DocumentDynamicEvalDefinition: # (new in 1.1)
36
+ Enabled: true
37
+ Style/NegatedIfElseCondition: # (new in 1.2)
38
+ Enabled: true
39
+ Style/NilLambda: # (new in 1.3)
40
+ Enabled: true
41
+ Style/RedundantArgument: # (new in 1.4)
42
+ Enabled: true
43
+ Style/SwapValues: # (new in 1.1)
44
+ Enabled: true
45
+ Gemspec/DateAssignment: # (new in 1.10)
46
+ Enabled: true
47
+ Layout/SpaceBeforeBrackets: # (new in 1.7)
48
+ Enabled: true
49
+ Lint/AmbiguousAssignment: # (new in 1.7)
50
+ Enabled: true
51
+ Lint/DeprecatedConstants: # (new in 1.8)
52
+ Enabled: true
53
+ Lint/LambdaWithoutLiteralBlock: # (new in 1.8)
54
+ Enabled: true
55
+ Lint/NumberedParameterAssignment: # (new in 1.9)
56
+ Enabled: true
57
+ Lint/OrAssignmentToConstant: # (new in 1.9)
58
+ Enabled: true
59
+ Lint/RedundantDirGlobSort: # (new in 1.8)
60
+ Enabled: true
61
+ Lint/SymbolConversion: # (new in 1.9)
62
+ Enabled: true
63
+ Lint/TripleQuotes: # (new in 1.9)
64
+ Enabled: true
65
+ Style/EndlessMethod: # (new in 1.8)
66
+ Enabled: true
67
+ Style/HashConversion: # (new in 1.10)
68
+ Enabled: true
69
+ Style/HashExcept: # (new in 1.7)
70
+ Enabled: true
71
+ Style/IfWithBooleanLiteralBranches: # (new in 1.9)
72
+ Enabled: true
73
+ Gemspec/RequireMFA: # new in 1.23
74
+ Enabled: true
75
+ Layout/LineEndStringConcatenationIndentation: # new in 1.18
76
+ Enabled: true
77
+ Lint/AmbiguousOperatorPrecedence: # new in 1.21
78
+ Enabled: true
79
+ Lint/AmbiguousRange: # new in 1.19
80
+ Enabled: true
81
+ Lint/EmptyInPattern: # new in 1.16
82
+ Enabled: true
83
+ Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
84
+ Enabled: true
85
+ Lint/RequireRelativeSelfPath: # new in 1.22
86
+ Enabled: true
87
+ Lint/UselessRuby2Keywords: # new in 1.23
88
+ Enabled: true
89
+ Naming/BlockForwarding: # new in 1.24
90
+ Enabled: true
91
+ Security/IoMethods: # new in 1.22
92
+ Enabled: true
93
+ Style/FileRead: # new in 1.24
94
+ Enabled: true
95
+ Style/FileWrite: # new in 1.24
96
+ Enabled: true
97
+ Style/InPatternThen: # new in 1.16
98
+ Enabled: true
99
+ Style/MapToHash: # new in 1.24
100
+ Enabled: true
101
+ Style/MultilineInPatternThen: # new in 1.16
102
+ Enabled: true
103
+ Style/NumberedParameters: # new in 1.22
104
+ Enabled: true
105
+ Style/NumberedParametersLimit: # new in 1.22
106
+ Enabled: true
107
+ Style/OpenStructUse: # new in 1.23
108
+ Enabled: true
109
+ Style/QuotedSymbols: # new in 1.16
110
+ Enabled: true
111
+ Style/RedundantSelfAssignmentBranch: # new in 1.19
112
+ Enabled: true
113
+ Style/SelectByRegexp: # new in 1.22
114
+ Enabled: true
115
+ Style/StringChars: # new in 1.12
116
+ Enabled: true
117
+ RSpec/ExcessiveDocstringSpacing: # new in 2.5
118
+ Enabled: true
119
+ RSpec/IdenticalEqualityAssertion: # new in 2.4
120
+ Enabled: true
121
+ RSpec/SubjectDeclaration: # new in 2.5
122
+ Enabled: true
123
+ RSpec/FactoryBot/SyntaxMethods: # new in 2.7
124
+ Enabled: true
125
+ RSpec/Rails/AvoidSetupHook: # new in 2.4
126
+ Enabled: true
127
+ Style/NestedFileDirname: # new in 1.26
128
+ Enabled: true
129
+ RSpec/BeEq: # new in 2.9.0
130
+ Enabled: true
131
+ RSpec/BeNil: # new in 2.9.0
132
+ Enabled: true
133
+ Lint/RefinementImportMethods: # new in 1.27
134
+ Enabled: true
135
+ Style/RedundantInitialize: # new in 1.27
136
+ Enabled: true
137
+
138
+ # Alterations
139
+ Naming/RescuedExceptionsVariableName:
140
+ Enabled: false
141
+ Style/BlockComments:
142
+ Exclude:
143
+ - spec/**/*
144
+ Style/Documentation:
145
+ Enabled: false
146
+ Style/FrozenStringLiteralComment:
147
+ Enabled: false
148
+ Layout/MultilineMethodCallIndentation:
149
+ Enabled: false
150
+ Style/StringLiterals:
151
+ EnforcedStyle: double_quotes
152
+ Layout/AccessModifierIndentation:
153
+ EnforcedStyle: outdent
154
+ RSpec/NestedGroups:
155
+ Enabled: false
156
+ RSpec/MultipleMemoizedHelpers:
157
+ Enabled: false
158
+ RSpec/MultipleExpectations:
159
+ Enabled: false
160
+ RSpec/ExampleLength:
161
+ Enabled: false
162
+ RSpec/ContextWording:
163
+ Prefixes:
164
+ - but
165
+ - if
166
+ - when
167
+ - with
168
+ - without
169
+ RSpec/AnyInstance:
170
+ Enabled: false
171
+ Metrics/BlockLength:
172
+ Exclude:
173
+ - spec/**/*
174
+ Layout/ArgumentAlignment:
175
+ Enabled: false
176
+ Style/TrailingCommaInArrayLiteral:
177
+ EnforcedStyleForMultiline: consistent_comma
178
+ Style/TrailingCommaInArguments:
179
+ EnforcedStyleForMultiline: consistent_comma
180
+ Style/TrailingCommaInHashLiteral:
181
+ EnforcedStyleForMultiline: consistent_comma
182
+ Layout/MultilineMethodCallBraceLayout:
183
+ EnforcedStyle: new_line
184
+ Style/AsciiComments:
185
+ Enabled: false
186
+ Layout/CaseIndentation:
187
+ EnforcedStyle: end
188
+ Style/Lambda:
189
+ Enabled: false
190
+ Layout/SpaceInLambdaLiteral:
191
+ Enabled: false
192
+ Style/RedundantSelf:
193
+ Enabled: false
194
+ Style/PreferredHashMethods:
195
+ EnforcedStyle: verbose
196
+ Style/IfUnlessModifier:
197
+ Enabled: false
198
+ Layout/EndAlignment:
199
+ EnforcedStyleAlignWith: variable
200
+ Style/BlockDelimiters:
201
+ EnforcedStyle: semantic
202
+ AllowBracesOnProceduralOneLiners: true
203
+ Style/GuardClause:
204
+ Enabled: false
205
+ Metrics/MethodLength:
206
+ Enabled: false
207
+ Naming/MethodParameterName:
208
+ AllowedNames:
209
+ - as
210
+ - id
211
+ Metrics/AbcSize:
212
+ Enabled: false
213
+ Style/IfUnlessModifierOfIfUnless:
214
+ Enabled: false
215
+ Style/NestedModifier:
216
+ Enabled: false
217
+ Style/SoleNestedConditional:
218
+ Enabled: false
219
+ Naming/VariableNumber:
220
+ EnforcedStyle: snake_case
221
+ RSpec/SubjectStub:
222
+ Enabled: false
223
+ Layout/ArrayAlignment:
224
+ Enabled: false
225
+ Layout/FirstArrayElementIndentation:
226
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2022-04-13
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in paypal-rest.gemspec
6
+ gemspec
7
+
8
+ gem "rack", "~> 2.2"
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.11"
11
+ gem "rubocop", "~> 1.7"
12
+ gem "rubocop-rake", "~> 0.6"
13
+ gem "rubocop-rspec", "~> 2.9"
14
+ gem "webmock", "~> 3.14"
data/Gemfile.lock ADDED
@@ -0,0 +1,82 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ paypal-rest (1.0.0)
5
+ faraday (~> 2.2)
6
+ faraday-retry (~> 1.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.8.0)
12
+ public_suffix (>= 2.0.2, < 5.0)
13
+ ast (2.4.2)
14
+ crack (0.4.5)
15
+ rexml
16
+ diff-lcs (1.5.0)
17
+ faraday (2.2.0)
18
+ faraday-net_http (~> 2.0)
19
+ ruby2_keywords (>= 0.0.4)
20
+ faraday-net_http (2.0.2)
21
+ faraday-retry (1.0.3)
22
+ hashdiff (1.0.1)
23
+ parallel (1.22.1)
24
+ parser (3.1.2.0)
25
+ ast (~> 2.4.1)
26
+ public_suffix (4.0.7)
27
+ rack (2.2.3)
28
+ rainbow (3.1.1)
29
+ rake (13.0.6)
30
+ regexp_parser (2.3.0)
31
+ rexml (3.2.5)
32
+ rspec (3.11.0)
33
+ rspec-core (~> 3.11.0)
34
+ rspec-expectations (~> 3.11.0)
35
+ rspec-mocks (~> 3.11.0)
36
+ rspec-core (3.11.0)
37
+ rspec-support (~> 3.11.0)
38
+ rspec-expectations (3.11.0)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.11.0)
41
+ rspec-mocks (3.11.1)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.11.0)
44
+ rspec-support (3.11.0)
45
+ rubocop (1.27.0)
46
+ parallel (~> 1.10)
47
+ parser (>= 3.1.0.0)
48
+ rainbow (>= 2.2.2, < 4.0)
49
+ regexp_parser (>= 1.8, < 3.0)
50
+ rexml
51
+ rubocop-ast (>= 1.16.0, < 2.0)
52
+ ruby-progressbar (~> 1.7)
53
+ unicode-display_width (>= 1.4.0, < 3.0)
54
+ rubocop-ast (1.17.0)
55
+ parser (>= 3.1.1.0)
56
+ rubocop-rake (0.6.0)
57
+ rubocop (~> 1.0)
58
+ rubocop-rspec (2.9.0)
59
+ rubocop (~> 1.19)
60
+ ruby-progressbar (1.11.0)
61
+ ruby2_keywords (0.0.5)
62
+ unicode-display_width (2.1.0)
63
+ webmock (3.14.0)
64
+ addressable (>= 2.8.0)
65
+ crack (>= 0.3.2)
66
+ hashdiff (>= 0.4.0, < 2.0.0)
67
+
68
+ PLATFORMS
69
+ x86_64-darwin-21
70
+
71
+ DEPENDENCIES
72
+ paypal-rest!
73
+ rack (~> 2.2)
74
+ rake (~> 13.0)
75
+ rspec (~> 3.11)
76
+ rubocop (~> 1.7)
77
+ rubocop-rake (~> 0.6)
78
+ rubocop-rspec (~> 2.9)
79
+ webmock (~> 3.14)
80
+
81
+ BUNDLED WITH
82
+ 2.3.10
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Smart/Casual Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Paypal::REST
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'paypal-rest'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle install
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install paypal-rest
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/SmartCasual/paypal-rest.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+ require "rspec/core/rake_task"
6
+
7
+ RuboCop::RakeTask.new
8
+ RSpec::Core::RakeTask.new(:spec)
9
+
10
+ task default: %i[rubocop spec]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "paypal/rest"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,47 @@
1
+ require_relative "connection"
2
+
3
+ module Paypal
4
+ class REST
5
+ class BearerToken
6
+ def expired?
7
+ return false if expires_at.nil?
8
+
9
+ Util.now > (expires_at - 60)
10
+ end
11
+
12
+ def to_s
13
+ token
14
+ end
15
+
16
+ private
17
+
18
+ def token
19
+ response[:access_token]
20
+ end
21
+
22
+ def expires_at
23
+ if (expires_in = response[:expires_in])
24
+ Util.now + expires_in.to_i
25
+ end
26
+ end
27
+
28
+ def response
29
+ @response ||= connection.post("/v1/oauth2/token", grant_type: "client_credentials").body.tap do |body|
30
+ body = Util.deep_symbolize_keys(body)
31
+
32
+ raise body[:message] if body[:name] == "Bad Request"
33
+ raise body[:error_description] if body.has_key?(:error_description)
34
+ end
35
+ end
36
+
37
+ def connection
38
+ @connection ||= Connection.new do |faraday|
39
+ faraday.request :authorization, :basic, config.client_id, config.client_secret
40
+
41
+ faraday.request :url_encoded
42
+ faraday.response :json
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ require "delegate"
2
+ require "faraday"
3
+ require "faraday/retry"
4
+
5
+ module Paypal
6
+ class REST
7
+ class Connection < SimpleDelegator
8
+ def initialize(&block)
9
+ connection = Faraday.new(url: config.api_endpoint) do |faraday|
10
+ faraday.request :retry
11
+ faraday.response :logger, logger, bodies: config.log_response_bodies
12
+
13
+ block.call(faraday) if Util.present?(block)
14
+ end
15
+
16
+ super(connection)
17
+ end
18
+
19
+ private
20
+
21
+ def config
22
+ @config ||= Paypal::REST.configuration
23
+ end
24
+
25
+ def logger
26
+ @logger ||= (config.logger || Logger.new($stdout))
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ module Paypal
2
+ class REST
3
+ VERSION = "1.0.0".freeze
4
+ end
5
+ end
@@ -0,0 +1,75 @@
1
+ require "English"
2
+
3
+ require_relative "util"
4
+ require_relative "rest/bearer_token"
5
+ require_relative "rest/connection"
6
+
7
+ module Paypal
8
+ class REST
9
+ class << self
10
+ def configure(&block)
11
+ block.call(configuration)
12
+ end
13
+
14
+ def configuration
15
+ @configuration ||= RESTConfiguration.new
16
+ end
17
+
18
+ def clear_configuration
19
+ @configuration = nil
20
+ end
21
+
22
+ def create_order(params)
23
+ post("/v2/checkout/orders", params)[:id]
24
+ end
25
+
26
+ def capture_payment_for_order(order_id)
27
+ post("/v2/checkout/orders/#{order_id}/capture")
28
+ end
29
+
30
+ def reset_connection
31
+ @connection = nil
32
+ @bearer_token = nil
33
+ end
34
+
35
+ private
36
+
37
+ def post(path, params = {})
38
+ response = connection.post(path, params).body
39
+
40
+ response = JSON.parse(response) if response.is_a?(String)
41
+ response = Util.deep_symbolize_keys(response)
42
+
43
+ raise response[:error_description] if response.has_key?(:error_description)
44
+
45
+ response
46
+ rescue JSON::ParserError, Faraday::ClientError => e
47
+ Rails.logger.error(["#{self.class} - #{e.class}: #{e.message}", e.backtrace].join($INPUT_RECORD_SEPARATOR))
48
+ head :unprocessable_entity
49
+ end
50
+
51
+ def connection
52
+ @connection ||= Connection.new do |faraday|
53
+ faraday.request :json
54
+ faraday.response :json
55
+
56
+ faraday.request :authorization, "Bearer", bearer_token
57
+ end
58
+ end
59
+
60
+ def bearer_token
61
+ return @bearer_token if @bearer_token && !@bearer_token.expired?
62
+
63
+ @bearer_token = BearerToken.new
64
+ end
65
+ end
66
+ end
67
+
68
+ RESTConfiguration = Struct.new("RESTConfiguration",
69
+ :api_endpoint,
70
+ :client_id,
71
+ :client_secret,
72
+ :log_response_bodies,
73
+ :logger,
74
+ )
75
+ end
@@ -0,0 +1,25 @@
1
+ module Paypal
2
+ module Util
3
+ def self.deep_symbolize_keys(hash)
4
+ hash.each_with_object({}) do |(key, value), memo|
5
+ memo[key.to_sym] = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
6
+ end
7
+ end
8
+
9
+ def self.blank?(value)
10
+ value.respond_to?(:empty?) ? value.empty? : !value
11
+ end
12
+
13
+ def self.present?(...)
14
+ !blank?(...)
15
+ end
16
+
17
+ def self.now
18
+ if Time.respond_to?(:zone)
19
+ Time.zone.now
20
+ else
21
+ Time.now
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,140 @@
1
+ require_relative "x509_certificate"
2
+
3
+ module Paypal
4
+ module Webhooks
5
+ class Event # rubocop:disable Metrics/ClassLength
6
+ VALID_PAYPAL_CERT_CN = "messageverificationcerts.paypal.com".freeze
7
+ VALID_PAYPAL_CERT_HOST = "api.paypal.com".freeze
8
+
9
+ class << self
10
+ def cert_cache
11
+ @cert_cache ||= {}
12
+ end
13
+
14
+ def clear_cert_cache
15
+ @cert_cache = nil
16
+ end
17
+ end
18
+
19
+ def initialize(params:, request:)
20
+ @request = request
21
+ @body = request.body&.read
22
+ @params = params
23
+ end
24
+
25
+ def verify!
26
+ raise EventVerificationFailed unless verify
27
+ end
28
+
29
+ def verify
30
+ verify_cert_url && verify_cert && verify_signature
31
+ end
32
+
33
+ def data
34
+ @data ||= Util.deep_symbolize_keys(@params)
35
+ end
36
+
37
+ private
38
+
39
+ def verify_signature
40
+ cert.verify(
41
+ algorithm:,
42
+ signature:,
43
+ fingerprint:,
44
+ ).tap do |result|
45
+ logger.debug("Bad signature") unless result
46
+ end
47
+ end
48
+
49
+ def verify_cert
50
+ verify_common_name && verify_date
51
+ end
52
+
53
+ def verify_common_name
54
+ (cert.common_name == VALID_PAYPAL_CERT_CN).tap do |result|
55
+ unless result
56
+ logger.debug {
57
+ "Incorrect common name #{cert.common_name} (expected #{VALID_PAYPAL_CERT_CN})"
58
+ }
59
+ end
60
+ end
61
+ end
62
+
63
+ def verify_date
64
+ cert.in_date?.tap do |result|
65
+ logger.debug("Not in date") unless result
66
+ end
67
+ end
68
+
69
+ def verify_cert_url
70
+ return false if Util.blank?(cert_url)
71
+
72
+ verify_cert_url_scheme && verify_cert_url_host
73
+ end
74
+
75
+ def verify_cert_url_scheme
76
+ (cert_url.scheme == "https").tap do |result|
77
+ logger.debug { "#{cert_url.scheme} is not HTTPS" } unless result
78
+ end
79
+ end
80
+
81
+ def verify_cert_url_host
82
+ (cert_url.host == VALID_PAYPAL_CERT_HOST).tap do |result|
83
+ unless result
84
+ logger.debug {
85
+ "Incorrect cert URL host #{cert_url.host} (expected #{VALID_PAYPAL_CERT_HOST})"
86
+ }
87
+ end
88
+ end
89
+ end
90
+
91
+ def algorithm
92
+ case (algo = get_header("HTTP_PAYPAL_AUTH_ALGO"))
93
+ when "SHA256withRSA" then "SHA256"
94
+ else
95
+ algo
96
+ end
97
+ end
98
+
99
+ def signature
100
+ get_header("HTTP_PAYPALAUTH_SIGNATURE")
101
+ end
102
+
103
+ def cert_url
104
+ @cert_url ||= URI.parse(get_header("HTTP_PAYPAL_CERT_URL")) if has_header?("HTTP_PAYPAL_CERT_URL")
105
+ end
106
+
107
+ def cert
108
+ self.class.cert_cache[cert_url.to_s] ||= X509Certificate.new(cert_url.open.read)
109
+ end
110
+
111
+ # https://developer.paypal.com/api/rest/webhooks/#link-eventheadervalidation
112
+ def fingerprint
113
+ [
114
+ get_header("HTTP_PAYPAL_TRANSMISSION_ID"),
115
+ get_header("HTTP_PAYPAL_TRANSMISSION_TIME"),
116
+ config.webhook_id,
117
+ Zlib.crc32(@body),
118
+ ].join("|")
119
+ end
120
+
121
+ def config
122
+ @config ||= Paypal::Webhooks.configuration
123
+ end
124
+
125
+ def logger
126
+ @logger ||= (config.logger || Logger.new($stdout))
127
+ end
128
+
129
+ def get_header(...)
130
+ @request.get_header(...)
131
+ end
132
+
133
+ def has_header?(...) # rubocop:disable Naming/PredicateName
134
+ @request.has_header?(...)
135
+ end
136
+ end
137
+
138
+ class EventVerificationFailed < StandardError; end
139
+ end
140
+ end
@@ -0,0 +1,33 @@
1
+ module Paypal
2
+ module Webhooks
3
+ class X509Certificate
4
+ def initialize(data)
5
+ @cert = OpenSSL::X509::Certificate.new(data)
6
+ end
7
+
8
+ def common_name
9
+ @common_name ||= get_subject_entry("CN")
10
+ end
11
+
12
+ def in_date?
13
+ Util.now.between?(@cert.not_before, @cert.not_after)
14
+ end
15
+
16
+ def verify(algorithm:, signature:, fingerprint:)
17
+ @cert.public_key.verify_pss(
18
+ algorithm,
19
+ signature,
20
+ fingerprint,
21
+ salt_length: :auto,
22
+ mgf1_hash: algorithm,
23
+ )
24
+ end
25
+
26
+ private
27
+
28
+ def get_subject_entry(key)
29
+ @cert.subject.to_a.assoc(key)[1]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ require_relative "util"
2
+
3
+ module Paypal
4
+ module Webhooks
5
+ class << self
6
+ def configure(&block)
7
+ block.call(configuration)
8
+ end
9
+
10
+ def configuration
11
+ @configuration ||= WebhooksConfiguration.new
12
+ end
13
+
14
+ def clear_configuration
15
+ @configuration = nil
16
+ end
17
+ end
18
+ end
19
+
20
+ WebhooksConfiguration = Struct.new("WebhooksConfiguration", :logger, :webhook_id)
21
+ end
22
+
23
+ require_relative "webhooks/event"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/paypal/rest/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "paypal-rest"
7
+ spec.version = Paypal::REST::VERSION
8
+ spec.authors = ["Elliot Crosby-McCullough"]
9
+ spec.email = ["elliot.cm@gmail.com"]
10
+
11
+ spec.summary = "Unofficial Ruby wrapper for the PayPal REST API"
12
+ spec.homepage = "https://github.com/SmartCasual/paypal-rest"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.1.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "https://github.com/SmartCasual/paypal-rest/blob/#{spec.version}/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) {
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ }
26
+ spec.require_paths = ["lib"]
27
+ spec.metadata["rubygems_mfa_required"] = "true"
28
+
29
+ spec.add_runtime_dependency "faraday", "~> 2.2"
30
+ spec.add_runtime_dependency "faraday-retry", "~> 1.0"
31
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paypal-rest
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Elliot Crosby-McCullough
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ description:
42
+ email:
43
+ - elliot.cm@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".circleci/config.yml"
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - ".rubocop.yml"
52
+ - ".ruby-version"
53
+ - CHANGELOG.md
54
+ - Gemfile
55
+ - Gemfile.lock
56
+ - LICENCE
57
+ - README.md
58
+ - Rakefile
59
+ - bin/console
60
+ - bin/setup
61
+ - lib/paypal/rest.rb
62
+ - lib/paypal/rest/bearer_token.rb
63
+ - lib/paypal/rest/connection.rb
64
+ - lib/paypal/rest/version.rb
65
+ - lib/paypal/util.rb
66
+ - lib/paypal/webhooks.rb
67
+ - lib/paypal/webhooks/event.rb
68
+ - lib/paypal/webhooks/x509_certificate.rb
69
+ - paypal-rest.gemspec
70
+ homepage: https://github.com/SmartCasual/paypal-rest
71
+ licenses: []
72
+ metadata:
73
+ allowed_push_host: https://rubygems.org
74
+ homepage_uri: https://github.com/SmartCasual/paypal-rest
75
+ source_code_uri: https://github.com/SmartCasual/paypal-rest
76
+ changelog_uri: https://github.com/SmartCasual/paypal-rest/blob/1.0.0/CHANGELOG.md
77
+ rubygems_mfa_required: 'true'
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.1.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 3.3.7
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: Unofficial Ruby wrapper for the PayPal REST API
97
+ test_files: []