Skip to content

Commit

Permalink
Merge pull request #44 from onlxltd/version/1.2.0
Browse files Browse the repository at this point in the history
Version 1.2.0
  • Loading branch information
mdidon committed Jan 3, 2024
2 parents a3aaf19 + 1ba71e5 commit e9e4e25
Show file tree
Hide file tree
Showing 14 changed files with 813 additions and 781 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:

strategy:
matrix:
node-version: [12.x, 14.x, 16.x]
node-version: [14.x, 16.x, 18.x, 20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 14
node-version: 16
registry-url: https://registry.npmjs.org/
- run: yarn install
- run: yarn build
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ multicast DNS.

This is a rewrite of the project Bonjour (https://github.com/watson/bonjour) into modern TypeScript.

bonjour-service is supported by [ON LX Limited](https://onlx.ltd/?src=bonjour-service). Check out our projects such as [Ctrl Suite](https://onlx.ltd/ctrl-suite?src=bonjour-service) and [Ctrl for iPad](https://onlx.ltd/ctrl-for-ipad?src=bonjour-service).



## Installation
Expand Down Expand Up @@ -69,6 +71,7 @@ Options are:
- `protocol` (string, optional) - `udp` or `tcp` (default)
- `txt` (object, optional) - a key/value object to broadcast as the TXT
record
- `disableIPv6` (boolean, optional) disble IPv6 addresses

IANA maintains a [list of official service types and port
numbers](http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml).
Expand Down Expand Up @@ -116,6 +119,10 @@ Emitted every time a new service is found that matches the browser.

Emitted every time an existing service emmits a goodbye message.

#### `Event: txt-update`

Emitted every time an existing service does a new announcement with an updated TXT record.

#### `browser.services()`

An array of services known by the browser to be online.
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{
"name": "bonjour-service",
"version": "1.0.0",
"version": "1.2.0",
"description": "A Bonjour/Zeroconf implementation in TypeScript",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"array-flatten": "^2.1.2",
"dns-equal": "^1.0.0",
"fast-deep-equal": "^3.1.3",
"multicast-dns": "^7.2.5"
},
Expand Down
19 changes: 10 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import Registry from './lib/registry'
import Server from './lib/mdns-server'
import Browser, { BrowserConfig } from './lib/browser'
import Registry from './lib/registry'
import Server from './lib/mdns-server'
import Browser, { BrowserConfig } from './lib/browser'
import Service, { ServiceConfig, ServiceReferer } from './lib/service'

export class Bonjour {
Expand All @@ -14,7 +14,7 @@ export class Bonjour {
* @param opts ServiceConfig | undefined
* @param errorCallback Function | undefined
*/
constructor(opts?: ServiceConfig | undefined, errorCallback?: Function | undefined) {
constructor(opts: Partial<ServiceConfig> = {}, errorCallback?: Function | undefined) {
this.server = new Server(opts, errorCallback)
this.registry = new Registry(this.server)
}
Expand Down Expand Up @@ -43,7 +43,7 @@ export class Bonjour {
* @param onup Callback when up event received
* @returns
*/
public find(opts: BrowserConfig | undefined = undefined, onup?: (service: Service) => void): Browser {
public find(opts: BrowserConfig | null = null, onup?: (service: Service) => void): Browser {
return new Browser(this.server.mdns, opts, onup)
}

Expand All @@ -54,9 +54,9 @@ export class Bonjour {
* @param callback Callback when device found
* @returns
*/
public findOne(opts: BrowserConfig | undefined = undefined, timeout = 10000, callback?: CallableFunction): Browser {
public findOne(opts: BrowserConfig | null = null, timeout = 10000, callback?: CallableFunction): Browser {
const browser: Browser = new Browser(this.server.mdns, opts)
var timer: any
var timer: NodeJS.Timeout
browser.once('up', (service: Service) => {
if(timer !== undefined) clearTimeout(timer)
browser.stop()
Expand All @@ -71,10 +71,11 @@ export class Bonjour {

/**
* Destroy the class
* @param callback Callback when underlying socket is closed
*/
public destroy() {
public destroy(callback?: CallableFunction) {
this.registry.destroy()
this.server.mdns.destroy()
this.server.mdns.destroy(callback)
}

}
Expand Down
60 changes: 39 additions & 21 deletions src/lib/browser.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import KeyValue from './KeyValue'
import DnsTxt from './dns-txt'
import dnsEqual from 'dns-equal'
import dnsEqual from './utils/dns-equal'
import { EventEmitter } from 'events'
import Service, { ServiceRecord } from './service'
import { toString as ServiceToString, toType as ServiceToType } from './service-types'
import filterService from './utils/filter-service'
import filterTxt from './utils/filter-txt'
import equalTxt from './utils/equal-txt'

const TLD = '.local'
const WILDCARD = '_services._dns-sd._udp' + TLD


export interface BrowserConfig {
type: string
protocol?: 'tcp' | 'udp'
subtypes?: Array<string>
txt?: any
type : string
name? : string
protocol? : 'tcp' | 'udp'
subtypes? : string[]
txt? : KeyValue
}

export type BrowserOnUp = (service: Service) => void

/**
* Start a browser
*
Expand All @@ -36,32 +39,28 @@ export interface BrowserConfig {
export class Browser extends EventEmitter {

private mdns : any
private onresponse : any = null
private onresponse : CallableFunction | undefined = undefined
private serviceMap : KeyValue = {}

private txt : any
private name? : string
private txtQuery : KeyValue | undefined
private wildcard : boolean = false

private _services : Array<any> = []
private _services : Service[] = []

constructor(mdns: any, opts: any, onup?: (service: Service) => void) {
constructor(mdns: any, opts: BrowserConfig | BrowserOnUp | null, onup?: BrowserOnUp) {
super()

if (typeof opts === 'function') return new Browser(mdns, null, opts)
if (typeof opts === 'function') return new Browser(mdns, null, opts as BrowserOnUp)

this.mdns = mdns
this.txt = new DnsTxt(opts !== null && opts.txt != null ? opts.txt : undefined)

if(opts != null && opts.txt != null) {
this.txt = new DnsTxt(opts.txt)
} else {
this.txt = new DnsTxt()
}

if (!opts || !opts.type) {
this.name = WILDCARD
this.wildcard = true
if (opts === null || opts.type === undefined) {
this.name = WILDCARD
this.wildcard = true
} else {
this.name = ServiceToString({ name: opts.type, protocol: opts.protocol || 'tcp'}) + TLD
if (opts.name) this.name = opts.name + '.' + this.name
Expand Down Expand Up @@ -105,7 +104,10 @@ export class Browser extends EventEmitter {
if (matches.length === 0) return

matches.forEach((service: Service) => {
if (self.serviceMap[service.fqdn]) return // ignore already registered services
if (self.serviceMap[service.fqdn]) {
self.updateService(service)
return
}
self.addService(service)
})
})
Expand All @@ -119,7 +121,7 @@ export class Browser extends EventEmitter {
if (!this.onresponse) return

this.mdns.removeListener('response', this.onresponse)
this.onresponse = null
this.onresponse = undefined
}

public update() {
Expand All @@ -138,6 +140,22 @@ export class Browser extends EventEmitter {
this.emit('up', service)
}

private updateService(service: Service) {
// check if txt updated
if (equalTxt(service.txt, this._services.find((s) => dnsEqual(s.fqdn, service.fqdn))?.txt || {})) return
// if the new service is not allowed by the txt query, remove it
if(!filterService(service, this.txtQuery)) {
this.removeService(service.fqdn)
return
}
// replace service
this._services = this._services.map(function (s) {
if (!dnsEqual(s.fqdn, service.fqdn)) return s
return service
})
this.emit('txt-update', service);
}

private removeService(fqdn: string) {
var service, index
this._services.some(function (s, i) {
Expand Down Expand Up @@ -176,7 +194,7 @@ export class Browser extends EventEmitter {
// https://tools.ietf.org/html/rfc6763#section-7.1
// Selective Instance Enumeration (Subtypes)
//
private buildServicesFor(name: string, packet: any, txt: any, referer: any) {
private buildServicesFor(name: string, packet: any, txt: KeyValue, referer: any) {
var records = packet.answers.concat(packet.additionals).filter( (rr: ServiceRecord) => rr.ttl > 0) // ignore goodbye messages

return records
Expand Down
27 changes: 13 additions & 14 deletions src/lib/mdns-server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import flatten from 'array-flatten'
import { ServiceRecord } from './service'
import deepEqual from 'fast-deep-equal/es6'

import MulticastDNS from 'multicast-dns'
import dnsEqual from 'dns-equal'
import { ServiceConfig, ServiceRecord } from './service'
import MulticastDNS from 'multicast-dns'
import KeyValue from './KeyValue'
import deepEqual from 'fast-deep-equal/es6'
import dnsEqual from './utils/dns-equal'

export class Server {

public mdns : any
private registry : any = {}
private errorCallback : Function;
private registry : KeyValue = {}
private errorCallback : Function

constructor(opts: any, errorCallback?: Function | undefined) {
constructor(opts: Partial<ServiceConfig>, errorCallback?: Function | undefined) {
this.mdns = MulticastDNS(opts)
this.mdns.setMaxListeners(0)
this.mdns.on('query', this.respondToQuery.bind(this))
this.errorCallback = errorCallback ?? function(err: any) {throw err;}
this.errorCallback = errorCallback ?? function(err: any) { throw err }
}

public register(records: Array<ServiceRecord> | ServiceRecord) {
Expand Down Expand Up @@ -58,15 +57,15 @@ export class Server {
}
}

private respondToQuery(query: any): any {
private respondToQuery(query: KeyValue): void {
let self = this
query.questions.forEach((question: any) => {
var type = question.type
var name = question.name

// generate the answers section
var answers = type === 'ANY'
? flatten.depth(Object.keys(self.registry).map(self.recordsFor.bind(self, name)), 1)
? Object.keys(self.registry).map(self.recordsFor.bind(self, name)).flat(1)
: self.recordsFor(name, type)

if (answers.length === 0) return
Expand Down Expand Up @@ -106,7 +105,7 @@ export class Server {
})
}

private recordsFor(name: string, type: string): Array<any> {
private recordsFor(name: string, type: string): boolean[] {
if (!(type in this.registry)) {
return []
}
Expand All @@ -117,7 +116,7 @@ export class Server {
})
}

private isDuplicateRecord (a: ServiceRecord): (b: ServiceRecord) => any {
private isDuplicateRecord (a: ServiceRecord): (b: ServiceRecord) => boolean {
return (b: ServiceRecord) => {
return a.type === b.type &&
a.name === b.name &&
Expand Down
27 changes: 13 additions & 14 deletions src/lib/registry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import flatten from 'array-flatten'
import dnsEqual from 'dns-equal'
import dnsEqual from './utils/dns-equal'
import Server from './mdns-server'
import Service, { ServiceConfig, ServiceRecord } from './service'



const REANNOUNCE_MAX_MS : number = 60 * 60 * 1000
const REANNOUNCE_FACTOR : number = 3
const noop = function () {}

export class Registry {

Expand All @@ -19,18 +17,18 @@ export class Registry {

public publish(config: ServiceConfig): Service {

function start(service: Service,registry: Registry, opts: {probe: boolean}) {
function start(service: Service, registry: Registry, opts?: { probe: boolean }) {
if (service.activated) return
service.activated = true

registry.services.push(service)

if(!(service instanceof Service)) return

if(opts.probe) {
registry.probe(registry.server.mdns, service, (exists: any) => {
if(opts?.probe) {
registry.probe(registry.server.mdns, service, (exists: boolean) => {
if(exists) {
service.stop()
if(service.stop !== undefined) service.stop()
console.log(new Error('Service name is already in use on the network'))
return
}
Expand All @@ -42,9 +40,10 @@ export class Registry {
}

function stop(service: Service, registry: Registry, callback?: CallableFunction) {
if (!service.activated) return
if (!callback) callback = noop
if (!service.activated) return process.nextTick(callback)

if(!(service instanceof Service)) return
if(!(service instanceof Service)) return process.nextTick(callback)
registry.teardown(registry.server, service, callback)

const index = registry.services.indexOf(service)
Expand Down Expand Up @@ -109,7 +108,7 @@ export class Registry {
return dnsEqual(rr.name, service.fqdn)
}

const done = (exists: any) => {
const done = (exists: boolean) => {
mdns.removeListener('response', onresponse)
clearTimeout(timer)
callback(!!exists)
Expand Down Expand Up @@ -167,16 +166,16 @@ export class Registry {

services = services.filter((service: Service) => service.activated) // ignore services not currently starting or started

var records: any = flatten.depth(services.map(function (service) {
var records: any = services.flatMap(function (service) {
service.activated = false
var records = service.records()
records.forEach((record: ServiceRecord) => {
record.ttl = 0 // prepare goodbye message
})
return records
}), 1)
})

if (records.length === 0) return callback && callback()
if (records.length === 0) return callback && process.nextTick(callback)
server.unregister(records)

// send goodbye message
Expand Down
Loading

0 comments on commit e9e4e25

Please sign in to comment.