sip2 0.0.9 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 66c5b3c4ac6909954ad5e3c0b11577cb97d17e91
4
- data.tar.gz: 47985f7e545caec1d2d71e7fdeda15dbf099af7a
2
+ SHA256:
3
+ metadata.gz: 4d3b9a457d4ca0131eb2039eda501a303c75c69e4891fc1128f75096279134e7
4
+ data.tar.gz: 6bb443020825760ba59150d79eb23490d220123ab0b86567535f6b89cee5a46a
5
5
  SHA512:
6
- metadata.gz: c59b28ec3bfb1c1e4604e0dc8e5196c91b994c0015c11a5badfc54e6cce2a4f45a4e4bbcae42fea24e0ee28a73f609353c609eaeb53a7109ed54d91f7b567c0c
7
- data.tar.gz: 6cab90f0952d40cc71664db95b746014ad4f996049523789be36d663539f44ddcae6f36420f496befc0c8f297bebbdccc483b1f6b98b9b4936fd025458282de2
6
+ metadata.gz: e0d0ce1e898a903b9713ab305631fa1e12a0d28eee2efad234b00c858a5d4aca2fef8e18449d1a1dd34ec2b0e549be6859eb9ace0d563a5037716ca62508c898
7
+ data.tar.gz: 800ff16573a4c25e76a8bcaf234dcf79745e3c3ee119d3276b8195ff8a0bc7fe1c6ad7eb0c1c920cce19076cd503e273ac3169b32ec7524e0f04872742a57557
@@ -1,10 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sip2/version'
2
4
 
5
+ require 'openssl'
6
+
3
7
  require 'sip2/patron_information'
4
8
 
5
9
  require 'sip2/messages/login'
6
10
  require 'sip2/messages/patron_information'
7
11
 
12
+ module Sip2
13
+ class TimeoutError < StandardError; end
14
+ class ConnectionTimeout < TimeoutError; end
15
+ class WriteTimeout < TimeoutError; end
16
+ class ReadTimeout < TimeoutError; end
17
+ end
18
+
8
19
  require 'sip2/non_blocking_socket'
9
20
  require 'sip2/connection'
10
21
  require 'sip2/client'
@@ -1,19 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
4
  #
3
5
  # Sip2 Client
4
6
  #
5
7
  class Client
6
- def initialize(host:, port:, ignore_error_detection: false)
8
+ def initialize(host:, port:, ignore_error_detection: false, timeout: nil, ssl_context: nil)
7
9
  @host = host
8
10
  @port = port
9
11
  @ignore_error_detection = ignore_error_detection
12
+ @timeout = timeout || NonBlockingSocket::DEFAULT_TIMEOUT
13
+ @ssl_context = ssl_context
10
14
  end
11
15
 
16
+ # rubocop:disable Metrics/MethodLength
12
17
  def connect
13
- socket = NonBlockingSocket.connect @host, @port
14
- yield Connection.new(socket, @ignore_error_detection) if block_given?
18
+ socket = NonBlockingSocket.connect host: @host, port: @port, timeout: @timeout
19
+
20
+ # If we've been provided with an SSL context then use it to wrap out existing connection
21
+ if @ssl_context
22
+ socket = ::OpenSSL::SSL::SSLSocket.new socket, @ssl_context
23
+ socket.hostname = @host # Needed for SNI
24
+ socket.sync_close = true
25
+ socket.connect
26
+ socket.post_connection_check @host # Validate the peer certificate matches the host
27
+ end
28
+
29
+ if block_given?
30
+ yield Connection.new(socket: socket, ignore_error_detection: @ignore_error_detection)
31
+ end
15
32
  ensure
16
- socket.close if socket
33
+ socket&.close
17
34
  end
35
+ # rubocop:enable Metrics/MethodLength
18
36
  end
19
37
  end
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
4
  #
3
5
  # Sip2 Connection
4
6
  #
5
7
  class Connection
8
+ LINE_SEPARATOR = "\r"
9
+
6
10
  @connection_modules = []
7
11
 
8
12
  class << self
@@ -16,12 +20,17 @@ module Sip2
16
20
  include Messages::Login
17
21
  include Messages::PatronInformation
18
22
 
19
- def initialize(socket, ignore_error_detection)
23
+ def initialize(socket:, ignore_error_detection: false)
20
24
  @socket = socket
21
25
  @ignore_error_detection = ignore_error_detection
22
26
  @sequence = 1
23
27
  end
24
28
 
29
+ def send_message(message)
30
+ write_with_timeout message
31
+ read_with_timeout
32
+ end
33
+
25
34
  def method_missing(method_name, *args)
26
35
  if Connection.connection_modules.include?(method_name.to_sym)
27
36
  send_and_handle_message(method_name, *args)
@@ -41,16 +50,31 @@ module Sip2
41
50
  message = with_checksum with_error_detection message
42
51
  response = send_message message
43
52
  return if response.nil?
53
+
44
54
  send "handle_#{message_type}_response", response
45
55
  end
46
56
 
47
- def send_message(message)
48
- @socket.send(message + "\r", 0)
49
- @socket.gets "\r"
57
+ def write_with_timeout(message, separator: LINE_SEPARATOR)
58
+ ::Timeout.timeout connection_timeout, WriteTimeout do
59
+ @socket.write message + separator
60
+ end
61
+ end
62
+
63
+ def read_with_timeout(separator: LINE_SEPARATOR)
64
+ ::Timeout.timeout connection_timeout, ReadTimeout do
65
+ @socket.gets(separator)&.chomp(separator)
66
+ end
67
+ end
68
+
69
+ def connection_timeout
70
+ # We want the underlying connection where the timeout is configured,
71
+ # so if we're dealing with an SSLSocket then we need to unwrap it
72
+ io = @socket.respond_to?(:io) ? @socket.io : @socket
73
+ io.connection_timeout || NonBlockingSocket::DEFAULT_TIMEOUT
50
74
  end
51
75
 
52
76
  def with_error_detection(message)
53
- message + '|AY' + @sequence.to_s
77
+ "#{message}|AY#{@sequence}"
54
78
  end
55
79
 
56
80
  def with_checksum(message)
@@ -63,12 +87,13 @@ module Sip2
63
87
  message.each_char { |m| check += m.ord }
64
88
  check += "\0".ord
65
89
  check = (check ^ 0xFFFF) + 1
66
- format '%4.4X', check
90
+ format '%<check>4.4X', check: check
67
91
  end
68
92
 
69
93
  def sequence_and_checksum_valid?(response)
70
94
  return true if @ignore_error_detection
71
- sequence_regex = /^(?<message>.*?AY(?<sequence>[0-9]+)AZ)(?<checksum>[A-F0-9]{4})$/
95
+
96
+ sequence_regex = /\A(?<message>.*?AY(?<sequence>[0-9]+)AZ)(?<checksum>[A-F0-9]{4})\z/
72
97
  match = response.strip.match sequence_regex
73
98
  match &&
74
99
  match[:sequence] == @sequence.to_s &&
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
4
  module Messages
3
5
  #
@@ -10,11 +12,11 @@ module Sip2
10
12
 
11
13
  private
12
14
 
13
- def build_login_message(username, password, location_code = nil)
15
+ def build_login_message(username:, password:, location_code: nil)
14
16
  code = '93' # Login
15
17
  uid_algorithm = pw_algorithm = '0' # Plain text
16
- username_field = 'CN' + username
17
- password_field = 'CO' + password
18
+ username_field = "CN#{username}"
19
+ password_field = "CO#{password}"
18
20
  location_code = location_code.strip if location_code.is_a? String
19
21
  location_field = location_code ? "|CP#{location_code}" : ''
20
22
 
@@ -24,7 +26,7 @@ module Sip2
24
26
  end
25
27
 
26
28
  def handle_login_response(response)
27
- sequence_and_checksum_valid?(response) && response[/^94([01])AY/, 1] == '1'
29
+ sequence_and_checksum_valid?(response) && response[/\A94([01])AY/, 1] == '1'
28
30
  end
29
31
  end
30
32
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
4
  module Messages
3
5
  #
@@ -10,7 +12,7 @@ module Sip2
10
12
 
11
13
  private
12
14
 
13
- def build_patron_information_message(uid, password, terminal_password = nil)
15
+ def build_patron_information_message(uid:, password:, terminal_password: nil)
14
16
  code = '63' # Patron information
15
17
  language = '000' # Unknown
16
18
  timestamp = Time.now.strftime('%Y%m%d %H%M%S')
@@ -23,6 +25,7 @@ module Sip2
23
25
 
24
26
  def handle_patron_information_response(response)
25
27
  return unless sequence_and_checksum_valid?(response)
28
+
26
29
  Sip2::PatronInformation.new response
27
30
  end
28
31
  end
@@ -1,20 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'socket'
4
+ require 'timeout'
2
5
 
3
6
  module Sip2
4
7
  #
5
8
  # Sip2 Non-blocking socket
6
9
  # From https://spin.atomicobject.com/2013/09/30/socket-connection-timeout-ruby/
7
10
  #
8
- class NonBlockingSocket
11
+ class NonBlockingSocket < Socket
12
+ DEFAULT_TIMEOUT = 5
13
+
14
+ attr_accessor :connection_timeout
15
+
9
16
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
- def self.connect(host, port, timeout = 5)
17
+ def self.connect(host:, port:, timeout: DEFAULT_TIMEOUT)
11
18
  # Convert the passed host into structures the non-blocking calls can deal with
12
19
  addr = Socket.getaddrinfo(host, nil)
13
20
  sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
14
21
 
15
- Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0).tap do |socket|
22
+ NonBlockingSocket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0).tap do |socket|
16
23
  socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
17
24
 
25
+ socket.connection_timeout = timeout
26
+
18
27
  begin
19
28
  # Initiate the socket connection in the background. If it doesn't fail
20
29
  # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
@@ -27,7 +36,7 @@ module Sip2
27
36
  begin
28
37
  # Verify there is now a good connection
29
38
  socket.connect_nonblock(sockaddr)
30
- rescue Errno::EISCONN # rubocop:disable Lint/HandleExceptions
39
+ rescue Errno::EISCONN
31
40
  # Good news everybody, the socket is connected!
32
41
  rescue StandardError
33
42
  # An unexpected exception was raised - the connection is no good.
@@ -38,7 +47,7 @@ module Sip2
38
47
  # IO.select returns nil when the socket is not ready before timeout
39
48
  # seconds have elapsed
40
49
  socket.close
41
- raise 'Connection timeout'
50
+ raise ConnectionTimeout
42
51
  end
43
52
  end
44
53
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
4
  #
3
5
  # Sip2 Patron Information
@@ -72,6 +74,7 @@ module Sip2
72
74
  def transaction_date
73
75
  match = raw_response.match(/\A64.{17}(\d{4})(\d{2})(\d{2})(.{4})(\d{2})(\d{2})(\d{2})/)
74
76
  return unless match
77
+
75
78
  _, year, month, day, zone, hour, minute, second = match.to_a
