payhyper 0.1.0
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.
- data/Gemfile +2 -0
- data/lib/payhyper/json.rb +23 -0
- data/lib/payhyper/security.rb +21 -0
- data/lib/payhyper/version.rb +3 -0
- data/lib/payhyper.rb +104 -0
- data/payhyper.gemspec +22 -0
- metadata +88 -0
data/Gemfile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module PayHyper
|
2
|
+
module JSON
|
3
|
+
if MultiJson.respond_to?(:dump)
|
4
|
+
|
5
|
+
def self.dump(*args)
|
6
|
+
MultiJson.dump(*args)
|
7
|
+
end
|
8
|
+
def self.load(*args)
|
9
|
+
MultiJson.load(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
else
|
13
|
+
|
14
|
+
def self.dump(*args)
|
15
|
+
MultiJson.encode(*args)
|
16
|
+
end
|
17
|
+
def self.load(*args)
|
18
|
+
MultiJson.decode(*args)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module PayHyper
|
2
|
+
module Security
|
3
|
+
|
4
|
+
def self._sign(key, data)
|
5
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), key, data)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.sign(key, data)
|
9
|
+
Base64.encode64(_sign(key, data)).gsub("\n", "")
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.secure_compare(a, b)
|
13
|
+
return false if a.nil? || b.nil? || a.length.zero? || b.length.zero? || a.bytesize != b.bytesize
|
14
|
+
l = a.unpack "C#{a.bytesize}"
|
15
|
+
res = 0
|
16
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
17
|
+
res == 0
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/lib/payhyper.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
require 'rest_client'
|
5
|
+
require 'multi_json'
|
6
|
+
|
7
|
+
require 'payhyper/version'
|
8
|
+
require 'payhyper/json'
|
9
|
+
require 'payhyper/security'
|
10
|
+
|
11
|
+
module PayHyper
|
12
|
+
|
13
|
+
BASE_URLS = {
|
14
|
+
:live => "http://api.payhyper.com",
|
15
|
+
:sandbox => "http://api.sandbox.payhyper.com",
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.setup(access_key_id, access_key_secret, mode)
|
19
|
+
raise PayHyperError, "Mode must be one of #{BASE_URLS.keys.map { |k| k.inspect }.join(" or ")}" unless BASE_URLS.keys.include?(mode)
|
20
|
+
@access_key_id = access_key_id
|
21
|
+
@access_key_secret = access_key_secret
|
22
|
+
@base_url = BASE_URLS[mode]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.raise_if_not_setup!
|
26
|
+
raise PayHyperError, "Must call setup() first" if @access_key_id.nil? || @access_key_secret.nil? || @base_url.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.create_order!(params)
|
30
|
+
raise_if_not_setup!
|
31
|
+
# == Validate ==
|
32
|
+
raise ValidationError, "Incorrect amount." if params[:amount].to_i <= 0
|
33
|
+
raise ValidationError, "Incorrect email." if params[:email].nil? || !params[:email].match(/\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/)
|
34
|
+
raise ValidationError, "Incorrect phone." if params[:phone].nil? || !params[:phone].match(/\A\+962[0-9]{8,9}\z/)
|
35
|
+
# == Do the request ==
|
36
|
+
body = JSON.dump({ :email => params[:email], :phone => params[:phone], :amount => params[:amount] })
|
37
|
+
failure = "Unknown error"
|
38
|
+
begin
|
39
|
+
uri = URI.parse(@base_url + "/v1/orders")
|
40
|
+
content_type = "application/json; charset=utf-8"
|
41
|
+
signature = Security.sign(@access_key_secret, ["POST", uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}", uri.request_uri, content_type, body.encode("UTF-8")].join("\n")) # Would be great if this could read directly from the request's headers.
|
42
|
+
res = RestClient.post(@base_url + "/v1/orders", body, :content_type => content_type, :authorization => "Hyper #{@access_key_id}:#{signature}")
|
43
|
+
failure = false
|
44
|
+
rescue RestClient::Exception => e # HTTP status codes not in 200-207, 301-303 and 307 result in a RestClient::Exception.
|
45
|
+
if e.http_code && e.http_code.to_i == 422 && e.http_body
|
46
|
+
raise ValidationError, JSON.load(e.http_body)["error"]
|
47
|
+
else
|
48
|
+
failure = "Remote returned HTTP status code #{e.http_code}"
|
49
|
+
end
|
50
|
+
rescue SocketError => e
|
51
|
+
if e.message.match(/getaddrinfo: (.*?)/)
|
52
|
+
failure = "Problem getting address information, possibly due to an incorrect domain name"
|
53
|
+
else
|
54
|
+
failure = "Unknown socket error"
|
55
|
+
end
|
56
|
+
rescue Errno::EINVAL => e
|
57
|
+
failure = "Incorrect host address"
|
58
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
|
59
|
+
failure = "Connection refused, or host unreachable"
|
60
|
+
rescue Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
|
61
|
+
failure = "Problem parsing HTTP response"
|
62
|
+
rescue EOFError, Errno::ECONNRESET => e
|
63
|
+
failure = "Remote closed connection unexpectedly"
|
64
|
+
end
|
65
|
+
if failure
|
66
|
+
raise CommunicationError, failure
|
67
|
+
else
|
68
|
+
JSON.load(res)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.check_ani_authenticity(request)
|
73
|
+
raise_if_not_setup!
|
74
|
+
authorization = request.env["HTTP_AUTHORIZATION"]
|
75
|
+
return false unless authorization
|
76
|
+
matches = authorization.match(/\A(.*) (.*):(.*)/)
|
77
|
+
return false unless matches
|
78
|
+
method, access_key_id, input_signature = matches.captures
|
79
|
+
return false unless method == "Hyper" && access_key_id && input_signature && access_key_id == @access_key_id
|
80
|
+
request.env["rack.input"].rewind # In case someone forgot to rewind the input.
|
81
|
+
body = request.env["rack.input"].read
|
82
|
+
request.env["rack.input"].rewind # Be nice to others.
|
83
|
+
canonical_request_representation = [request.env["REQUEST_METHOD"], request.env["HTTP_HOST"], request.env["PATH_INFO"], request.env["CONTENT_TYPE"], body].join("\n")
|
84
|
+
correct_signature = Security.sign(@access_key_secret, canonical_request_representation)
|
85
|
+
return Security.secure_compare(correct_signature, input_signature)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.parse_notification(request)
|
89
|
+
raise_if_not_setup!
|
90
|
+
if check_ani_authenticity(request)
|
91
|
+
request.env["rack.input"].rewind # In case someone forgot to rewind the input.
|
92
|
+
body = request.env["rack.input"].read
|
93
|
+
request.env["rack.input"].rewind # Be nice to others.
|
94
|
+
JSON.load(body)
|
95
|
+
else
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class PayHyperError < StandardError; end
|
101
|
+
class AuthenticationError < PayHyperError; end
|
102
|
+
class ValidationError < PayHyperError; end
|
103
|
+
class CommunicationError < PayHyperError; end
|
104
|
+
end
|
data/payhyper.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
2
|
+
|
3
|
+
require 'payhyper/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
|
7
|
+
s.name = 'payhyper'
|
8
|
+
s.version = PayHyper::VERSION
|
9
|
+
s.date = '2013-05-12'
|
10
|
+
s.summary = 'The Ruby bindings of the Hyper API'
|
11
|
+
s.description = 'Hyper is an API for cash collection. See http://payhyper.com for more.'
|
12
|
+
s.authors = ["Sinan Taifour"]
|
13
|
+
s.email = 'sinan@payhyper.com'
|
14
|
+
s.homepage = 'http://payhyper.com'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.require_paths = ['lib']
|
18
|
+
|
19
|
+
s.add_dependency('rest-client', '~> 1.4')
|
20
|
+
s.add_dependency('multi_json', '>= 1.0.4', '< 2')
|
21
|
+
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: payhyper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sinan Taifour
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rest-client
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.4'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.4'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: multi_json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.0.4
|
38
|
+
- - <
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 1.0.4
|
49
|
+
- - <
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '2'
|
52
|
+
description: Hyper is an API for cash collection. See http://payhyper.com for more.
|
53
|
+
email: sinan@payhyper.com
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files: []
|
57
|
+
files:
|
58
|
+
- Gemfile
|
59
|
+
- lib/payhyper.rb
|
60
|
+
- lib/payhyper/json.rb
|
61
|
+
- lib/payhyper/security.rb
|
62
|
+
- lib/payhyper/version.rb
|
63
|
+
- payhyper.gemspec
|
64
|
+
homepage: http://payhyper.com
|
65
|
+
licenses: []
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
require_paths:
|
69
|
+
- lib
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubyforge_project:
|
84
|
+
rubygems_version: 1.8.24
|
85
|
+
signing_key:
|
86
|
+
specification_version: 3
|
87
|
+
summary: The Ruby bindings of the Hyper API
|
88
|
+
test_files: []
|