zeroconf 1.2.0 → 1.3.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/ci.yml +2 -2
- data/.github/workflows/release.yml +52 -0
- data/Gemfile +2 -0
- data/Rakefile +3 -0
- data/lib/zeroconf/service.rb +87 -3
- data/lib/zeroconf/version.rb +1 -1
- data/test/helper.rb +9 -0
- data/test/service_test.rb +126 -0
- data/zeroconf.gemspec +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b9ec7f1320fa0807e2cdb89addcc9b6796745c9248ac0463a72877d908b74f4
|
|
4
|
+
data.tar.gz: 3be32a8bdabfc923576ce8ed0b8c9dbf430141aae8be1eaf9bc7898b1f425109
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3eedb494ee9507f0e4d541b05f2627fb94d9b63b90d1d1a27f303a901b02fa4c5640719857f35927fd922b511960e753b7e530dc40e30cccf106b0e2cc10168
|
|
7
|
+
data.tar.gz: ef7526948bbe2a34ae9a850c48ba0d12760ad4acb846c33f808338c277d184d9056e8681246e7bbb6154ce1cd31398feb9a4336c4dc6835bc36a63c489642c6a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Publish gem to rubygems.org
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
push:
|
|
13
|
+
if: github.repository == 'tenderlove/zeroconf'
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
environment:
|
|
17
|
+
name: rubygems.org
|
|
18
|
+
url: https://rubygems.org/gems/zeroconf
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: write
|
|
22
|
+
id-token: write
|
|
23
|
+
|
|
24
|
+
strategy:
|
|
25
|
+
matrix:
|
|
26
|
+
ruby: ["ruby"]
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Harden Runner
|
|
30
|
+
uses: step-security/harden-runner@v2
|
|
31
|
+
with:
|
|
32
|
+
egress-policy: audit
|
|
33
|
+
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: Set up Ruby
|
|
37
|
+
uses: ruby/setup-ruby@v1
|
|
38
|
+
with:
|
|
39
|
+
ruby-version: ${{ matrix.ruby }}
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: bundle install --jobs 4 --retry 3
|
|
43
|
+
|
|
44
|
+
- name: Publish to RubyGems
|
|
45
|
+
uses: rubygems/release-gem@v1
|
|
46
|
+
|
|
47
|
+
- name: Create GitHub release
|
|
48
|
+
run: |
|
|
49
|
+
tag_name="$(git describe --tags --abbrev=0)"
|
|
50
|
+
gh release create "${tag_name}" --verify-tag --generate-notes
|
|
51
|
+
env:
|
|
52
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/lib/zeroconf/service.rb
CHANGED
|
@@ -7,9 +7,10 @@ module ZeroConf
|
|
|
7
7
|
include Utils
|
|
8
8
|
|
|
9
9
|
attr_reader :service, :service_port, :hostname, :service_interfaces,
|
|
10
|
-
:service_name, :qualified_host, :text, :abort_on_malformed_requests
|
|
10
|
+
:service_name, :qualified_host, :text, :abort_on_malformed_requests,
|
|
11
|
+
:subtypes
|
|
11
12
|
|
|
12
|
-
def initialize service, service_port, hostname = Socket.gethostname, service_interfaces: ZeroConf.service_interfaces, instance_name: nil, text: [""], abort_on_malformed_requests: false, started_callback: nil
|
|
13
|
+
def initialize service, service_port, hostname = Socket.gethostname, service_interfaces: ZeroConf.service_interfaces, instance_name: nil, text: [""], subtypes: [], abort_on_malformed_requests: false, started_callback: nil
|
|
13
14
|
@service = service
|
|
14
15
|
@service_port = service_port
|
|
15
16
|
|
|
@@ -19,13 +20,14 @@ module ZeroConf
|
|
|
19
20
|
|
|
20
21
|
instance_name ||= @hostname
|
|
21
22
|
if instance_name.include?(".")
|
|
22
|
-
raise ArgumentError, "instance_name must not contain dots (is #{instance_name.inspect})"
|
|
23
|
+
raise ArgumentError, "instance_name must not contain dots (is #{instance_name.inspect})"
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
@service_name = "#{instance_name}.#{@service}"
|
|
26
27
|
@qualified_host = "#{@hostname}.local."
|
|
27
28
|
@started_callback = started_callback
|
|
28
29
|
@text = text
|
|
30
|
+
@subtypes = subtypes.map { |st| "#{st}._sub.#{@service}" }
|
|
29
31
|
@started = false
|
|
30
32
|
@rd, @wr = IO.pipe
|
|
31
33
|
end
|
|
@@ -61,6 +63,11 @@ module ZeroConf
|
|
|
61
63
|
60,
|
|
62
64
|
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
|
63
65
|
|
|
66
|
+
subtypes.each do |st|
|
|
67
|
+
msg.add_answer st, 60,
|
|
68
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
|
69
|
+
end
|
|
70
|
+
|
|
64
71
|
msg
|
|
65
72
|
end
|
|
66
73
|
|
|
@@ -93,6 +100,11 @@ module ZeroConf
|
|
|
93
100
|
0,
|
|
94
101
|
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
|
95
102
|
|
|
103
|
+
subtypes.each do |st|
|
|
104
|
+
msg.add_answer st, 0,
|
|
105
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
|
106
|
+
end
|
|
107
|
+
|
|
96
108
|
msg
|
|
97
109
|
end
|
|
98
110
|
|
|
@@ -174,6 +186,12 @@ module ZeroConf
|
|
|
174
186
|
else
|
|
175
187
|
name_answer_multicast
|
|
176
188
|
end
|
|
189
|
+
when *subtypes
|
|
190
|
+
if unicast
|
|
191
|
+
subtype_unicast_answer qn
|
|
192
|
+
else
|
|
193
|
+
subtype_multicast_answer qn
|
|
194
|
+
end
|
|
177
195
|
else
|
|
178
196
|
#p [:QUERY2, type, type::ClassValue, name]
|
|
179
197
|
end
|
|
@@ -313,6 +331,72 @@ module ZeroConf
|
|
|
313
331
|
msg
|
|
314
332
|
end
|
|
315
333
|
|
|
334
|
+
def subtype_multicast_answer subtype
|
|
335
|
+
msg = Resolv::DNS::Message.new(0)
|
|
336
|
+
msg.qr = 1
|
|
337
|
+
msg.aa = 1
|
|
338
|
+
|
|
339
|
+
msg.add_additional service_name, 60, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
|
340
|
+
|
|
341
|
+
service_interfaces.each do |iface|
|
|
342
|
+
if iface.addr.ipv4?
|
|
343
|
+
msg.add_additional qualified_host,
|
|
344
|
+
60,
|
|
345
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
|
346
|
+
else
|
|
347
|
+
msg.add_additional qualified_host,
|
|
348
|
+
60,
|
|
349
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if @text
|
|
354
|
+
msg.add_additional service_name,
|
|
355
|
+
60,
|
|
356
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
msg.add_answer subtype,
|
|
360
|
+
60,
|
|
361
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
|
362
|
+
|
|
363
|
+
msg
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def subtype_unicast_answer subtype
|
|
367
|
+
msg = Resolv::DNS::Message.new(0)
|
|
368
|
+
msg.qr = 1
|
|
369
|
+
msg.aa = 1
|
|
370
|
+
|
|
371
|
+
msg.add_additional service_name, 10, Resolv::DNS::Resource::IN::SRV.new(0, 0, service_port, qualified_host)
|
|
372
|
+
|
|
373
|
+
service_interfaces.each do |iface|
|
|
374
|
+
if iface.addr.ipv4?
|
|
375
|
+
msg.add_additional qualified_host,
|
|
376
|
+
10,
|
|
377
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
|
378
|
+
else
|
|
379
|
+
msg.add_additional qualified_host,
|
|
380
|
+
10,
|
|
381
|
+
Resolv::DNS::Resource::IN::AAAA.new(iface.addr.ip_address)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
if @text
|
|
386
|
+
msg.add_additional service_name,
|
|
387
|
+
10,
|
|
388
|
+
Resolv::DNS::Resource::IN::TXT.new(*@text)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
msg.add_answer subtype,
|
|
392
|
+
10,
|
|
393
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
|
|
394
|
+
|
|
395
|
+
msg.add_question subtype, PTR
|
|
396
|
+
|
|
397
|
+
msg
|
|
398
|
+
end
|
|
399
|
+
|
|
316
400
|
def service_instance_multicast_answer
|
|
317
401
|
msg = Resolv::DNS::Message.new(0)
|
|
318
402
|
msg.qr = 1
|
data/lib/zeroconf/version.rb
CHANGED
data/test/helper.rb
CHANGED
|
@@ -47,6 +47,15 @@ module ZeroConf
|
|
|
47
47
|
**opts
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def make_subtype_server iface, host = HOST_NAME, **opts
|
|
51
|
+
Service.new SERVICE + ".",
|
|
52
|
+
42424,
|
|
53
|
+
host,
|
|
54
|
+
service_interfaces: [iface], text: ["test=1", "other=value"],
|
|
55
|
+
subtypes: ["_universal"],
|
|
56
|
+
**opts
|
|
57
|
+
end
|
|
58
|
+
|
|
50
59
|
def make_listener rd, q, started_callback: nil
|
|
51
60
|
Thread.new do
|
|
52
61
|
sock = open_ipv4 Addrinfo.new(Socket.sockaddr_in(Resolv::MDNS::Port, Socket::INADDR_ANY)), Resolv::MDNS::Port
|
data/test/service_test.rb
CHANGED
|
@@ -440,6 +440,132 @@ module ZeroConf
|
|
|
440
440
|
assert_equal expected, res
|
|
441
441
|
end
|
|
442
442
|
|
|
443
|
+
def test_subtypes_expanded
|
|
444
|
+
s = Service.new "_ipp._tcp.local.", 631, "printer",
|
|
445
|
+
service_interfaces: [iface],
|
|
446
|
+
subtypes: ["_universal", "_print"]
|
|
447
|
+
|
|
448
|
+
assert_equal ["_universal._sub._ipp._tcp.local.", "_print._sub._ipp._tcp.local."], s.subtypes
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def test_subtypes_default_empty
|
|
452
|
+
s = make_server iface
|
|
453
|
+
assert_equal [], s.subtypes
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def test_announcement_includes_subtype_ptrs
|
|
457
|
+
s = Service.new "_test-mdns._tcp.local.", 42424,
|
|
458
|
+
"tc-lan-adapter",
|
|
459
|
+
service_interfaces: [iface],
|
|
460
|
+
subtypes: ["_universal"]
|
|
461
|
+
|
|
462
|
+
ann = s.announcement
|
|
463
|
+
subtype_ptrs = ann.answer.select { |name, ttl, data|
|
|
464
|
+
name.to_s == "_universal._sub._test-mdns._tcp.local"
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
assert_equal 1, subtype_ptrs.length
|
|
468
|
+
assert_equal s.service_name, subtype_ptrs.first.last.name.to_s + "."
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def test_disconnect_includes_subtype_ptrs
|
|
472
|
+
s = Service.new "_test-mdns._tcp.local.", 42424,
|
|
473
|
+
"tc-lan-adapter",
|
|
474
|
+
service_interfaces: [iface],
|
|
475
|
+
subtypes: ["_universal"]
|
|
476
|
+
|
|
477
|
+
msg = s.disconnect_msg
|
|
478
|
+
subtype_ptrs = msg.answer.select { |name, ttl, data|
|
|
479
|
+
name.to_s == "_universal._sub._test-mdns._tcp.local"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
assert_equal 1, subtype_ptrs.length
|
|
483
|
+
assert_equal 0, subtype_ptrs.first[1] # TTL 0 for disconnect
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def test_subtype_unicast_answer
|
|
487
|
+
latch = Queue.new
|
|
488
|
+
s = make_subtype_server iface, started_callback: -> { latch << :start }
|
|
489
|
+
runner = Thread.new { s.start }
|
|
490
|
+
latch.pop
|
|
491
|
+
|
|
492
|
+
subtype = "_universal._sub._test-mdns._tcp.local."
|
|
493
|
+
query = Resolv::DNS::Message.new 0
|
|
494
|
+
query.add_question subtype, PTR
|
|
495
|
+
|
|
496
|
+
sock = open_ipv4 iface.addr, 0
|
|
497
|
+
multicast_send sock, query.encode
|
|
498
|
+
res = Resolv::DNS::Message.decode read_with_timeout(sock).first
|
|
499
|
+
s.stop
|
|
500
|
+
runner.join
|
|
501
|
+
|
|
502
|
+
expected = Resolv::DNS::Message.new(0)
|
|
503
|
+
expected.qr = 1
|
|
504
|
+
expected.aa = 1
|
|
505
|
+
|
|
506
|
+
expected.add_additional s.service_name, 10, Resolv::DNS::Resource::IN::SRV.new(0, 0, s.service_port, s.qualified_host)
|
|
507
|
+
|
|
508
|
+
expected.add_additional s.qualified_host,
|
|
509
|
+
10,
|
|
510
|
+
Resolv::DNS::Resource::IN::A.new(iface.addr.ip_address)
|
|
511
|
+
|
|
512
|
+
expected.add_additional s.service_name,
|
|
513
|
+
10,
|
|
514
|
+
Resolv::DNS::Resource::IN::TXT.new(*s.text)
|
|
515
|
+
|
|
516
|
+
expected.add_answer subtype,
|
|
517
|
+
10,
|
|
518
|
+
Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(s.service_name))
|
|
519
|
+
|
|
520
|
+
expected.add_question subtype, PTR
|
|
521
|
+
|
|
522
|
+
assert_equal expected, res
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def test_subtype_multicast_answer
|
|
526
|
+
q = Thread::Queue.new
|
|
527
|
+
rd, wr = IO.pipe
|
|
528
|
+
|
|
529
|
+
latch = Queue.new
|
|
530
|
+
listen = make_listener rd, q, started_callback: -> { latch << :start }
|
|
531
|
+
s = make_subtype_server iface, started_callback: -> { latch << :start }
|
|
532
|
+
server = Thread.new { s.start }
|
|
533
|
+
latch.pop
|
|
534
|
+
latch.pop
|
|
535
|
+
|
|
536
|
+
subtype = "_universal._sub._test-mdns._tcp.local."
|
|
537
|
+
query = Resolv::DNS::Message.new 0
|
|
538
|
+
query.add_question subtype, Resolv::DNS::Resource::IN::PTR
|
|
539
|
+
sock = open_ipv4 iface.addr, 0
|
|
540
|
+
multicast_send sock, query.encode
|
|
541
|
+
|
|
542
|
+
subtype_name = Resolv::DNS::Name.create subtype
|
|
543
|
+
service_name = Resolv::DNS::Name.create s.service_name
|
|
544
|
+
|
|
545
|
+
while res = q.pop
|
|
546
|
+
if res.answer.find { |name, ttl, data| name == subtype_name && data.name == service_name }
|
|
547
|
+
wr.write "x"
|
|
548
|
+
break
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
listen.join
|
|
553
|
+
s.stop
|
|
554
|
+
server.join
|
|
555
|
+
|
|
556
|
+
# Verify the response contains the subtype PTR
|
|
557
|
+
subtype_ptr = res.answer.find { |name, ttl, data|
|
|
558
|
+
name == subtype_name && data.name == service_name
|
|
559
|
+
}
|
|
560
|
+
assert subtype_ptr, "expected subtype PTR in answer"
|
|
561
|
+
|
|
562
|
+
# Verify it has SRV in additional
|
|
563
|
+
srv = res.additional.find { |_, _, data|
|
|
564
|
+
ZeroConf::MDNS::Announce::IN::SRV == data.class
|
|
565
|
+
}
|
|
566
|
+
assert srv, "expected SRV in additional"
|
|
567
|
+
end
|
|
568
|
+
|
|
443
569
|
def test_raise_on_malformed_requests
|
|
444
570
|
latch = Queue.new
|
|
445
571
|
s = make_server iface, abort_on_malformed_requests: true, started_callback: -> { latch << :start }
|
data/zeroconf.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.homepage = "https://github.com/tenderlove/zeroconf"
|
|
13
13
|
s.license = "Apache-2.0"
|
|
14
14
|
|
|
15
|
-
s.add_dependency("resolv", "
|
|
15
|
+
s.add_dependency("resolv", ">= 0.7.1")
|
|
16
16
|
s.add_development_dependency("rake", "~> 13.0")
|
|
17
17
|
s.add_development_dependency("minitest", "~> 5.20")
|
|
18
18
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zeroconf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aaron Patterson
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: resolv
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.7.1
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.7.1
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: rake
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,6 +58,7 @@ extensions: []
|
|
|
58
58
|
extra_rdoc_files: []
|
|
59
59
|
files:
|
|
60
60
|
- ".github/workflows/ci.yml"
|
|
61
|
+
- ".github/workflows/release.yml"
|
|
61
62
|
- CODE_OF_CONDUCT.md
|
|
62
63
|
- Gemfile
|
|
63
64
|
- LICENSE
|
|
@@ -93,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
93
94
|
- !ruby/object:Gem::Version
|
|
94
95
|
version: '0'
|
|
95
96
|
requirements: []
|
|
96
|
-
rubygems_version:
|
|
97
|
+
rubygems_version: 4.0.6
|
|
97
98
|
specification_version: 4
|
|
98
99
|
summary: Multicast DNS client and server
|
|
99
100
|
test_files:
|