zeroconf 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a6933171abe1ab052bf99d2bdb6efa5bd0378f536859e0cf812f2dd68b23461
4
- data.tar.gz: 9c016607692b33ed3a516a872aa80117a119f3ac138a997a19a845684914890c
3
+ metadata.gz: 1b9ec7f1320fa0807e2cdb89addcc9b6796745c9248ac0463a72877d908b74f4
4
+ data.tar.gz: 3be32a8bdabfc923576ce8ed0b8c9dbf430141aae8be1eaf9bc7898b1f425109
5
5
  SHA512:
6
- metadata.gz: bdc315b09c4ada48b56f6f102c24b5c0a51740c6163ccb3b9a96e67c7f262a35e95126c950bb1efd394b4cbea06ec0b29e1ae3e55bd02bbd63b42b4159faa06f
7
- data.tar.gz: 663cc13801926e626a61aea7aebe75d51e92e07ab5133723c53be48259dc19bec30c42cbe10348c3a6881028fac9124afb8c50a697203736f8e394060b479b99
6
+ metadata.gz: b3eedb494ee9507f0e4d541b05f2627fb94d9b63b90d1d1a27f303a901b02fa4c5640719857f35927fd922b511960e753b7e530dc40e30cccf106b0e2cc10168
7
+ data.tar.gz: ef7526948bbe2a34ae9a850c48ba0d12760ad4acb846c33f808338c277d184d9056e8681246e7bbb6154ce1cd31398feb9a4336c4dc6835bc36a63c489642c6a
@@ -4,12 +4,12 @@ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  test:
7
- runs-on: ${{ matrix.os }}-latest
7
+ runs-on: ${{ matrix.os }}
8
8
 
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- os: [ubuntu, macos]
12
+ os: [ubuntu-latest, macos-14]
13
13
  ruby: [ head, 3.2 ]
14
14
 
15
15
  steps:
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  gemspec
data/README.md CHANGED
@@ -108,12 +108,14 @@ to run an HTTP server and simultaneously advertise the server:
108
108
 
