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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f58175fb721fb08fa2a5abb36b0b2165d9b38592bb7ff66bd3a3f4c303ed2f9
4
- data.tar.gz: afd32fb72b31c90e84aaea02db2f81e603b872f8f81c1f6f147d15c13ce3713e
3
+ metadata.gz: 1b9ec7f1320fa0807e2cdb89addcc9b6796745c9248ac0463a72877d908b74f4
4
+ data.tar.gz: 3be32a8bdabfc923576ce8ed0b8c9dbf430141aae8be1eaf9bc7898b1f425109
5
5
  SHA512:
6
- metadata.gz: 2e4faed4b18775a5fec2faf0b4064556356699b049c6ca1d6d2d4c03b685cd8702e3ba8591d098799d3ec6ee54efdb9aee1f0e5ea68578b92c1df7ea8fbbba7b
7
- data.tar.gz: 210e338f7d21bbe98df962e4785e0716e2364e538fb44c1029217465fca0fe6ea2f414986d4bb7ce0f3588efd5bfbbd8caeb140ef5f1028b5e75b58648de57e6
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/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,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
@@ -1,3 +1,3 @@
1
1
  module ZeroConf
2
- VERSION = "1.2.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
@@ -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", "~> 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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeroconf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
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.3.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.3.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: 3.7.0.dev
97
+ rubygems_version: 4.0.6
97
98
  specification_version: 4
98
99
  summary: Multicast DNS client and server
99
100
  test_files: