omnipay 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.
- checksums.yaml +7 -0
- data/lib/omnipay.rb +32 -0
- data/lib/omnipay/adapter.rb +62 -0
- data/lib/omnipay/adapters/mangopay.rb +140 -0
- data/lib/omnipay/adapters/mangopay/client.rb +70 -0
- data/lib/omnipay/adapters/oneclicpay.rb +130 -0
- data/lib/omnipay/autosubmit_form.rb +43 -0
- data/lib/omnipay/callback_phase.rb +68 -0
- data/lib/omnipay/gateway.rb +85 -0
- data/lib/omnipay/request_phase.rb +76 -0
- data/lib/omnipay/signer.rb +34 -0
- data/lib/omnipay/version.rb +3 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c3c13dede2e2ea788ee716eb834b225b1cae1ca9
|
4
|
+
data.tar.gz: 949abeb3bde942deb0c11d8c011814b692f538f1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: afbd5ccc734ef955631e111ded2dbf447dc63ae3658069d7b72c1c8210dbee2eb707a989f5057dc0ec112f0a7dcfa43660b6f8d0aaa6b76a80155b6883331d09
|
7
|
+
data.tar.gz: d419fd51650c301b5c2a379fea3a11192921b1b585f146726af5ed2d933d0c1054afd28ac1c6d41cafb443e835e2075554048a85aa59faeddb8cb23af40934c3
|
data/lib/omnipay.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Omnipay
|
4
|
+
|
5
|
+
autoload :Gateway, 'omnipay/gateway'
|
6
|
+
autoload :Adapter, 'omnipay/adapter'
|
7
|
+
autoload :RequestPhase, 'omnipay/request_phase'
|
8
|
+
autoload :CallbackPhase, 'omnipay/callback_phase'
|
9
|
+
autoload :AutosubmitForm, 'omnipay/autosubmit_form'
|
10
|
+
autoload :Signer, 'omnipay/signer'
|
11
|
+
|
12
|
+
# Error codes
|
13
|
+
INVALID_RESPONSE = :invalid_response
|
14
|
+
CANCELATION = :cancelation
|
15
|
+
PAYMENT_REFUSED = :payment_refused
|
16
|
+
WRONG_SIGNATURE = :wrong_signature
|
17
|
+
|
18
|
+
# Configuration
|
19
|
+
class Configuration
|
20
|
+
include Singleton
|
21
|
+
attr_accessor :secret_token
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.configuration
|
25
|
+
Configuration.instance
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.configure
|
29
|
+
yield configuration
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Glue between the rack middleware and the adapter implementation
|
2
|
+
# Responsible for initializing, configuring the adapter, and formatting
|
3
|
+
# the responses for use in the middleware
|
4
|
+
|
5
|
+
module Omnipay
|
6
|
+
|
7
|
+
class Adapter
|
8
|
+
|
9
|
+
attr_reader :uid
|
10
|
+
|
11
|
+
def initialize(uid, callback_url, config, dynamic_config)
|
12
|
+
@uid = uid
|
13
|
+
@callback_url = callback_url
|
14
|
+
@config = config
|
15
|
+
@dynamic_config = dynamic_config
|
16
|
+
|
17
|
+
@strategy = build_strategy
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
@strategy != nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def request_phase(amount, opts)
|
25
|
+
@strategy.request_phase(amount, opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
def callback_hash(params)
|
29
|
+
@strategy.callback_hash(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def build_strategy
|
36
|
+
return nil unless strategy_class
|
37
|
+
|
38
|
+
strategy_class.new(@callback_url, strategy_config)
|
39
|
+
end
|
40
|
+
|
41
|
+
def strategy_class
|
42
|
+
params[:adapter]
|
43
|
+
end
|
44
|
+
|
45
|
+
def strategy_config
|
46
|
+
params[:config] || {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def params
|
50
|
+
return @params if @params
|
51
|
+
|
52
|
+
if @dynamic_config
|
53
|
+
@params = @dynamic_config.call(@uid) || {}
|
54
|
+
else
|
55
|
+
@params = @config || {}
|
56
|
+
end
|
57
|
+
|
58
|
+
@params ||= {}
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# Omnipay adapter for mangopay
|
2
|
+
# documentation : http://docs.mangopay.com/api-references/
|
3
|
+
#
|
4
|
+
# Configuration :
|
5
|
+
# - client_id:string (mandatory) : client id of your mangopay account
|
6
|
+
# - client_passphrase:string (mandatory) : the passphrase for your account
|
7
|
+
# - wallet_id:string (mandatory) : the wallet to be credited with the payments
|
8
|
+
|
9
|
+
require_relative 'mangopay/client'
|
10
|
+
|
11
|
+
|
12
|
+
module Omnipay
|
13
|
+
module Adapters
|
14
|
+
|
15
|
+
class Mangopay
|
16
|
+
|
17
|
+
attr_reader :client
|
18
|
+
|
19
|
+
def initialize(callback_url, config = {})
|
20
|
+
|
21
|
+
raise ArgumentError.new("Missing client_id, client_passphrase, or wallet_id parameter") unless [config[:client_id], config[:client_passphrase], config[:wallet_id]].all?
|
22
|
+
|
23
|
+
@client = Client.new(config[:client_id], config[:client_passphrase], :sandbox => !!config[:sandbox])
|
24
|
+
@callback_url = callback_url
|
25
|
+
@wallet_id = config[:wallet_id]
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def request_phase(amount, params = {})
|
31
|
+
|
32
|
+
transaction_id, redirect_url = create_web_payin(amount, params)
|
33
|
+
|
34
|
+
# Generate the path and query parameters from the returned redirect_url string
|
35
|
+
uri = URI(redirect_url)
|
36
|
+
|
37
|
+
return [
|
38
|
+
'GET',
|
39
|
+
"#{uri.scheme}://#{uri.host}#{uri.path}",
|
40
|
+
Rack::Utils.parse_nested_query(uri.query),
|
41
|
+
transaction_id
|
42
|
+
]
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def callback_hash(params)
|
48
|
+
|
49
|
+
transaction_id = params[:transactionId]
|
50
|
+
|
51
|
+
begin
|
52
|
+
response = @client.get "/payins/#{transaction_id}"
|
53
|
+
rescue Mangopay::Client::Error => e
|
54
|
+
return {
|
55
|
+
:success => false,
|
56
|
+
:error => Omnipay::INVALID_RESPONSE,
|
57
|
+
:error_message => "Could not fetch details of transaction #{transaction_id}"
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if the response is valid
|
62
|
+
if response['code'] != 200
|
63
|
+
return {
|
64
|
+
:success => false,
|
65
|
+
:error => Omnipay::INVALID_RESPONSE
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Successful transaction
|
71
|
+
if response['Status'] == 'SUCCEEDED'
|
72
|
+
{
|
73
|
+
:success => true,
|
74
|
+
:amount => response['DebitedFunds']['Amount'],
|
75
|
+
:transaction_id => transaction_id
|
76
|
+
}
|
77
|
+
else
|
78
|
+
|
79
|
+
# Cancelation
|
80
|
+
if ['101001', '101002'].include? response['ResultCode']
|
81
|
+
{
|
82
|
+
:success => false,
|
83
|
+
:error => Omnipay::CANCELATION
|
84
|
+
}
|
85
|
+
else
|
86
|
+
{
|
87
|
+
:success => false,
|
88
|
+
:error => Omnipay::PAYMENT_REFUSED,
|
89
|
+
:error_message => "Refused payment for transaction #{transaction_id}.\nCode : #{response['ResultCode']}\nMessage : #{response['ResultMessage']}"
|
90
|
+
}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def create_web_payin(amount, params)
|
99
|
+
|
100
|
+
# Create a user
|
101
|
+
random_key = "#{Time.now.to_i}-#{(0...3).map { ('a'..'z').to_a[rand(26)] }.join}"
|
102
|
+
user_params = {
|
103
|
+
:Email => "user-#{random_key}@host.tld",
|
104
|
+
:FirstName => "User #{random_key}",
|
105
|
+
:LastName => "User #{random_key}",
|
106
|
+
:Birthday => Time.now.to_i,
|
107
|
+
:Nationality => "FR",
|
108
|
+
:CountryOfResidence => "FR"
|
109
|
+
}
|
110
|
+
|
111
|
+
user_id = (@client.post('/users/natural', user_params))["Id"]
|
112
|
+
|
113
|
+
# Create the web payin
|
114
|
+
payin_params = {
|
115
|
+
:AuthorId => user_id,
|
116
|
+
:DebitedFunds => {
|
117
|
+
:Currency => 'EUR',
|
118
|
+
:Amount => amount
|
119
|
+
},
|
120
|
+
:Fees => {
|
121
|
+
:Currency => 'EUR',
|
122
|
+
:Amount => 0
|
123
|
+
},
|
124
|
+
:CreditedWalletId => @wallet_id,
|
125
|
+
:ReturnURL => @callback_url,
|
126
|
+
:Culture => (params[:locale] || 'fr').upcase,
|
127
|
+
:CardType => 'CB_VISA_MASTERCARD',
|
128
|
+
:SecureMode => 'FORCE'
|
129
|
+
}
|
130
|
+
|
131
|
+
payin = @client.post '/payins/card/web', payin_params
|
132
|
+
|
133
|
+
# Return the transaction reference, and the full redirection url
|
134
|
+
return [payin["Id"], payin["RedirectURL"]]
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# Client for the mangopay API
|
2
|
+
|
3
|
+
require 'httparty'
|
4
|
+
|
5
|
+
module Omnipay
|
6
|
+
module Adapters
|
7
|
+
class Mangopay
|
8
|
+
|
9
|
+
class Client
|
10
|
+
|
11
|
+
class Error < ::StandardError ; end
|
12
|
+
|
13
|
+
include HTTParty
|
14
|
+
|
15
|
+
format :json
|
16
|
+
|
17
|
+
headers 'Accept' => 'application/json'
|
18
|
+
headers 'Content-Type' => 'application/json'
|
19
|
+
|
20
|
+
def initialize(key, secret, opts = {})
|
21
|
+
@key = key
|
22
|
+
@secret = secret
|
23
|
+
|
24
|
+
if opts[:sandbox]
|
25
|
+
@base_uri = 'https://api.sandbox.mangopay.com/v2'
|
26
|
+
else
|
27
|
+
@base_uri = 'https://api.mangopay.com/v2'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(path)
|
32
|
+
response = self.class.get "/#{@key}#{path}",
|
33
|
+
:basic_auth => {:username => @key, :password => @secret},
|
34
|
+
:base_uri => @base_uri
|
35
|
+
|
36
|
+
check_errors(response)
|
37
|
+
|
38
|
+
response.parsed_response.merge("code" => response.code)
|
39
|
+
end
|
40
|
+
|
41
|
+
def post(path, params = {})
|
42
|
+
response = self.class.post "/#{@key}#{path}",
|
43
|
+
:body => params.to_json,
|
44
|
+
:basic_auth => {:username => @key, :password => @secret},
|
45
|
+
:base_uri => @base_uri
|
46
|
+
|
47
|
+
check_errors(response)
|
48
|
+
|
49
|
+
response.parsed_response.merge("code" => response.code)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def check_errors(response)
|
56
|
+
# nocommit, log the request :/
|
57
|
+
if response.code != 200
|
58
|
+
error_message = response.parsed_response.merge(
|
59
|
+
"code" => response.code
|
60
|
+
)
|
61
|
+
|
62
|
+
raise Error, error_message.inspect
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# Omnipay adapter for oneclicpay
|
2
|
+
# documentation : docs.oneclicpay.com
|
3
|
+
#
|
4
|
+
# Configuration :
|
5
|
+
# - tpe_id:string (mandatory) : serial number of your virutal payment terminal
|
6
|
+
# - secret_key:string (mandatory) : security key for your account
|
7
|
+
# - sandbox:boolean (default: false) : use the sandbox or the production environment
|
8
|
+
|
9
|
+
require 'base64'
|
10
|
+
require 'digest'
|
11
|
+
require 'httparty'
|
12
|
+
|
13
|
+
module Omnipay
|
14
|
+
module Adapters
|
15
|
+
|
16
|
+
class Oneclicpay
|
17
|
+
|
18
|
+
HTTP_METHOD = 'POST'
|
19
|
+
|
20
|
+
REDIRECT_URLS = {
|
21
|
+
:sandbox => 'https://secure.homologation.oneclicpay.com',
|
22
|
+
:production => 'https://secure.oneclicpay.com'
|
23
|
+
}
|
24
|
+
|
25
|
+
VALIDATION_URLS = {
|
26
|
+
:sandbox => 'https://secure.homologation.oneclicpay.com:60000',
|
27
|
+
:production => 'https://secure.oneclicpay.com:60000'
|
28
|
+
}
|
29
|
+
|
30
|
+
def initialize(callback_url, config)
|
31
|
+
@callback_url = callback_url
|
32
|
+
|
33
|
+
@tpe_id = config[:tpe_id]
|
34
|
+
@secret_key = config[:secret_key]
|
35
|
+
@is_sandbox = config[:sandbox]
|
36
|
+
|
37
|
+
raise ArgumentError.new("Missing tpe_id or secret_key parameter") unless [@tpe_id, @secret_key].all?
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def request_phase(amount, params={})
|
42
|
+
product_name = params[:title] || ''
|
43
|
+
transaction_id = params[:transaction_id] || random_transaction_id
|
44
|
+
locale = params[:locale] || 'fr'
|
45
|
+
|
46
|
+
[
|
47
|
+
HTTP_METHOD,
|
48
|
+
redirect_url,
|
49
|
+
redirect_params_for(amount, product_name, transaction_id, locale),
|
50
|
+
transaction_id
|
51
|
+
]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def callback_hash(params)
|
56
|
+
|
57
|
+
if params[:result] == "NOK" && params[:reason] == "Abandon de la transaction."
|
58
|
+
return { :success => false, :error => Omnipay::CANCELATION }
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
if params[:result] == "OK"
|
63
|
+
|
64
|
+
# Validate the response via the API
|
65
|
+
transaction_id = params[:transactionId]
|
66
|
+
amount = get_transaction_amount(transaction_id)
|
67
|
+
|
68
|
+
if amount
|
69
|
+
{ :success => true, :amount => amount, :transaction_id => transaction_id }
|
70
|
+
else
|
71
|
+
{ :success => false, :error => Omnipay::INVALID_RESPONSE, :error_message => "Could not fetch the amount of the transaction #{transaction_id}" }
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
elsif params[:result] == "NOK"
|
76
|
+
{ :success => false, :error => Omnipay::PAYMENT_REFUSED, :error_message => params[:reason] }
|
77
|
+
|
78
|
+
else
|
79
|
+
{ :success => false, :error => Omnipay::INVALID_RESPONSE, :error_message => "No :result key in the params #{params.inspect}" }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def redirect_params_for(amount, product_name, transaction_id, locale)
|
87
|
+
{
|
88
|
+
:montant => (amount.to_f/100).to_s,
|
89
|
+
:idTPE => @tpe_id,
|
90
|
+
:idTransaction => transaction_id,
|
91
|
+
:devise => 'EUR',
|
92
|
+
:lang => locale,
|
93
|
+
:nom_produit => product_name,
|
94
|
+
:urlRetourOK => @callback_url,
|
95
|
+
:urlRetourNOK => @callback_url
|
96
|
+
}.tap{|params|
|
97
|
+
params[:sec] = signature(params)
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def random_transaction_id
|
102
|
+
"#{Time.now.to_i}-#{@tpe_id}-#{random_token}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def random_token
|
106
|
+
(0...3).map { ('a'..'z').to_a[rand(26)] }.join
|
107
|
+
end
|
108
|
+
|
109
|
+
def signature(params)
|
110
|
+
to_sign = (params.values + [@secret_key]).join('|')
|
111
|
+
Digest::SHA512.hexdigest(Base64.encode64(to_sign).gsub(/\n/, ''))
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_transaction_amount(transaction_id)
|
115
|
+
response = HTTParty.post("#{validation_url}/rest/payment/find?serialNumber=#{@tpe_id}&key=#{@secret_key}&transactionRef=#{transaction_id}")
|
116
|
+
(response.parsed_response["transaction"][0]["ok"] != 0) && response.parsed_response["transaction"][0]["amount"]
|
117
|
+
end
|
118
|
+
|
119
|
+
def redirect_url
|
120
|
+
@is_sandbox ? REDIRECT_URLS[:sandbox] : REDIRECT_URLS[:production]
|
121
|
+
end
|
122
|
+
|
123
|
+
def validation_url
|
124
|
+
@is_sandbox ? VALIDATION_URLS[:sandbox] : VALIDATION_URLS[:production]
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Builds an html page with an autosubmitted form, to handle POST redirections
|
2
|
+
module Omnipay
|
3
|
+
class AutosubmitForm
|
4
|
+
|
5
|
+
HEADER = <<-HEADER
|
6
|
+
<!DOCTYPE html>
|
7
|
+
<html>
|
8
|
+
<head>
|
9
|
+
<script type="text/javascript">
|
10
|
+
window.onload=function(){
|
11
|
+
document.getElementById('autosubmit-form').submit();
|
12
|
+
}
|
13
|
+
</script>
|
14
|
+
</head>
|
15
|
+
|
16
|
+
<body>
|
17
|
+
<h1>Redirecting...</h1>
|
18
|
+
HEADER
|
19
|
+
|
20
|
+
FOOTER = <<-FOOTER
|
21
|
+
</body>
|
22
|
+
</html>
|
23
|
+
FOOTER
|
24
|
+
|
25
|
+
def initialize(action, fields)
|
26
|
+
@action = action
|
27
|
+
@fields = fields.map{|name, value| {:name => name, :value => value}}
|
28
|
+
end
|
29
|
+
|
30
|
+
def html
|
31
|
+
HEADER + form_html + FOOTER
|
32
|
+
end
|
33
|
+
|
34
|
+
def form_html
|
35
|
+
" <form method=\"POST\" id=\"autosubmit-form\" action=\"#{@action}\">\n" +
|
36
|
+
@fields.map{|field|
|
37
|
+
" <input type=\"hidden\" name=\"#{field[:name]}\" value=\"#{field[:value]}\"/>\n"
|
38
|
+
}.join +
|
39
|
+
" </form>\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Class responsible for updating the request env in the callback phase
|
2
|
+
|
3
|
+
module Omnipay
|
4
|
+
class CallbackPhase
|
5
|
+
|
6
|
+
def initialize(request, adapter)
|
7
|
+
@request = request
|
8
|
+
@adapter = adapter
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def update_env!
|
13
|
+
|
14
|
+
# The request params, keys symbolized
|
15
|
+
params = Hash[@request.params.map{|k,v|[k.to_sym,v]}]
|
16
|
+
|
17
|
+
# Get the callback hash
|
18
|
+
callback_hash = @adapter.callback_hash(params)
|
19
|
+
|
20
|
+
# Check the signature
|
21
|
+
if callback_hash[:success] && !valid_signature?(callback_hash)
|
22
|
+
callback_hash = {
|
23
|
+
:success => false,
|
24
|
+
:error => Omnipay::WRONG_SIGNATURE,
|
25
|
+
:error_message => "Signatures do not match."
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# If not successful response, add informations to the error message
|
30
|
+
if !callback_hash[:success]
|
31
|
+
callback_hash[:error_message] = (callback_hash[:error_message] || callback_hash[:error].to_s) +
|
32
|
+
"\nAdapter : #{@adapter.uid}" +
|
33
|
+
"\nContext : #{context.inspect}" +
|
34
|
+
"\nStored signature : #{stored_signature}" +
|
35
|
+
"\nRequest : #{@request.inspect}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Store the response in the environment
|
39
|
+
@request.env['omnipay.response'] = callback_hash.merge(:raw => params, :context => context)
|
40
|
+
|
41
|
+
# Force GET request
|
42
|
+
@request.env['REQUEST_METHOD'] = 'GET'
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def valid_signature?(callback_hash)
|
49
|
+
callback_signature = Signer.new(callback_hash[:transaction_id], callback_hash[:amount], context).signature
|
50
|
+
|
51
|
+
callback_signature == stored_signature
|
52
|
+
end
|
53
|
+
|
54
|
+
def stored_signature
|
55
|
+
@stored_signature ||= @request.session['omnipay.signature'] && @request.session['omnipay.signature'].delete(@adapter.uid)
|
56
|
+
end
|
57
|
+
|
58
|
+
def callback_signature
|
59
|
+
@callback_signatureSigner.new(callback_hash[:transaction_id], callback_hash[:amount], context).signature
|
60
|
+
end
|
61
|
+
|
62
|
+
def context
|
63
|
+
@context ||= @request.session['omnipay.context'] && @request.session['omnipay.context'].delete(@adapter.uid)
|
64
|
+
@context ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Omnipay
|
4
|
+
|
5
|
+
# Gateway middleware
|
6
|
+
class Gateway
|
7
|
+
|
8
|
+
BASE_PATH = '/pay'
|
9
|
+
|
10
|
+
def initialize(app, options={}, &block)
|
11
|
+
@app = app
|
12
|
+
|
13
|
+
@adapter_options = options
|
14
|
+
@adapter_config_block = block
|
15
|
+
|
16
|
+
# Refreshed at each request
|
17
|
+
@uid = nil
|
18
|
+
@request = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
|
24
|
+
# Get the current request
|
25
|
+
@request = Rack::Request.new(env)
|
26
|
+
|
27
|
+
# Check if the path is good, and extract the uid
|
28
|
+
@uid = extract_uid_from_path(@request.path)
|
29
|
+
return @app.call(env) unless @uid
|
30
|
+
|
31
|
+
# Get the adapter config for this uid (to handle dynamic configuration)
|
32
|
+
adapter = Adapter.new(@uid, callback_url, @adapter_options, @adapter_config_block)
|
33
|
+
return @app.call(env) unless adapter.valid?
|
34
|
+
|
35
|
+
# Handle the request phase
|
36
|
+
if request_phase?
|
37
|
+
return RequestPhase.new(@request, adapter).response
|
38
|
+
end
|
39
|
+
|
40
|
+
# Handle the callback phase
|
41
|
+
if callback_phase?
|
42
|
+
CallbackPhase.new(@request, adapter).update_env!
|
43
|
+
end
|
44
|
+
|
45
|
+
# Forward to the app
|
46
|
+
@app.call(env)
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Check if the current request matches the request or callback phase
|
54
|
+
def request_phase?
|
55
|
+
@request.path == "#{BASE_PATH}/#{@uid}" && @request.request_method == 'GET'
|
56
|
+
end
|
57
|
+
|
58
|
+
def callback_phase?
|
59
|
+
@request.path == "#{BASE_PATH}/#{@uid}/callback"
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Extract the uid from the path
|
64
|
+
# If uid already defined, check if it matches
|
65
|
+
# "/pay/foobar/callback" => "foobar"
|
66
|
+
def extract_uid_from_path(path)
|
67
|
+
if path.start_with? BASE_PATH
|
68
|
+
uid = path.gsub(/^#{BASE_PATH}/, '').split('/')[1]
|
69
|
+
end
|
70
|
+
|
71
|
+
if !@adapter_options.empty?
|
72
|
+
uid = nil unless uid == @adapter_options[:uid]
|
73
|
+
end
|
74
|
+
|
75
|
+
uid
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# The callback url for a uid in the current host
|
80
|
+
def callback_url
|
81
|
+
"#{@request.base_url}#{BASE_PATH}/#{@uid}/callback"
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Class responsible for formatting the redirection in the request phase
|
2
|
+
|
3
|
+
module Omnipay
|
4
|
+
class RequestPhase
|
5
|
+
|
6
|
+
def initialize(request, adapter)
|
7
|
+
@request = request
|
8
|
+
@adapter = adapter
|
9
|
+
end
|
10
|
+
|
11
|
+
def response
|
12
|
+
method, url, params, transaction_id = @adapter.request_phase(amount, adapter_params)
|
13
|
+
|
14
|
+
context = store_context!
|
15
|
+
|
16
|
+
signature = Signer.new(transaction_id, amount, context).signature
|
17
|
+
store_signature!(signature)
|
18
|
+
|
19
|
+
if method == 'GET'
|
20
|
+
get_redirect_response(url, params)
|
21
|
+
elsif method == 'POST'
|
22
|
+
post_redirect_response(url, params)
|
23
|
+
else
|
24
|
+
raise TypeError.new('request_phase returned http method must be \'GET\' or \'POST\'')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def amount
|
32
|
+
@request.params['amount'].tap{ |amount|
|
33
|
+
raise ArgumentError.new('No amount specified') unless amount
|
34
|
+
}.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def adapter_params
|
38
|
+
params = @request.params.dup
|
39
|
+
|
40
|
+
params.delete 'amount'
|
41
|
+
params.delete 'context'
|
42
|
+
|
43
|
+
# Symbolize the keys
|
44
|
+
Hash[params.map{|k,v|[k.to_sym,v]}]
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_redirect_response(url, params)
|
48
|
+
redirect_url = url + '?' + Rack::Utils.build_query(params)
|
49
|
+
Rack::Response.new.tap{|response| response.redirect(redirect_url)}
|
50
|
+
end
|
51
|
+
|
52
|
+
def post_redirect_response(url, params)
|
53
|
+
form = AutosubmitForm.new(url, params)
|
54
|
+
Rack::Response.new([form.html], 200, {'Content-Type' => 'text/html;charset=utf-8'})
|
55
|
+
end
|
56
|
+
|
57
|
+
# Store the request's context in session
|
58
|
+
def store_context!
|
59
|
+
context = @request.params.delete('context')
|
60
|
+
|
61
|
+
if context
|
62
|
+
@request.session['omnipay.context'] ||= {}
|
63
|
+
@request.session['omnipay.context'][@adapter.uid] = context
|
64
|
+
end
|
65
|
+
|
66
|
+
return context
|
67
|
+
end
|
68
|
+
|
69
|
+
# Store the requests signature in the session
|
70
|
+
def store_signature!(signature)
|
71
|
+
@request.session['omnipay.signature'] ||= {}
|
72
|
+
@request.session['omnipay.signature'][@adapter.uid] = signature
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Class responsible for signing the outgoing payments in the request phase,
|
2
|
+
# and validating them in the callback phase
|
3
|
+
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module Omnipay
|
7
|
+
class Signer
|
8
|
+
|
9
|
+
def initialize(transaction_id, amount, context)
|
10
|
+
@transaction_id = transaction_id
|
11
|
+
@amount = amount
|
12
|
+
@context = context || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def signature
|
16
|
+
to_sign = "#{secret_token}:#{@transaction_id}:#{@amount}:#{self.class.hash_to_string @context}"
|
17
|
+
CGI.escape(Base64.encode64(OpenSSL::HMAC.digest('sha1', secret_token, to_sign)))
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# Unique key : to configure globally
|
22
|
+
def secret_token
|
23
|
+
Omnipay.configuration.secret_token
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def self.hash_to_string(hash)
|
29
|
+
# key/values appended by alphabetical key order
|
30
|
+
hash.sort_by{|k,_|k}.flatten.join('')
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: omnipay
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ClicRDV
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-01-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: httparty
|
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: Payment gateway abstraction for rack applications. Think omniauth for
|
42
|
+
off-site payment.
|
43
|
+
email:
|
44
|
+
- contact@clicrdv.com
|
45
|
+
executables: []
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files: []
|
48
|
+
files:
|
49
|
+
- lib/omnipay.rb
|
50
|
+
- lib/omnipay/adapter.rb
|
51
|
+
- lib/omnipay/adapters/mangopay.rb
|
52
|
+
- lib/omnipay/adapters/mangopay/client.rb
|
53
|
+
- lib/omnipay/adapters/oneclicpay.rb
|
54
|
+
- lib/omnipay/autosubmit_form.rb
|
55
|
+
- lib/omnipay/callback_phase.rb
|
56
|
+
- lib/omnipay/gateway.rb
|
57
|
+
- lib/omnipay/request_phase.rb
|
58
|
+
- lib/omnipay/signer.rb
|
59
|
+
- lib/omnipay/version.rb
|
60
|
+
homepage: https://github.com/clicrdv/omnipay
|
61
|
+
licenses: []
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project: '[none]'
|
79
|
+
rubygems_version: 2.0.6
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: Payment gateway abstraction for rack applications.
|
83
|
+
test_files: []
|