omnipay 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|