76
79
  Time.new(
77
80
  year.to_i, month.to_i, day.to_i,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
- VERSION = '0.0.9'.freeze
4
+ VERSION = '0.2.1'
3
5
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sip2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - abrom
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-11 00:00:00.000000000 Z
11
+ date: 2020-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.11'
19
+ version: '2'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.11'
26
+ version: '2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '12.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '12.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -56,30 +56,30 @@ dependencies:
56
56
  name: rubocop
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">="
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '0.60'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">="
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '0.60'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: timecop
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '0.9'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '0.9'
83
83
  description: 3M™ Standard Interchange Protocol v2 client implementation in Ruby
84
84
  email:
85
85
  - a.bromwich@gmail.com
@@ -87,13 +87,7 @@ executables: []
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
- - ".gitignore"
91
- - ".rubocop.yml"
92
- - ".travis.yml"
93
- - Gemfile
94
90
  - LICENSE
95
- - README.md
96
- - Rakefile
97
91
  - lib/sip2.rb
98
92
  - lib/sip2/client.rb
99
93
  - lib/sip2/connection.rb
@@ -102,7 +96,6 @@ files:
102
96
  - lib/sip2/non_blocking_socket.rb
103
97
  - lib/sip2/patron_information.rb
104
98
  - lib/sip2/version.rb
105
- - sip2.gemspec
106
99
  homepage: https://github.com/Studiosity/sip2-ruby
107
100
  licenses:
108
101
  - MIT
@@ -115,16 +108,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
115
108
  requirements:
116
109
  - - ">="
117
110
  - !ruby/object:Gem::Version
118
- version: 2.1.0
111
+ version: 2.4.0
119
112
  required_rubygems_version: !ruby/object:Gem::Requirement
120
113
  requirements:
121
114
  - - ">="
122
115
  - !ruby/object:Gem::Version
123
116
  version: '0'
124
117
  requirements: []
125
- rubyforge_project:
126
- rubygems_version: 2.6.14.1
118
+ rubygems_version: 3.0.6
127
119
  signing_key:
128
120
  specification_version: 4
129
- summary: 3M™ Standard Interchange Protocol v2 client implementation in Ruby
121
+ summary: SIP2 Ruby client
130
122
  test_files: []
data/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- *.gem
2
- Gemfile.lock
@@ -1,9 +0,0 @@
1
- Metrics/BlockLength:
2
- Exclude:
3
- - "**/*_spec.rb"
4
-
5
- Metrics/LineLength:
6
- Max: 100
7
-
8
- Metrics/ClassLength:
9
- Enabled: false
@@ -1,17 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.1.0
4
- - 2.2.5
5
- - 2.3.0
6
- - 2.4.0
7
-
8
- before_install:
9
- - gem update bundler
10
-
11
- install:
12
- - bundle install --jobs=3 --retry=3
13
- - gem install rubocop
14
-
15
- script:
16
- - rubocop
17
- - bundle exec rake
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in sip2.gemspec
4
- gemspec
data/README.md DELETED
@@ -1,65 +0,0 @@
1
- [![Travis Build Status](http://img.shields.io/travis/Studiosity/sip2-ruby.svg?style=flat)](https://travis-ci.org/Studiosity/sip2-ruby)
2
- [![Gem Version](http://img.shields.io/gem/v/sip2.svg?style=flat)](#)
3
-
4
- # 3M™ Standard Interchange Protocol v2 (SIP2) client implementation in Ruby
5
-
6
- This is a gem wrapping the SIP v2 protocol.
7
-
8
- http://multimedia.3m.com/mws/media/355361O/sip2-protocol.pdf
9
-
10
-
11
- ## Installation
12
-
13
- Add this line to your application's Gemfile:
14
-
15
- ```ruby
16
- gem 'sip2'
17
- ```
18
-
19
- And then execute:
20
-
21
- ```bash
22
- $ bundle
23
- ```
24
-
25
-
26
- ## Protocol support
27
-
28
- So far only login (code 93) and patron_information (code 63) are supported
29
-
30
-
31
- ## Usage
32
-
33
- ```ruby
34
- client = Sip2::Client.new(host: 'my.sip2.host.net', port: 6001)
35
- patron =
36
- client.connect do |connection|
37
- if connection.login 'sip_username', 'sip_password'
38
- connection.patron_information 'patron_username', 'patron_password'
39
- end
40
- end
41
-
42
- puts 'Valid patron' if patron && patron.authenticated?
43
- ```
44
-
45
-
46
- ## Contributing
47
-
48
- Bug reports and pull requests are welcome on GitHub at https://github.com/Studiosity/sip2-ruby.
49
-
50
- Note that spec tests are appreciated to minimise regressions. Before submitting a PR, please ensure that:
51
-
52
- ```bash
53
- $ rspec
54
- ```
55
- and
56
-
57
- ```bash
58
- $ rubocop
59
- ```
60
- both succeed
61
-
62
-
63
- ## License
64
-
65
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env rake
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
@@ -1,27 +0,0 @@
1
- lib = File.expand_path('lib', __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
-
4
- require 'sip2/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = 'sip2'
8
- spec.version = Sip2::VERSION
9
- spec.authors = ['abrom']
10
- spec.email = ['a.bromwich@gmail.com']
11
-
12
- spec.summary = '3M™ Standard Interchange Protocol v2 client implementation in Ruby'
13
- spec.description = '3M™ Standard Interchange Protocol v2 client implementation in Ruby'
14
- spec.homepage = 'https://github.com/Studiosity/sip2-ruby'
15
- spec.license = 'MIT'
16
-
17
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
18
- spec.require_paths = ['lib']
19
-
20
- spec.required_ruby_version = '>= 2.1.0'
21
-
22
- spec.add_development_dependency 'bundler', '>= 1.11'
23
- spec.add_development_dependency 'rake', '>= 10.0'
24
- spec.add_development_dependency 'rspec', '~> 3.0'
25
- spec.add_development_dependency 'rubocop'
26
- spec.add_development_dependency 'timecop', '~> 0'
27
- end