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 +5 -5
- data/lib/sip2.rb +11 -0
- data/lib/sip2/client.rb +22 -4
- data/lib/sip2/connection.rb +32 -7
- data/lib/sip2/messages/login.rb +6 -4
- data/lib/sip2/messages/patron_information.rb +4 -1
- data/lib/sip2/non_blocking_socket.rb +14 -5
- data/lib/sip2/patron_information.rb +3 -0
- data/lib/sip2/version.rb +3 -1
- metadata +19 -27
- data/.gitignore +0 -2
- data/.rubocop.yml +0 -9
- data/.travis.yml +0 -17
- data/Gemfile +0 -4
- data/README.md +0 -65
- data/Rakefile +0 -8
- data/sip2.gemspec +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4d3b9a457d4ca0131eb2039eda501a303c75c69e4891fc1128f75096279134e7
|
4
|
+
data.tar.gz: 6bb443020825760ba59150d79eb23490d220123ab0b86567535f6b89cee5a46a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e0d0ce1e898a903b9713ab305631fa1e12a0d28eee2efad234b00c858a5d4aca2fef8e18449d1a1dd34ec2b0e549be6859eb9ace0d563a5037716ca62508c898
|
7
|
+
data.tar.gz: 800ff16573a4c25e76a8bcaf234dcf79745e3c3ee119d3276b8195ff8a0bc7fe1c6ad7eb0c1c920cce19076cd503e273ac3169b32ec7524e0f04872742a57557
|
data/lib/sip2.rb
CHANGED
@@ -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'
|
data/lib/sip2/client.rb
CHANGED
@@ -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
|
-
|
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
|
33
|
+
socket&.close
|
17
34
|
end
|
35
|
+
# rubocop:enable Metrics/MethodLength
|
18
36
|
end
|
19
37
|
end
|
data/lib/sip2/connection.rb
CHANGED
@@ -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
|
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
|
48
|
-
|
49
|
-
|
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
|
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 '
|
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
|
-
|
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 &&
|
data/lib/sip2/messages/login.rb
CHANGED
@@ -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
|
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 =
|
17
|
-
password_field =
|
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[
|
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
|
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
|
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
|
-
|
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
|
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
|
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,
|
data/lib/sip2/version.rb
CHANGED
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.
|
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:
|
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: '
|
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: '
|
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: '
|
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: '
|
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.
|
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
|
-
|
126
|
-
rubygems_version: 2.6.14.1
|
118
|
+
rubygems_version: 3.0.6
|
127
119
|
signing_key:
|
128
120
|
specification_version: 4
|
129
|
-
summary:
|
121
|
+
summary: SIP2 Ruby client
|
130
122
|
test_files: []
|
data/.gitignore
DELETED
data/.rubocop.yml
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/README.md
DELETED
@@ -1,65 +0,0 @@
|
|
1
|
-
[](https://travis-ci.org/Studiosity/sip2-ruby)
|
2
|
-
[](#)
|
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
data/sip2.gemspec
DELETED
@@ -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
|