Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
watson committed Oct 24, 2015
0 parents commit 0c70bfd
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules
5 changes: 5 additions & 0 deletions .travis.yml
@@ -0,0 +1,5 @@
language: node_js
node_js:
- '4'
- '0.12'
- '0.10'
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 Thomas Watson Steen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
5 changes: 5 additions & 0 deletions index.js
@@ -0,0 +1,5 @@
'use strict'

exports.tcp = require('./tcp')
exports.udp = require('./udp')
exports.unpublishAll = require('./lib/registry').unpublishAll
63 changes: 63 additions & 0 deletions lib/mdns-server.js
@@ -0,0 +1,63 @@
'use strict'

var registry = {}

var mdns = require('./mdns')

mdns.on('query', respondToQuery)

exports.register = function (records) {
if (Array.isArray(records)) records.forEach(register)
else register(records)

function register (record) {
// TODO: Should the registry be able to hold two records with the
// same name and type? Or should the new record replace the old?
var subRegistry = registry[record.type]
if (!subRegistry) subRegistry = registry[record.type] = []
subRegistry.push(record)
}
}

exports.unregister = function (records) {
if (Array.isArray(records)) records.forEach(unregister)
else unregister(records)

function unregister (record) {
var type = record.type
if (!(type in registry)) return
registry[type] = registry[type].filter(function (r) {
return r.name !== record.name
})
}
}

function respondToQuery (query) {
query.questions.forEach(function (question) {
var type = question.type
var name = question.name
var records = type === 'ANY'
? flatten(Object.keys(registry).map(recordsFor.bind(null, name))) // TODO: In case of ANY, should the PTR records be the primary records and everything else should be listed as additionals?
: recordsFor(type, name)

if (records.length === 0) return

// TODO: When responding to PTR queries, the additionals array should be
// populated with the related SRV and TXT records

mdns.respond(records, function (err) {
if (err) throw err // TODO: Handle this (if no callback is given, the error will be ignored)
})
})
}

function recordsFor (name, type) {
if (!(type in registry)) return []
return registry[type].filter(function (record) {
return record.name === name
})
}

function flatten (arr) {
return [].concat.apply.bind([].concat, [])(arr)
}
3 changes: 3 additions & 0 deletions lib/mdns.js
@@ -0,0 +1,3 @@
'use strict'

module.exports = require('multicast-dns')()
154 changes: 154 additions & 0 deletions lib/registry.js
@@ -0,0 +1,154 @@
'use strict'

var mdns = require('./mdns')
var server = require('./mdns-server')
var Service = require('./service')

var services = []

var REANNOUNCE_MAX_MS = 60 * 60 * 1000
var REANNOUNCE_FACTOR = 3

exports.publish = function publish (protocol, opts) {
if (typeof opts === 'string') return publish(protocol, { type: opts, port: arguments[2] })
if (!opts.type) throw new Error('Required type not given')
if (!opts.port) throw new Error('Required port not given')

var service = new Service(opts)
service.unpublish = function (cb) {
teardown(service, cb)
var index = services.indexOf(service)
if (index !== -1) services.splice(index, 1)
}
services.push(service)

// TODO: The RFC allows that probing can be optional if it's know that
// the name is unique or in some way is already owned by the service (but
// maybe the class then need to change)
probe(service, function (exists) {
if (exists) throw new Error('Service name is already in use on the network') // TODO: Handle this. Maybe implement fallback option to auto-increment a number trailing the name
announce(service)
})

return service
}

exports.unpublishAll = function () {
teardown(services)
services = []
}

/**
* Check if a service name is already in use on the network.
*
* Used before announcing the new service.
*
* To guard against race conditions where multiple services are started
* simultaneously on the network, wait a random amount of time (between
* 0 and 250 ms) before probing.
*
* TODO: Add support for Simultaneous Probe Tiebreaking:
* https://tools.ietf.org/html/rfc6762#section-8.2
*/
function probe (service, cb) {
var sent = false
var retries = 0
var retryTimer

mdns.on('response', onresponse)
setTimeout(send, Math.random() * 250)

function send () {
// abort if the service have been unpublished in the meantime
if (!service.published) return

mdns.query(service.name, 'ANY', function () {
// This function will optionally be called with an error object. We'll
// just silently ignore it and retry as we normally would
// TODO: Maybe we should not just ignore it we have no successfull probes at all?
sent = true
if (++retries < 3) retryTimer = setTimeout(send, 250)
else done()
})
}

function onresponse (packet) {
// Apparently conflicting Multicast DNS responses received *before*
// the first probe packet is sent MUST be silently ignored (see
// discussion of stale probe packets in RFC 6762 Section 8.2,
// "Simultaneous Probe Tiebreaking" at
// https://tools.ietf.org/html/rfc6762#section-8.2
if (!sent) return

if (packet.answers.some(exists) || packet.additionals.some(exists)) done(true)
}

function exists (answer) {
return answer.name === service.name
}

function done (exists) {
mdns.removeListener('response', onresponse)
clearTimeout(retryTimer)
cb(!!exists)
}
}

/**
* Initial service announcement
*
* Used to announce new services when they are first registered.
*
* Broadcasts right away, then after 3 seconds, 9 seconds, 27 seconds,
* and so on, up to a maximum interval og one hour.
*/
function announce (service) {
var delay = 1000
var packet = service.records()

server.register(packet)

;(function broadcast () {
// abort if the service have been unpublished in the meantime
if (!service.published) return

mdns.respond(packet, function () {
// This function will optionally be called with an error object. We'll
// just silently ignore it and retry as we normally would
delay = delay * REANNOUNCE_FACTOR
if (delay < REANNOUNCE_MAX_MS) setTimeout(broadcast, delay)
})
})()
}

/**
* Stop a given service
*
* Besides removing the service from the mDNS registry, it's recommended to
* send a "goodbye" message to let the network know about the shutdown.
*/
function teardown (services, cb) {
if (!Array.isArray(service)) services = [services]

var records = flatten(services
.filter(function (service) {
return service.published
})
.map(function (service) {
service.published = false
var records = service.records()
records.forEach(function (record) {
record.ttl = 0
})
return records
}))

if (records.length === 0) return

server.unregister(records)
mdns.respond(records, cb)
}

function flatten (arr) {
return [].concat.apply.bind([].concat, [])(arr)
}
51 changes: 51 additions & 0 deletions lib/service.js
@@ -0,0 +1,51 @@
'use strict'

var os = require('os')
var serviceName = require('multicast-dns-service-types')
var mdns = require('./mdns')

var TLD = '.local'
var hostname = os.hostname()

var Service = module.exports = function (opts) {
this.name = opts.name || hostname.replace(/\.local\.?$/, '')
this.type = serviceName.stringify(opts.type, opts.protocol)
this.port = opts.port
this.fqdn = this.name + '.' + this.type + TLD, // TODO: Encode illegal chars in name
this.txt = opts.txt
this.published = true
}

Service.prototype.records = function () {
return [rr_ptr(this), rr_srv(this), rr_txt(this)]
}

function rr_ptr (service) {
return {
name: service.type + TLD,
type: 'PTR',
ttl: 28800,
data: service.fqdn
}
}

function rr_srv (service) {
return {
name: service.fqdn,
type: 'SRV',
ttl: 120,
data: {
port: service.port,
target: hostname
}
}
}

function rr_txt (service) {
return {
name: service.fqdn,
type: 'TXT',
ttl: 4500,
data: service.txt
}
}
7 changes: 7 additions & 0 deletions tcp.js
@@ -0,0 +1,7 @@
'use strict'

var publish = require('./lib/registry').publish

var PROTOCOL = 'tcp'

exports.publish = publish.bind(null, PROTOCOL)
7 changes: 7 additions & 0 deletions udp.js
@@ -0,0 +1,7 @@
'use strict'

var publish = require('./lib/registry').publish

var PROTOCOL = 'udp'

exports.publish = publish.bind(null, PROTOCOL)

0 comments on commit 0c70bfd

Please sign in to comment.