opensrs 0.3.4 → 0.4.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.
- 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
|