109
109
  ```ruby
110
110
  require "zeroconf"
111
- require 'webrick'
111
+ require "webrick"
112
112
 
113
113
  port = 8080
114
114
  host = "test-hostname"
115
115
 
116
- Thread.new { ZeroConf.service "_http._tcp.local.", port, host }
116
+ Ractor.new(port, host) { |port, host|
117
+ ZeroConf.service "_http._tcp.local.", port, host
118
+ }
117
119
 
118
120
  server = WEBrick::HTTPServer.new(:Port => port,
119
121
  :SSLEnable => false,
data/Rakefile CHANGED
@@ -1,6 +1,9 @@
1
+ require "bundler"
1
2
  require "bundler/gem_tasks"
2
3
  require "rake/testtask"
3
4
 
5
+ Bundler::GemHelper.install_tasks
6
+
4
7
  Rake::TestTask.new do |t|
5
8
  t.libs << "test"
6
9
  t.test_files = FileList['test/*_test.rb']
@@ -7,18 +7,27 @@ 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, 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
- @hostname = hostname
16
+
17
+ @hostname = strip_dot_local(hostname) # We re-add .local anyway, later on
16
18
  @service_interfaces = service_interfaces
17
19
  @abort_on_malformed_requests = abort_on_malformed_requests
18
- @service_name = "#{hostname}.#{service}"
19
- @qualified_host = "#{hostname}.local."
20
+
21
+ instance_name ||= @hostname
22
+ if instance_name.include?(".")
23
+ raise ArgumentError, "instance_name must not contain dots (is #{instance_name.inspect})"
24
+ end
25
+
26
+ @service_name = "#{instance_name}.#{@service}"
27
+ @qualified_host = "#{@hostname}.local."
20
28
  @started_callback = started_callback
21
29
  @text = text
30
+ @subtypes = subtypes.map { |st| "#{st}._sub.#{@service}" }
22
31
  @started = false
23
32
  @rd, @wr = IO.pipe
24
33
  end
@@ -54,6 +63,11 @@ module ZeroConf
54
63
  60,
55
64
  Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
56
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
+
57
71
  msg
58
72
  end
59
73
 
@@ -86,6 +100,11 @@ module ZeroConf
86
100
  0,
87
101
  Resolv::DNS::Resource::IN::PTR.new(Resolv::DNS::Name.create(service_name))
88
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
+
89
108
  msg
90
109
  end
91
110
 
@@ -167,6 +186,12 @@ module ZeroConf
167
186
  else
168
187
  name_answer_multicast
169
188
  end
189
+ when *subtypes
190
+ if unicast
191
+ subtype_unicast_answer qn
192
+ else
193
+ subtype_multicast_answer qn
194
+ end
170
195
  else
171
196
  #p [:QUERY2, type, type::ClassValue, name]
172
197
  end
@@ -306,6 +331,72 @@ module ZeroConf
306
331
  msg
307
332
  end
308
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
+
309
400
  def service_instance_multicast_answer
310
401
  msg = Resolv::DNS::Message.new(0)
311
402
  msg.qr = 1
@@ -69,8 +69,8 @@ module ZeroConf
69
69
  sock
70
70
  end
71
71
 
72
- BROADCAST_V4 = Addrinfo.new Socket.sockaddr_in(Resolv::MDNS::Port, Resolv::MDNS::AddressV4)
73
- BROADCAST_V6 = Addrinfo.new Socket.sockaddr_in(Resolv::MDNS::Port, Resolv::MDNS::AddressV6)
72
+ BROADCAST_V4 = Addrinfo.new(Socket.sockaddr_in(Resolv::MDNS::Port, Resolv::MDNS::AddressV4)).freeze
73
+ BROADCAST_V6 = Addrinfo.new(Socket.sockaddr_in(Resolv::MDNS::Port, Resolv::MDNS::AddressV6)).freeze
74
74
 
75
75
  def multicast_send sock, query
76
76
  dest = if sock.local_address.ipv4?
@@ -94,6 +94,10 @@ module ZeroConf
94
94
  sock.send(data, 0, Addrinfo.new(to))
95
95
  end
96
96
 
97
+ def strip_dot_local(from_string)
98
+ from_string.to_s.gsub(/\.local\.?$/, "")
99
+ end
100
+
97
101
  def open_ipv6 saddr, port
98
102
  sock = UDPSocket.new Socket::AF_INET6
99
103
  sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
@@ -1,3 +1,3 @@
1
1
  module ZeroConf
2
- VERSION = "1.1.0"
2
+ VERSION = "1.3.0"
3
3
  end
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
@@ -26,6 +26,37 @@ module ZeroConf
26
26
  assert_equal [""], s.text
27
27
  end
28
28
 
29
+ def test_raises_when_creating_a_service_with_dot_in_the_name
30
+ assert_raises ArgumentError do
31
+ Service.new "_test-mdns._tcp.local.", 42424,
32
+ "some.subdomain.workstation"
33
+ end
34
+
35
+ assert_raises ArgumentError do
36
+ Service.new "_test-mdns._tcp.local.", 42424,
37
+ "ruby-mdns", instance_name: "ruby.test"
38
+ end
39
+ end
40
+
41
+ def test_sets_correct_service_name_from_instance_name
42
+ s = Service.new "_test-mdns._tcp.local.", 42424,
43
+ "my-rb-service", instance_name: "My RB service"
44
+ assert_equal "my-rb-service.local.", s.qualified_host
45
+ assert_equal "My RB service._test-mdns._tcp.local.", s.service_name
46
+ end
47
+
48
+ def test_removes_dot_local_tld_from_passed_hostname
49
+ s = Service.new "_test-mdns._tcp.local.", 42424,
50
+ "ThisMac.local"
51
+ assert_equal "ThisMac.local.", s.qualified_host
52
+ assert_equal "ThisMac._test-mdns._tcp.local.", s.service_name
53
+
54
+ s = Service.new "_test-mdns._tcp.local.", 42424,
55
+ "ThisMac.local."
56
+ assert_equal "ThisMac.local.", s.qualified_host
57
+ assert_equal "ThisMac._test-mdns._tcp.local.", s.service_name
58
+ end
59
+
29
60
  def test_unicast_service_instance_answer
30
61
  latch = Queue.new
31
62
  s = make_server iface, started_callback: -> { latch << :start }
@@ -409,6 +440,132 @@ module ZeroConf
409
440
  assert_equal expected, res
410
441
  end
411
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
+
412
569
  def test_raise_on_malformed_requests
413
570
  latch = Queue.new
414
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", "~> 0.3.0")
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,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeroconf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Patterson
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: resolv
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 0.3.0
18
+ version: 0.7.1
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 0.3.0
25
+ version: 0.7.1
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: rake
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -59,6 +58,7 @@ extensions: []
59
58
  extra_rdoc_files: []
60
59
  files:
61
60
  - ".github/workflows/ci.yml"
61
+ - ".github/workflows/release.yml"
62
62
  - CODE_OF_CONDUCT.md
63
63
  - Gemfile
64
64
  - LICENSE
@@ -80,7 +80,6 @@ homepage: https://github.com/tenderlove/zeroconf
80
80
  licenses:
81
81
  - Apache-2.0
82
82
  metadata: {}
83
- post_install_message:
84
83
  rdoc_options: []
85
84
  require_paths:
86
85
  - lib
@@ -95,8 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
94
  - !ruby/object:Gem::Version
96
95
  version: '0'
97
96
  requirements: []
98
- rubygems_version: 3.5.11
99
- signing_key:
97
+ rubygems_version: 4.0.6
100
98
  specification_version: 4
101
99
  summary: Multicast DNS client and server
102
100
  test_files: