Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support setting the Bonjour service TXT record. #188

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Sources/NIOTransportServices/NIOTSChannelOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//
#if canImport(Network)
import Foundation
import NIOCore
@preconcurrency import Network

Expand Down Expand Up @@ -45,6 +46,13 @@ public struct NIOTSChannelOptions {

/// See: ``Types/NIOTSMultipathOption``
public static let multipathServiceType = NIOTSChannelOptions.Types.NIOTSMultipathOption()

/// See: ``Types/NIOTSServiceTXTRecordObjectOption``
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public static let serviceTXTRecordObject = NIOTSChannelOptions.Types.NIOTSServiceTXTRecordObjectOption()

/// See: ``Types/NIOTSServiceTXTRecordOption``
public static let serviceTXTRecord = NIOTSChannelOptions.Types.NIOTSServiceTXTRecordOption()
}


Expand Down Expand Up @@ -143,6 +151,24 @@ extension NIOTSChannelOptions {

public init() {}
}

/// ``NIOTSServiceTXTRecordObjectOption`` gets/sets the TXT record object for the `NWListener.Service`. The TXT
/// record is a dictionary of arbitrary metadata that can be included in the advertisement of a Bonjour service.
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public struct NIOTSServiceTXTRecordObjectOption: ChannelOption, Equatable {
public typealias Value = NWTXTRecord?

public init() {}
}

/// ``NIOTSServiceTXTRecordOption`` gets/sets the TXT record data for the `NWListener.Service`. The TXT record
/// is a dictionary of arbitrary metadata that can be included in the advertisement of a Bonjour service.
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
public struct NIOTSServiceTXTRecordOption: ChannelOption, Equatable {
public typealias Value = Data?

public init() {}
}
}
}

Expand Down
57 changes: 55 additions & 2 deletions Sources/NIOTransportServices/StateManagedListenerChannel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,35 @@ internal class StateManagedListenerChannel<ChildChannel: StateManagedChannel>: S
/// The default multipath service type.
internal var multipathServiceType = NWParameters.MultipathServiceType.disabled

/// Storage for `serviceTXTRecordObject`. This property has its type erased because the property is only available
/// on later OS versions, and stored properties can't be marked as unavailable on unsupported OS versions.
private var _serviceTXTRecordObject: Any?

/// The TXT record object to use with the NWListener.Service
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
internal var serviceTXTRecordObject: NWTXTRecord?
{
get {
return self._serviceTXTRecordObject.map({ $0 as! NWTXTRecord })
}
set {
self._serviceTXTRecordObject = newValue
self.nwListener?.service?.txtRecordObject = newValue
}
}

internal var serviceTXTRecord: Data?
{
didSet {
if let listener = self.nwListener, let service = listener.service {
listener.service = NWListener.Service(name: service.name,
type: service.type,
domain: service.domain,
txtRecord: self.serviceTXTRecord)
}
}
}

/// The event loop group to use for child channels.
internal let childLoopGroup: EventLoopGroup

Expand Down Expand Up @@ -243,8 +272,19 @@ extension StateManagedListenerChannel {
self.allowLocalEndpointReuse = value as! NIOTSChannelOptions.Types.NIOTSAllowLocalEndpointReuse.Value
case is NIOTSChannelOptions.Types.NIOTSMultipathOption:
self.multipathServiceType = value as! NIOTSChannelOptions.Types.NIOTSMultipathOption.Value
case is NIOTSChannelOptions.Types.NIOTSServiceTXTRecordOption:
self.serviceTXTRecord = value as! NIOTSChannelOptions.Types.NIOTSServiceTXTRecordOption.Value
default:
fatalError("option \(option) not supported")
if #available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
if option is NIOTSChannelOptions.Types.NIOTSServiceTXTRecordObjectOption {
self.serviceTXTRecordObject = value as! NIOTSChannelOptions.Types.NIOTSServiceTXTRecordObjectOption.Value
}
else {
fatalError("option \(option) not supported")
}
} else {
fatalError("option \(option) not supported")
}
}
}

Expand Down Expand Up @@ -287,7 +327,14 @@ extension StateManagedListenerChannel {
return self.allowLocalEndpointReuse as! Option.Value
case is NIOTSChannelOptions.Types.NIOTSMultipathOption:
return self.multipathServiceType as! Option.Value
case is NIOTSChannelOptions.Types.NIOTSServiceTXTRecordOption:
return self.serviceTXTRecord as! Option.Value
default:
if #available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
if option is NIOTSChannelOptions.Types.NIOTSServiceTXTRecordObjectOption {
return self.serviceTXTRecordObject as! Option.Value
}
}
fatalError("option \(option) not supported")
}
}
Expand Down Expand Up @@ -385,7 +432,13 @@ extension StateManagedListenerChannel {

if case .service(let name, let type, let domain, _) = target {
// Ok, now we deal with Bonjour.
listener.service = NWListener.Service(name: name, type: type, domain: domain)
var service = NWListener.Service(name: name, type: type, domain: domain, txtRecord: self.serviceTXTRecord)
if #available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
if let txtRecordObject = self.serviceTXTRecordObject {
service.txtRecordObject = txtRecordObject
}
}
listener.service = service
}

listener.stateUpdateHandler = self.stateUpdateHandler(newState:)
Expand Down
76 changes: 75 additions & 1 deletion Tests/NIOTransportServicesTests/NIOTSListenerChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class NIOTSListenerChannelTests: XCTestCase {
}.flatMap {
channel.setOption(NIOTSChannelOptions.enablePeerToPeer, value: true)
}.flatMap {
channel.getOption(NIOTSChannelOptions.enablePeerToPeer)
channel.getOption(NIOTSChannelOptions.enablePeerToPeer)
}.map { value in
XCTAssertTrue(value)
}
Expand All @@ -221,6 +221,80 @@ class NIOTSListenerChannelTests: XCTestCase {
}
}

func testCanObserveValueOfServiceTXTRecordObject() throws {
if #available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
let txtRecordObjectToSet = NWTXTRecord(["key": "value"])
let listener = try NIOTSListenerBootstrap(group: self.group)
.serverChannelInitializer { channel in
return channel.getOption(NIOTSChannelOptions.serviceTXTRecordObject).map { value in
XCTAssertNil(value)
}.flatMap {
channel.setOption(NIOTSChannelOptions.serviceTXTRecordObject, value: txtRecordObjectToSet)
}.flatMap {
channel.getOption(NIOTSChannelOptions.serviceTXTRecordObject)
}.map { value in
XCTAssertEqual(value, txtRecordObjectToSet)
}
}
.bind(host: "localhost", port: 0).wait()
XCTAssertNoThrow(try listener.close().wait())
}
}

func testCanObserveValueOfServiceTXTRecord() throws {
let txtRecordToSet = Data([0x00, 0x01])
let listener = try NIOTSListenerBootstrap(group: self.group)
.serverChannelInitializer { channel in
return channel.getOption(NIOTSChannelOptions.serviceTXTRecord).map { value in
XCTAssertNil(value)
}.flatMap {
channel.setOption(NIOTSChannelOptions.serviceTXTRecord, value: txtRecordToSet)
}.flatMap {
channel.getOption(NIOTSChannelOptions.serviceTXTRecord)
}.map { value in
XCTAssertEqual(value, txtRecordToSet)
}
}
.bind(host: "localhost", port: 0).wait()
XCTAssertNoThrow(try listener.close().wait())
}

func testCanUpdateServiceTXTRecordObject() throws
{
if #available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
let listener = try NWListener(using: .tcp)
listener.service = NWListener.Service(type: "_http._tcp")
XCTAssertNil(listener.service?.txtRecordObject)

let txtRecordObjectToSet = NWTXTRecord(["key": "value"])
let bootstrap = try NIOTSListenerBootstrap(group: self.group)
.serverChannelOption(NIOTSChannelOptions.serviceTXTRecordObject, value: txtRecordObjectToSet)
.withNWListener(listener).wait()
defer {
XCTAssertNoThrow(try bootstrap.close().wait())
}

XCTAssertEqual(listener.service?.txtRecordObject, txtRecordObjectToSet)
}
}

func testCanUpdateServiceTXTRecord() throws
{
let listener = try NWListener(using: .tcp)
listener.service = NWListener.Service(type: "_http._tcp")
XCTAssertNil(listener.service?.txtRecord)

let txtRecordToSet = Data([0x00, 0x01])
let bootstrap = try NIOTSListenerBootstrap(group: self.group)
.serverChannelOption(NIOTSChannelOptions.serviceTXTRecord, value: txtRecordToSet)
.withNWListener(listener).wait()
defer {
XCTAssertNoThrow(try bootstrap.close().wait())
}

XCTAssertEqual(listener.service?.txtRecord, txtRecordToSet)
}

func testChannelEmitsChannels() throws {
class ChannelReceiver: ChannelInboundHandler {
typealias InboundIn = Channel
Expand Down