sparoid 1.2.0 → 2.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -1
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +11 -0
- data/exe/sparoid +2 -6
- data/lib/sparoid/cli.rb +48 -39
- data/lib/sparoid/version.rb +1 -1
- data/lib/sparoid.rb +114 -42
- data/sparoid.gemspec +1 -2
- metadata +5 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df9de779b1b4fcfd29f3f0ad3ad986001ecc39b61fbe3aeff36c9b3cb5409327
|
|
4
|
+
data.tar.gz: 56789005516d3c0c2e3b4799f1cf3d6c9873465ff3516b27edee0649b2f5d373
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce6cca5c5219a9f7f99fb6fa34b16efee1d60acae4e5f6bf639524e8ebfd5d4cc805953e9df48656aae11211b25aa824b28751478691a8a293ad3704bd464416
|
|
7
|
+
data.tar.gz: bc288e38f49a5c54f10f338d943169dd662932f516fbdccc6c7d5dc75857da7e0c73dbf2dcb3e5082ee7e769f70552deb12f23a11f8f5cca7f993cc4d6104ad0
|
data/.github/workflows/main.yml
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## [2.1.0] - 2026-03-10
|
|
2
|
+
|
|
3
|
+
- Aligning CLI with Crystal client interface (`send`/`connect` subcommands with flags)
|
|
4
|
+
- Detect public IPv6 via UDP socket probe instead of interface enumeration
|
|
5
|
+
- Remove v2 message format, only send v1 (with IPv4 or IPv6 address)
|
|
6
|
+
|
|
7
|
+
## [2.0.0] - 2026-02-19
|
|
8
|
+
|
|
9
|
+
- Add IPv6 support
|
|
10
|
+
- Add Timeout.timeout safety net around Socket.tcp for Ruby 3.3+ Happy Eyeballs v2 compatibility
|
|
11
|
+
|
|
1
12
|
## [1.2.0] - 2024-07-24
|
|
2
13
|
|
|
3
14
|
- Lookup public IP using http://checkip.amazonaws.com, as it's more reliable for IPv6 connections than the DNS method
|
data/exe/sparoid
CHANGED
data/lib/sparoid/cli.rb
CHANGED
|
@@ -1,55 +1,66 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "optparse"
|
|
4
4
|
require_relative "../sparoid"
|
|
5
5
|
|
|
6
6
|
module Sparoid
|
|
7
7
|
# CLI
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
module CLI
|
|
9
|
+
def self.run(args = ARGV) # rubocop:disable Metrics/AbcSize
|
|
10
|
+
subcommand = args.shift
|
|
11
|
+
host = "0.0.0.0"
|
|
12
|
+
port = 8484
|
|
13
|
+
tcp_port = 22
|
|
14
|
+
config_path = "~/.sparoid.ini"
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
case subcommand
|
|
17
|
+
when "keygen"
|
|
18
|
+
Sparoid.keygen
|
|
19
|
+
when "send"
|
|
20
|
+
parse_send_options!(args, binding)
|
|
21
|
+
key, hmac_key = read_keys(config_path)
|
|
22
|
+
Sparoid.auth(key, hmac_key, host, port)
|
|
23
|
+
when "connect"
|
|
24
|
+
parse_connect_options!(args, binding)
|
|
25
|
+
key, hmac_key = read_keys(config_path)
|
|
26
|
+
ips = Sparoid.auth(key, hmac_key, host, port)
|
|
27
|
+
Sparoid.fdpass(ips, tcp_port)
|
|
28
|
+
when "--version"
|
|
29
|
+
puts Sparoid::VERSION
|
|
30
|
+
else
|
|
31
|
+
puts "Usage: sparoid [subcommand] [options]"
|
|
32
|
+
puts ""
|
|
33
|
+
puts "Subcommands: keygen, send, connect"
|
|
34
|
+
puts "Use --version to show version"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
26
37
|
rescue StandardError => e
|
|
27
|
-
|
|
38
|
+
warn "Sparoid error: #{e.message}"
|
|
39
|
+
exit 1
|
|
28
40
|
end
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
def self.parse_send_options!(args, ctx, banner: "send")
|
|
43
|
+
OptionParser.new do |p|
|
|
44
|
+
p.banner = "Usage: sparoid #{banner} [options]"
|
|
45
|
+
p.on("-h HOST", "--host=HOST", "Host to send to") { |v| ctx.local_variable_set(:host, v) }
|
|
46
|
+
p.on("-p PORT", "--port=PORT", "UDP port (default: 8484)") { |v| ctx.local_variable_set(:port, v.to_i) }
|
|
47
|
+
p.on("-c PATH", "--config=PATH", "Path to config file") { |v| ctx.local_variable_set(:config_path, v) }
|
|
48
|
+
yield p if block_given?
|
|
49
|
+
end.parse!(args)
|
|
33
50
|
end
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
52
|
+
def self.parse_connect_options!(args, ctx)
|
|
53
|
+
parse_send_options!(args, ctx, banner: "connect") do |p|
|
|
54
|
+
p.on("-P PORT", "--tcp-port=PORT", "TCP port (default: 22)") { |v| ctx.local_variable_set(:tcp_port, v.to_i) }
|
|
55
|
+
end
|
|
38
56
|
end
|
|
39
57
|
|
|
40
|
-
def self.
|
|
41
|
-
|
|
58
|
+
def self.read_keys(config_path)
|
|
59
|
+
parse_ini(config_path).values_at("key", "hmac-key")
|
|
42
60
|
end
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def send_auth(host, port, config)
|
|
47
|
-
key, hmac_key = get_keys(parse_ini(config))
|
|
48
|
-
Sparoid.auth(key, hmac_key, host, port.to_i)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def parse_ini(path)
|
|
52
|
-
File.readlines(File.expand_path(path)).map! { |line| line.split("=", 2).map!(&:strip) }.to_h
|
|
62
|
+
def self.parse_ini(path)
|
|
63
|
+
File.readlines(File.expand_path(path)).to_h { |line| line.split("=", 2).map(&:strip) }
|
|
53
64
|
rescue Errno::ENOENT
|
|
54
65
|
{
|
|
55
66
|
"key" => ENV.fetch("SPAROID_KEY", nil),
|
|
@@ -57,8 +68,6 @@ module Sparoid
|
|
|
57
68
|
}
|
|
58
69
|
end
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
config.values_at("key", "hmac-key")
|
|
62
|
-
end
|
|
71
|
+
private_class_method :parse_send_options!, :parse_connect_options!, :read_keys, :parse_ini
|
|
63
72
|
end
|
|
64
73
|
end
|
data/lib/sparoid/version.rb
CHANGED
data/lib/sparoid.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "sparoid/version"
|
|
|
4
4
|
require "socket"
|
|
5
5
|
require "openssl"
|
|
6
6
|
require "resolv"
|
|
7
|
+
require "timeout"
|
|
7
8
|
|
|
8
9
|
# Single Packet Authorisation client
|
|
9
10
|
module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
@@ -11,20 +12,30 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
11
12
|
|
|
12
13
|
SPAROID_CACHE_PATH = ENV.fetch("SPAROID_CACHE_PATH", "/tmp/.sparoid_public_ip")
|
|
13
14
|
|
|
15
|
+
URLS = [
|
|
16
|
+
"ipv6.icanhazip.com",
|
|
17
|
+
"ipv4.icanhazip.com"
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
GOOGLE_DNS_V6 = ["2001:4860:4860::8888", 53].freeze
|
|
21
|
+
|
|
14
22
|
# Send an authorization packet
|
|
15
|
-
def auth(key, hmac_key, host, port, open_for_ip:
|
|
23
|
+
def auth(key, hmac_key, host, port, open_for_ip: nil)
|
|
16
24
|
addrs = resolve_ip_addresses(host, port)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
addrs.each do |addr|
|
|
26
|
+
messages = generate_messages(open_for_ip)
|
|
27
|
+
data = messages.map do |message|
|
|
28
|
+
prefix_hmac(hmac_key, encrypt(key, message))
|
|
29
|
+
end
|
|
30
|
+
sendmsg(addr, data)
|
|
31
|
+
end
|
|
21
32
|
|
|
22
33
|
# wait some time for the server to actually open the port
|
|
23
34
|
# if we don't wait the next SYN package will be dropped
|
|
24
35
|
# and it have to be redelivered, adding 1 second delay
|
|
25
36
|
sleep 0.02
|
|
26
37
|
|
|
27
|
-
addrs
|
|
38
|
+
addrs
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
# Generate new aes and hmac keys, print to stdout
|
|
@@ -37,11 +48,11 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
# Connect to a TCP server and pass the FD to the parent
|
|
40
|
-
def fdpass(
|
|
51
|
+
def fdpass(addrs, port, connect_timeout: 10) # rubocop:disable Metrics/AbcSize
|
|
41
52
|
# try connect to all IPs
|
|
42
|
-
sockets =
|
|
43
|
-
Socket.new(
|
|
44
|
-
s.connect_nonblock(Socket.sockaddr_in(port,
|
|
53
|
+
sockets = addrs.map do |addr|
|
|
54
|
+
Socket.new(addr.afamily, Socket::SOCK_STREAM).tap do |s|
|
|
55
|
+
s.connect_nonblock(Socket.sockaddr_in(port, addr.ip_address), exception: false)
|
|
45
56
|
end
|
|
46
57
|
end
|
|
47
58
|
# wait for any socket to be connected
|
|
@@ -51,9 +62,9 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
51
62
|
writeable.each do |s|
|
|
52
63
|
idx = sockets.index(s)
|
|
53
64
|
sockets.delete_at(idx) # don't retry this socket again
|
|
54
|
-
|
|
65
|
+
addr = addrs.delete_at(idx) # find the IP for the socket
|
|
55
66
|
begin
|
|
56
|
-
s.connect_nonblock(Socket.sockaddr_in(port,
|
|
67
|
+
s.connect_nonblock(Socket.sockaddr_in(port, addr.ip_address)) # check for errors
|
|
57
68
|
rescue Errno::EISCONN
|
|
58
69
|
# already connected, continue
|
|
59
70
|
rescue SystemCallError
|
|
@@ -69,11 +80,25 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
69
80
|
|
|
70
81
|
private
|
|
71
82
|
|
|
72
|
-
def
|
|
73
|
-
|
|
83
|
+
def generate_messages(ip)
|
|
84
|
+
if ip
|
|
85
|
+
[message(string_to_ip(ip))]
|
|
86
|
+
else
|
|
87
|
+
ips = cached_public_ips
|
|
88
|
+
native_ipv6 = public_ipv6_by_udp
|
|
89
|
+
if native_ipv6
|
|
90
|
+
ips = ips.grep_v(Resolv::IPv6)
|
|
91
|
+
ips << Resolv::IPv6.create(native_ipv6)
|
|
92
|
+
end
|
|
93
|
+
ips.map { |i| message(i) }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def sendmsg(addr, data)
|
|
98
|
+
socket = UDPSocket.new(addr.afamily)
|
|
74
99
|
socket.nonblock = false
|
|
75
|
-
|
|
76
|
-
socket.sendmsg
|
|
100
|
+
data.each do |packet|
|
|
101
|
+
socket.sendmsg packet, 0, addr
|
|
77
102
|
rescue StandardError => e
|
|
78
103
|
warn "Sparoid error: #{e.message}"
|
|
79
104
|
end
|
|
@@ -103,14 +128,16 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
103
128
|
hmac + data
|
|
104
129
|
end
|
|
105
130
|
|
|
131
|
+
# Message format: version(4) + timestamp(8) + nonce(16) + ip(4 or 16)
|
|
132
|
+
# https://github.com/84codes/sparoid/blob/main/src/message.cr
|
|
106
133
|
def message(ip)
|
|
107
134
|
version = 1
|
|
108
135
|
ts = (Time.now.utc.to_f * 1000).floor
|
|
109
136
|
nounce = OpenSSL::Random.random_bytes(16)
|
|
110
|
-
[version, ts, nounce, ip.address].pack("N q> a16
|
|
137
|
+
[version, ts, nounce, ip.address].pack("N q> a16 a*")
|
|
111
138
|
end
|
|
112
139
|
|
|
113
|
-
def
|
|
140
|
+
def cached_public_ips
|
|
114
141
|
if up_to_date_cache?
|
|
115
142
|
read_cache
|
|
116
143
|
else
|
|
@@ -118,7 +145,7 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
118
145
|
end
|
|
119
146
|
rescue StandardError => e
|
|
120
147
|
warn "Sparoid: #{e.inspect}"
|
|
121
|
-
|
|
148
|
+
public_ips
|
|
122
149
|
end
|
|
123
150
|
|
|
124
151
|
def up_to_date_cache?
|
|
@@ -131,7 +158,9 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
131
158
|
def read_cache
|
|
132
159
|
File.open(SPAROID_CACHE_PATH, "r") do |f|
|
|
133
160
|
f.flock(File::LOCK_SH)
|
|
134
|
-
|
|
161
|
+
f.readlines(chomp: true).map do |line|
|
|
162
|
+
string_to_ip(line)
|
|
163
|
+
end
|
|
135
164
|
end
|
|
136
165
|
rescue ArgumentError => e
|
|
137
166
|
return write_cache if /cannot interpret as IPv4 address/.match?(e.message)
|
|
@@ -142,54 +171,97 @@ module Sparoid # rubocop:disable Metrics/ModuleLength
|
|
|
142
171
|
def write_cache
|
|
143
172
|
File.open(SPAROID_CACHE_PATH, File::WRONLY | File::CREAT, 0o0644) do |f|
|
|
144
173
|
f.flock(File::LOCK_EX)
|
|
145
|
-
|
|
174
|
+
ips = public_ips
|
|
175
|
+
warn "Sparoid: Failed to retrieve public IPs" if ips.empty?
|
|
146
176
|
f.truncate(0)
|
|
147
|
-
f.
|
|
148
|
-
ip
|
|
177
|
+
f.rewind
|
|
178
|
+
ips.each do |ip|
|
|
179
|
+
f.puts ip.to_s
|
|
180
|
+
end
|
|
181
|
+
ips
|
|
149
182
|
end
|
|
150
183
|
end
|
|
151
184
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
185
|
+
def public_ips(port = 80) # rubocop:disable Metrics/AbcSize
|
|
186
|
+
URLS.map do |host|
|
|
187
|
+
Timeout.timeout(5) do
|
|
188
|
+
Socket.tcp(host, port, connect_timeout: 3, resolv_timeout: 3) do |sock|
|
|
189
|
+
sock.sync = true
|
|
190
|
+
sock.print "GET / HTTP/1.1\r\nHost: #{host}\r\nConnection: close\r\n\r\n"
|
|
191
|
+
status = sock.readline(chomp: true)
|
|
192
|
+
raise(ResolvError, "#{host}:#{port} response: #{status}") unless status.start_with? "HTTP/1.1 200 "
|
|
158
193
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
194
|
+
content_length = 0
|
|
195
|
+
until (header = sock.readline(chomp: true)).empty?
|
|
196
|
+
if (m = header.match(/^Content-Length: (\d+)/))
|
|
197
|
+
content_length = m[1].to_i
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
ip = sock.read(content_length).chomp
|
|
201
|
+
string_to_ip(ip)
|
|
163
202
|
end
|
|
164
203
|
end
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
rescue StandardError
|
|
205
|
+
nil
|
|
206
|
+
end.compact
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def string_to_ip(ip)
|
|
210
|
+
case ip
|
|
211
|
+
when Resolv::IPv4::Regex
|
|
212
|
+
Resolv::IPv4.create(ip)
|
|
213
|
+
when Resolv::IPv6::Regex
|
|
214
|
+
Resolv::IPv6.create(ip)
|
|
215
|
+
else
|
|
216
|
+
raise ArgumentError, "Unsupported IP format #{ip}"
|
|
167
217
|
end
|
|
168
218
|
end
|
|
169
219
|
|
|
170
220
|
def resolve_ip_addresses(host, port)
|
|
171
|
-
addresses = Addrinfo.getaddrinfo(host, port
|
|
221
|
+
addresses = Addrinfo.getaddrinfo(host, port)
|
|
172
222
|
raise(ResolvError, "Sparoid failed to resolv #{host}") if addresses.empty?
|
|
173
223
|
|
|
174
|
-
addresses
|
|
224
|
+
addresses.select { |addr| addr.socktype == Socket::SOCK_DGRAM }
|
|
175
225
|
rescue SocketError
|
|
176
226
|
raise(ResolvError, "Sparoid failed to resolv #{host}")
|
|
177
227
|
end
|
|
178
228
|
|
|
229
|
+
# Get the public IPv6 address by asking the OS which source address
|
|
230
|
+
# it would use to reach a well-known IPv6 destination.
|
|
231
|
+
# Returns nil if no global IPv6 address is available.
|
|
232
|
+
def public_ipv6_by_udp
|
|
233
|
+
socket = UDPSocket.new(Socket::AF_INET6)
|
|
234
|
+
socket.connect(*GOOGLE_DNS_V6)
|
|
235
|
+
addr = socket.local_address
|
|
236
|
+
return addr.ip_address if global_ipv6?(addr)
|
|
237
|
+
|
|
238
|
+
nil
|
|
239
|
+
rescue StandardError
|
|
240
|
+
nil
|
|
241
|
+
ensure
|
|
242
|
+
socket&.close
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def global_ipv6?(addr)
|
|
246
|
+
!(addr.ipv6_loopback? || addr.ipv6_linklocal? || addr.ipv6_unspecified? ||
|
|
247
|
+
addr.ipv6_sitelocal? || addr.ipv6_multicast? || addr.ipv6_v4mapped? ||
|
|
248
|
+
addr.ip_address.start_with?("fd", "fc"))
|
|
249
|
+
end
|
|
250
|
+
|
|
179
251
|
class Error < StandardError; end
|
|
180
252
|
|
|
181
253
|
class ResolvError < Error; end
|
|
182
254
|
|
|
183
|
-
# Instance of SPAroid that only resolved
|
|
255
|
+
# Instance of SPAroid that only resolved public_ips once
|
|
184
256
|
class Instance
|
|
185
257
|
include Sparoid
|
|
186
258
|
|
|
187
|
-
def
|
|
188
|
-
@
|
|
259
|
+
def public_ips(*args)
|
|
260
|
+
@public_ips ||= super
|
|
189
261
|
end
|
|
190
262
|
|
|
191
|
-
def
|
|
192
|
-
|
|
263
|
+
def cached_public_ips
|
|
264
|
+
public_ips
|
|
193
265
|
end
|
|
194
266
|
end
|
|
195
267
|
end
|
data/sparoid.gemspec
CHANGED
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
|
11
11
|
spec.summary = "Single Packet Authorisation client"
|
|
12
12
|
spec.homepage = "https://github.com/84codes/sparoid.rb"
|
|
13
13
|
spec.license = "MIT"
|
|
14
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
15
15
|
|
|
16
16
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
17
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
@@ -26,6 +26,5 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
27
27
|
spec.require_paths = ["lib"]
|
|
28
28
|
|
|
29
|
-
spec.add_dependency "thor"
|
|
30
29
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
31
30
|
end
|
metadata
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sparoid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl Hörberg
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: thor
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ">="
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ">="
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
27
|
-
description:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
28
12
|
email:
|
|
29
13
|
- carl@84codes.com
|
|
30
14
|
executables:
|
|
@@ -56,7 +40,6 @@ metadata:
|
|
|
56
40
|
source_code_uri: https://github.com/84codes/sparoid.rb
|
|
57
41
|
changelog_uri: https://raw.githubusercontent.com/84codes/sparoid.rb/main/CHANGELOG.md
|
|
58
42
|
rubygems_mfa_required: 'true'
|
|
59
|
-
post_install_message:
|
|
60
43
|
rdoc_options: []
|
|
61
44
|
require_paths:
|
|
62
45
|
- lib
|
|
@@ -64,15 +47,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
64
47
|
requirements:
|
|
65
48
|
- - ">="
|
|
66
49
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 2.
|
|
50
|
+
version: 3.2.0
|
|
68
51
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
52
|
requirements:
|
|
70
53
|
- - ">="
|
|
71
54
|
- !ruby/object:Gem::Version
|
|
72
55
|
version: '0'
|
|
73
56
|
requirements: []
|
|
74
|
-
rubygems_version: 3.
|
|
75
|
-
signing_key:
|
|
57
|
+
rubygems_version: 3.6.9
|
|
76
58
|
specification_version: 4
|
|
77
59
|
summary: Single Packet Authorisation client
|
|
78
60
|
test_files: []
|