opensrs 0.3.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/rubocop-analysis.yml +43 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +131 -0
- data/Gemfile +2 -11
- data/Gemfile.lock +89 -43
- data/{LICENSE → LICENSE.txt} +4 -2
- data/README.md +126 -0
- data/Rakefile +1 -40
- data/SECURITY.md +12 -0
- data/bin/console +9 -0
- data/bin/setup +7 -0
- data/lib/opensrs.rb +7 -6
- data/lib/opensrs/response.rb +18 -12
- data/lib/opensrs/sanitizable_string.rb +21 -0
- data/lib/opensrs/server.rb +55 -27
- data/lib/opensrs/version.rb +1 -3
- data/lib/opensrs/xml_processor.rb +36 -37
- data/lib/opensrs/xml_processor/libxml.rb +66 -45
- data/lib/opensrs/xml_processor/nokogiri.rb +57 -38
- data/opensrs.gemspec +28 -72
- metadata +111 -54
- data/README.rdoc +0 -105
- data/spec/opensrs/server_spec.rb +0 -109
- data/spec/opensrs/version_spec.rb +0 -9
- data/spec/opensrs/xml_processor/libxml_spec.rb +0 -254
- data/spec/opensrs/xml_processor/nokogiri_spec.rb +0 -228
- data/spec/spec_helper.rb +0 -6
data/Rakefile
CHANGED
@@ -1,40 +1 @@
|
|
1
|
-
require '
|
2
|
-
require 'bundler'
|
3
|
-
|
4
|
-
begin
|
5
|
-
Bundler.setup(:default, :development)
|
6
|
-
rescue Bundler::BundlerError => e
|
7
|
-
$stderr.puts e.message
|
8
|
-
$stderr.puts "Run `bundle install` to install missing gems"
|
9
|
-
exit e.status_code
|
10
|
-
end
|
11
|
-
|
12
|
-
$LOAD_PATH.unshift("lib")
|
13
|
-
|
14
|
-
require 'rake'
|
15
|
-
require 'opensrs'
|
16
|
-
|
17
|
-
begin
|
18
|
-
require 'jeweler'
|
19
|
-
|
20
|
-
Jeweler::Tasks.new do |gem|
|
21
|
-
gem.name = "opensrs"
|
22
|
-
gem.version = OpenSRS::Version::VERSION
|
23
|
-
gem.summary = "Provides support to utilize the OpenSRS API with Ruby/Rails."
|
24
|
-
gem.description = "Provides support to utilize the OpenSRS API with Ruby/Rails."
|
25
|
-
gem.email = "jdelsman@voxxit.com"
|
26
|
-
gem.homepage = "http://github.com/voxxit/opensrs"
|
27
|
-
gem.license = "MIT"
|
28
|
-
gem.authors = ["Josh Delsman"]
|
29
|
-
|
30
|
-
# Requirements are in Gemfile
|
31
|
-
end
|
32
|
-
rescue LoadError
|
33
|
-
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
34
|
-
end
|
35
|
-
|
36
|
-
require 'rspec/core/rake_task'
|
37
|
-
|
38
|
-
RSpec::Core::RakeTask.new(:spec)
|
39
|
-
|
40
|
-
task :default => :spec
|
1
|
+
require 'bundler/gem_tasks'
|
data/SECURITY.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Supported Versions
|
4
|
+
|
5
|
+
| Version | Supported |
|
6
|
+
| ------- | ------------------ |
|
7
|
+
| 0.4.x | :white_check_mark: |
|
8
|
+
| < 0.4.x | :x: |
|
9
|
+
|
10
|
+
## Reporting a Vulnerability
|
11
|
+
|
12
|
+
Please encyrpt your message to me at https://keybase.io/encrypt#jdelsman, then send to j@srv.im. Since this is an unsupported open-source project, I will get to it as quickly as I can.
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/opensrs.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require 'opensrs/xml_processor'
|
2
|
+
require 'opensrs/server'
|
3
|
+
require 'opensrs/response'
|
4
|
+
require 'opensrs/sanitizable_string'
|
5
|
+
require 'opensrs/version'
|
6
6
|
|
7
|
+
# OpenSRS
|
7
8
|
module OpenSRS
|
8
|
-
end
|
9
|
+
end
|
data/lib/opensrs/response.rb
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
module OpenSRS
|
2
|
+
# Response
|
2
3
|
class Response
|
3
4
|
attr_reader :request_xml, :response_xml
|
4
5
|
attr_accessor :response, :success
|
5
|
-
|
6
|
+
|
6
7
|
def initialize(parsed_response, request_xml, response_xml)
|
7
8
|
@response = parsed_response
|
8
9
|
@request_xml = request_xml
|
9
10
|
@response_xml = response_xml
|
10
11
|
@success = success?
|
11
12
|
end
|
12
|
-
|
13
|
+
|
13
14
|
# We need to return the error message unless the
|
14
15
|
# response is successful.
|
15
16
|
def errors
|
16
|
-
if
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
17
|
+
return if success?
|
18
|
+
|
19
|
+
msg = @response['response_text']
|
20
|
+
code = @response['response_code']
|
21
|
+
|
22
|
+
msg && code ? "#{msg} (Code #{code})" : 'Unknown error'
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
|
+
# If 'is_success' is returned, the API is letting us know that they
|
26
|
+
# will explicitly tell us whether something has succeeded or not.
|
27
|
+
#
|
28
|
+
# Otherwise, just assume it failed.
|
25
29
|
def success?
|
26
|
-
|
30
|
+
return false unless @response['is_success']
|
31
|
+
|
32
|
+
@response['is_success'] == '1'
|
27
33
|
end
|
28
34
|
end
|
29
|
-
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module OpenSRS
|
4
|
+
# SanitizableString
|
5
|
+
class SanitizableString < SimpleDelegator
|
6
|
+
@enable_sanitization = false
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :enable_sanitization
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(original_string, sanitized_string)
|
13
|
+
super(original_string)
|
14
|
+
@sanitized_string = sanitized_string
|
15
|
+
end
|
16
|
+
|
17
|
+
def sanitized
|
18
|
+
self.class.enable_sanitization ? @sanitized_string : self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/opensrs/server.rb
CHANGED
@@ -1,37 +1,53 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'openssl'
|
5
5
|
|
6
6
|
module OpenSRS
|
7
|
-
class
|
8
|
-
class TimeoutError < StandardError; end
|
7
|
+
class OpenSRSError < StandardError; end
|
9
8
|
|
9
|
+
class BadResponse < OpenSRSError; end
|
10
|
+
|
11
|
+
class ConnectionError < OpenSRSError; end
|
12
|
+
|
13
|
+
class TimeoutError < ConnectionError; end
|
14
|
+
|
15
|
+
# Server
|
10
16
|
class Server
|
11
|
-
attr_accessor :server, :username, :password, :key, :timeout, :open_timeout
|
17
|
+
attr_accessor :server, :username, :password, :key, :timeout, :open_timeout, :logger, :proxy
|
12
18
|
|
13
19
|
def initialize(options = {})
|
14
|
-
@server = URI.parse(options[:server] ||
|
20
|
+
@server = URI.parse(options[:server] || 'https://rr-n1-tor.opensrs.net:55443/')
|
15
21
|
@username = options[:username]
|
16
22
|
@password = options[:password]
|
17
23
|
@key = options[:key]
|
18
24
|
@timeout = options[:timeout]
|
19
|
-
@open_timeout
|
25
|
+
@open_timeout = options[:open_timeout]
|
26
|
+
@logger = options[:logger]
|
27
|
+
@proxy = URI.parse(options[:proxy]) if options[:proxy]
|
28
|
+
@sanitize_request = options[:sanitize_request]
|
29
|
+
|
30
|
+
OpenSRS::SanitizableString.enable_sanitization = @sanitize_request
|
20
31
|
end
|
21
32
|
|
22
33
|
def call(data = {})
|
23
|
-
xml = xml_processor.build({ :
|
34
|
+
xml = xml_processor.build({ protocol: 'XCP' }.merge!(data))
|
35
|
+
log('Request', xml.sanitized, data)
|
24
36
|
|
25
37
|
begin
|
26
38
|
response = http.post(server_path, xml, headers(xml))
|
39
|
+
log('Response', response.body, data)
|
27
40
|
rescue Net::HTTPBadResponse
|
28
|
-
raise OpenSRS::BadResponse,
|
41
|
+
raise OpenSRS::BadResponse,
|
42
|
+
'Received a bad response from OpenSRS. Please check that your IP address is added to the whitelist, and try again.'
|
29
43
|
end
|
30
44
|
|
31
45
|
parsed_response = xml_processor.parse(response.body)
|
32
|
-
|
33
|
-
rescue Timeout::Error =>
|
34
|
-
raise OpenSRS::TimeoutError,
|
46
|
+
OpenSRS::Response.new(parsed_response, xml.sanitized, response.body)
|
47
|
+
rescue Timeout::Error => e
|
48
|
+
raise OpenSRS::TimeoutError, e
|
49
|
+
rescue Errno::ECONNRESET, Errno::ECONNREFUSED => e
|
50
|
+
raise OpenSRS::ConnectionError, e
|
35
51
|
end
|
36
52
|
|
37
53
|
def xml_processor
|
@@ -39,37 +55,49 @@ module OpenSRS
|
|
39
55
|
end
|
40
56
|
|
41
57
|
def self.xml_processor=(name)
|
42
|
-
require
|
43
|
-
@@xml_processor = OpenSRS::XmlProcessor.const_get(
|
58
|
+
require "opensrs/xml_processor/#{name.to_s.downcase}"
|
59
|
+
@@xml_processor = OpenSRS::XmlProcessor.const_get(name.to_s.capitalize.to_s)
|
44
60
|
end
|
45
61
|
|
46
|
-
OpenSRS::Server.xml_processor = :libxml
|
47
|
-
|
48
62
|
private
|
49
63
|
|
50
64
|
def headers(request)
|
51
|
-
{
|
52
|
-
|
53
|
-
|
54
|
-
|
65
|
+
{
|
66
|
+
'Content-Length' => request.length.to_s,
|
67
|
+
'Content-Type' => 'text/xml',
|
68
|
+
'X-Username' => username,
|
69
|
+
'X-Signature' => signature(request)
|
55
70
|
}
|
56
71
|
end
|
57
72
|
|
58
73
|
def signature(request)
|
59
|
-
|
60
|
-
signature = Digest::MD5.hexdigest(signature + key)
|
61
|
-
signature
|
74
|
+
Digest::MD5.hexdigest(Digest::MD5.hexdigest(request + key) + key)
|
62
75
|
end
|
63
76
|
|
64
77
|
def http
|
65
|
-
http =
|
66
|
-
|
78
|
+
http = if @proxy
|
79
|
+
Net::HTTP.new(server.host, server.port, @proxy.host, @proxy.port, @proxy.user, @proxy.password)
|
80
|
+
else
|
81
|
+
Net::HTTP.new(server.host, server.port)
|
82
|
+
end
|
83
|
+
|
84
|
+
http.use_ssl = (server.scheme == 'https')
|
67
85
|
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
68
86
|
http.read_timeout = http.open_timeout = @timeout if @timeout
|
69
87
|
http.open_timeout = @open_timeout if @open_timeout
|
70
88
|
http
|
71
89
|
end
|
72
90
|
|
91
|
+
def log(type, data, options = {})
|
92
|
+
return unless logger
|
93
|
+
|
94
|
+
message = "[OpenSRS] #{type} XML"
|
95
|
+
message = "#{message} for #{options[:object]} #{options[:action]}" if options[:object] && options[:action]
|
96
|
+
|
97
|
+
line = [message, data].join("\n")
|
98
|
+
logger.info(line)
|
99
|
+
end
|
100
|
+
|
73
101
|
def server_path
|
74
102
|
server.path.empty? ? '/' : server.path
|
75
103
|
end
|
data/lib/opensrs/version.rb
CHANGED
@@ -1,59 +1,59 @@
|
|
1
1
|
module OpenSRS
|
2
|
-
|
2
|
+
# XmlProcessor
|
3
3
|
class XmlProcessor
|
4
|
-
|
5
4
|
# Parses the main data block from OpenSRS and discards
|
6
5
|
# the rest of the response.
|
7
6
|
def self.parse(response)
|
8
7
|
data_block = data_block_element(response)
|
9
8
|
|
10
|
-
raise ArgumentError
|
9
|
+
raise ArgumentError, 'No data found in document' unless data_block
|
11
10
|
|
12
|
-
|
11
|
+
decode_data(data_block)
|
13
12
|
end
|
14
13
|
|
15
|
-
protected
|
16
|
-
|
17
14
|
# Encodes individual elements, and their child elements, for the root XML document.
|
18
15
|
def self.encode_data(data, container = nil)
|
19
|
-
case data
|
20
|
-
when
|
21
|
-
|
22
|
-
when
|
23
|
-
|
16
|
+
case data
|
17
|
+
when Array
|
18
|
+
encode_dt_array(data, container)
|
19
|
+
when Hash
|
20
|
+
encode_dt_assoc(data, container)
|
21
|
+
when String, Numeric, Date, Time, Symbol, NilClass
|
22
|
+
data.to_s
|
24
23
|
else
|
25
|
-
|
24
|
+
data.inspect
|
26
25
|
end
|
27
|
-
|
28
|
-
return nil
|
29
26
|
end
|
30
27
|
|
31
28
|
def self.encode_dt_array(data, container)
|
32
|
-
|
33
|
-
|
34
|
-
data.each_with_index do |item, index|
|
35
|
-
item_node = new_element(:item, container)
|
36
|
-
item_node["key"] = index.to_s
|
37
|
-
item_node << encode_data(item, item_node)
|
38
|
-
|
39
|
-
dt_array << item_node
|
40
|
-
end
|
41
|
-
|
42
|
-
return dt_array
|
29
|
+
build_element(:dt_array, data, container)
|
43
30
|
end
|
44
31
|
|
45
32
|
def self.encode_dt_assoc(data, container)
|
46
|
-
|
33
|
+
build_element(:dt_assoc, data, container)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.build_element(type, data, container)
|
37
|
+
element = new_element(type, container)
|
47
38
|
|
48
|
-
|
39
|
+
# if array, item = the item
|
40
|
+
# if hash, item will be array of the key & value
|
41
|
+
data.each_with_index do |item, index|
|
49
42
|
item_node = new_element(:item, container)
|
50
|
-
item_node[
|
51
|
-
|
43
|
+
item_node['key'] = item.is_a?(Array) ? item[0].to_s : index.to_s
|
44
|
+
|
45
|
+
value = item.is_a?(Array) ? item[1] : item
|
52
46
|
|
53
|
-
|
47
|
+
encoded_data = encode_data(value, item_node)
|
48
|
+
if encoded_data.is_a?(String)
|
49
|
+
item_node.content = encoded_data
|
50
|
+
else
|
51
|
+
item_node << encoded_data
|
52
|
+
end
|
53
|
+
element << item_node
|
54
54
|
end
|
55
55
|
|
56
|
-
|
56
|
+
element
|
57
57
|
end
|
58
58
|
|
59
59
|
# Recursively decodes individual data elements from OpenSRS
|
@@ -61,17 +61,16 @@ module OpenSRS
|
|
61
61
|
def self.decode_data(data)
|
62
62
|
data.each do |element|
|
63
63
|
case element.name
|
64
|
-
when
|
64
|
+
when 'dt_array'
|
65
65
|
return decode_dt_array_data(element)
|
66
|
-
when
|
66
|
+
when 'dt_assoc'
|
67
67
|
return decode_dt_assoc_data(element)
|
68
|
-
when
|
68
|
+
when 'text', 'item', 'dt_scalar'
|
69
69
|
next if element.content.strip.empty?
|
70
|
+
|
70
71
|
return element.content.strip
|
71
72
|
end
|
72
73
|
end
|
73
74
|
end
|
74
|
-
|
75
75
|
end
|
76
|
-
|
77
|
-
end
|
76
|
+
end
|
@@ -1,60 +1,81 @@
|
|
1
|
-
|
1
|
+
begin
|
2
|
+
require 'libxml'
|
3
|
+
rescue LoadError => e
|
4
|
+
warn 'Cannot find `libxml` gem. Please install it before using it as the XML processor'
|
5
|
+
raise e
|
6
|
+
end
|
2
7
|
|
3
8
|
module OpenSRS
|
4
|
-
class XmlProcessor
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# go ahead and build the entire XML document to send to OpenSRS.
|
9
|
-
def self.build(data)
|
10
|
-
xml = Document.new
|
11
|
-
xml.root = envelope = Node.new("OPS_envelope")
|
12
|
-
|
13
|
-
envelope << header = Node.new("header")
|
14
|
-
envelope << body = Node.new("body")
|
15
|
-
header << Node.new("version", "0.9")
|
16
|
-
body << data_block = Node.new("data_block")
|
17
|
-
|
18
|
-
data_block << encode_data(data, data_block)
|
19
|
-
|
20
|
-
return xml.to_s
|
21
|
-
end
|
9
|
+
class XmlProcessor
|
10
|
+
# Libxml
|
11
|
+
class Libxml < OpenSRS::XmlProcessor
|
12
|
+
include ::LibXML::XML
|
22
13
|
|
23
|
-
|
14
|
+
# First, builds REXML elements for the inputted data. Then, it will
|
15
|
+
# go ahead and build the entire XML document to send to OpenSRS.
|
16
|
+
def self.build(data)
|
17
|
+
xml = Document.new
|
18
|
+
xml.root = envelope = Node.new('OPS_envelope')
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
envelope << header = Node.new('header')
|
21
|
+
envelope << body = Node.new('body')
|
22
|
+
header << Node.new('version', '0.9')
|
23
|
+
body << data_block = Node.new('data_block')
|
29
24
|
|
30
|
-
|
31
|
-
dt_array = []
|
25
|
+
data_block << encode_data(data, data_block)
|
32
26
|
|
33
|
-
|
34
|
-
next if item.empty?
|
35
|
-
dt_array[item.attributes["key"].to_i] = decode_data(item)
|
27
|
+
OpenSRS::SanitizableString.new(xml.to_s, sanitize(xml).to_s)
|
36
28
|
end
|
37
29
|
|
38
|
-
|
39
|
-
|
30
|
+
def self.sanitize(doc)
|
31
|
+
# Before changing the iteration through the nodes, read:
|
32
|
+
# https://www.rubydoc.info/gems/libxml-ruby/LibXML/XML/Document#find-instance_method
|
40
33
|
|
41
|
-
|
42
|
-
|
34
|
+
username_nodes = doc.find("//item[@key='reg_username']")
|
35
|
+
username_nodes.each { |node| node.content = 'FILTERED' }
|
43
36
|
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
password_nodes = doc.find("//item[@key='reg_password']")
|
38
|
+
password_nodes.each { |node| node.content = 'FILTERED' }
|
39
|
+
|
40
|
+
doc
|
47
41
|
end
|
42
|
+
private_class_method :sanitize
|
48
43
|
|
49
|
-
|
50
|
-
|
44
|
+
def self.data_block_element(response)
|
45
|
+
doc = Parser.string(response).parse
|
46
|
+
doc.find('//OPS_envelope/body/data_block/*')
|
47
|
+
end
|
51
48
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
49
|
+
def self.decode_dt_array_data(element)
|
50
|
+
dt_array = []
|
51
|
+
|
52
|
+
element.children.each do |item|
|
53
|
+
next if item.empty?
|
54
|
+
|
55
|
+
dt_array[item.attributes['key'].to_i] = decode_data(item)
|
56
|
+
end
|
58
57
|
|
58
|
+
dt_array
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.decode_dt_assoc_data(element)
|
62
|
+
dt_assoc = {}
|
63
|
+
|
64
|
+
element.children.each do |item|
|
65
|
+
next if item.content.strip.empty?
|
66
|
+
|
67
|
+
dt_assoc[item.attributes['key']] = decode_data(item)
|
68
|
+
end
|
69
|
+
|
70
|
+
dt_assoc
|
71
|
+
end
|
72
|
+
|
73
|
+
# Accepts two parameters but uses only one; to keep the interface same as other xml parser classes
|
74
|
+
# Is that a side effect of Template pattern?
|
75
|
+
#
|
76
|
+
def self.new_element(element_name, _container)
|
77
|
+
Node.new(element_name.to_s)
|
78
|
+
end
|
79
|
+
end
|
59
80
|
end
|
60
|
-
end
|
81
|
+
end
|