Source: mdns.js

/**
 * Multicast DNS based service & host discovery and registration
 *
 * @constructor
 * @param {strategy} instrategy - a strategy to use for host-dependent operations
 */

sensible.MDNS = function ()
{
	this.strategy = sensible.StrategyFactory.createStrategy ();
	
	this.foundHosts = new Object ();
	
	this.hostsByName = new Object ();
	this.hostResolutionsByName = new Object ();

	this.foundServices = new Object ();

	this.servicesByKey = new Object ();
	this.servicesByType = new Object ();
	this.serviceResolutionsByType = new Object ();
	
	// note the polling period should be less than the default TTL
	// polling period is in ms, TTL is in seconds, haha
	this.pollingPeriod = 30000;
	this.defaultTTL = 500;
};

/**
 * Start Multicast DNS processing.
 * Must be called prior to any request for discovery or registration.
 *
 * @param {function} inCallback - function to be called once startup is complete
 */

sensible.MDNS.prototype.start = function (inCallback)
{	
	var	self = this;
	
	this.strategy.open
	(
		function (inError)
		{
			if (inError)
			{
				console.log ("error calling open: " + inError.toString ());
				inCallback (inError);
			}
			else
			{
				// we set up a fake service record
				// for name resolution off .local
				// which we do automagically
				self.localService = new Object ();
				
				self.strategy.getHostName
				(
					function (inHostName)
					{
						self.localService.name = inHostName;
						
						if (self.localService.name && self.localService.name.length)
						{
							// some hosts give .local in self and some don't
							// we especially want to match the ones that don't
							self.localService.name = self.localService.name.replace (".local", "");
							self.localService.name = self.localService.name.toLowerCase ();
						}
						else
						{
							console.log ("MDNS() couldn't find hostname");
							self.localService.name = "unknown";
						}
				
						self.strategy.getIPAddress
						(
							function (inIPAddress)
							{
								self.localService.host = inIPAddress;
				
								self.strategy.listen (self.onPacketReceived.bind (self));
								self.startPolling ();
								
								inCallback ();
							}
						);
					}
				);
			}
		}
	);
}

/**
 * Cancel a request to resolve a hostname to an address.
 *
 * @param {object} inResolution - resolution descriptor returned by resolveHost()
 */

sensible.MDNS.prototype.cancelResolveHost = function (inResolution)
{
	var	result = false;
	var	resolutions = this.hostResolutionsByName [inResolution.name];
	
	if (resolutions)
	{
		for (var i = 0; i < resolutions.length; i++)
		{
			if (this.resolutions [i] == inResolution)
			{
				resolutions.splice (i, 1);
				result = true;
				break;
			}
		}
	}
	
	return result;
}

/**
 * Cancel a request to discover a service type.
 *
 * @param {object} inResolution - resolution descriptor returned by resolveService()
 */

sensible.MDNS.prototype.cancelResolveService = function (inResolution)
{
	console.log ("MDNS.unregisterService(" + inResolution.type + ")");

	var	result = false;
	var	resolutions = this.serviceResolutionsByType [inResolution.type];
	
	if (resolutions)
	{
		for (var i = 0; i < resolutions.length; i++)
		{
			if (this.resolutions [i] == inResolution)
			{
				resolutions.splice (i, 1);
				result = true;
				break;
			}
		}
	}
	
	return result;
}

/**
 * Stop Multicast DNS operations.
 *
 */

sensible.MDNS.prototype.stop = function ()
{
	this.stopPolling ();
	this.strategy.close ();
}

/**
 * Register a host for proxy MDNS A record resolution.
 * Note we leave names alone, we don't remove .local or anything like that.
 *
 * @param {string} inName - name to proxy resolve, eg printer.local
 * @param {string} inHost - host to proxy resolve, eg 10.0.1.10
 */

sensible.MDNS.prototype.registerHost = function (inName, inHost)
{
	var	name = inName.toLowerCase ();
	
	if (this.hostsByName [name])
	{
		console.error ("host at " + key + " already registered...");
		return null;
	}

	// passing null or undefined for the host means "just get it"
	if (inHost == null || inHost.length == 0)
	{
		inHost = this.localService.host;
	}

	this.hostsByName [name] = inHost;
	
	return name;
}

/**
 * Register a service for proxy MDNS PTR record resolution.
 *
 * @param {string} inName - human readable service name, eg My Printer
 * @param {string} inType - type of service, eg _printer._tcp.local
 * @param {string} inHost - host of service, eg 10.0.1.10
 * @param {integer} inPort - port number of service, eg 2000
 * @param {string} inTXTRecord - optional text record to additionally serve
 */

sensible.MDNS.prototype.registerService = function (inName, inType, inHost, inPort, inTXTRecord, inTTL)
{
	console.log ("sensible.MDNS.registerService(" + inName + ")");
	
	// passing null or undefined for the host means "just get it"
	if (inHost == null || inHost.length == 0)
	{
		inHost = this.localService.host;
	}
	
	// note the key is just the host and port
	// that's really the unique combo
	var	key = inHost + ":" + inPort;
	
	if (this.servicesByKey [key])
	{
		console.error ("service at " + key + " already registered...");
		return null;
	}
	
	var	lowerCaseType = inType.toLowerCase ();
	
	var	service = 
	{
		name: inName,
		type: lowerCaseType,
		host: inHost,
		port: inPort,
		text: inTXTRecord,
		ttl: inTTL ? inTTL : this.defaultTTL
	};
	
	this.servicesByKey [key] = service;

	if (this.servicesByType [lowerCaseType])
	{
		// there can't be a dupe, we'd have found it above
		// so just add to the existing list
	}
	else
	{
		this.servicesByType [lowerCaseType] = new Array ();
	}
	
	this.servicesByType [lowerCaseType].push (service);

	// ensure that everyone has up to date info
	this.sendPTRResponse (service);

	return service;
}

/**
 * Request notification of name to host resolutions.
 *
 * @param {string} inName - name to resolve, eg printer.local
 * @param {function} inCallback - function to call on resolution
 * @returns {object} object to provide to unresolveHost() call to cancel
 */

sensible.MDNS.prototype.resolveHost = function (inName, inAddCallback, inRemoveCallback)
{
	var	lowerCaseName = inName.toLowerCase ();
	var	resolutions = this.hostResolutionsByName [lowerCaseName];
	
	if (!resolutions)
	{
		resolutions = new Array ();
		this.hostResolutionsByName [lowerCaseName] = resolutions;
	}
	
	var	resolution = 
	{
		name: lowerCaseName,
		addCallback: inAddCallback,
		removeCallback: inRemoveCallback,
		hosts: new Object ()
	};
	
	resolutions.push (resolution);
	
	this.sendARequest (lowerCaseName);
	
	return resolution;
}

/**
 * Request notification of service resolutions.
 *
 * @param {string} inType - type to resolve, eg _printer._tcp.local
 * @param {function} inCallback - function to call on resolution
 * @returns {object} object to provide to unresolveService() call to cancel
 */
sensible.MDNS.prototype.resolveService = function (inType, inAddCallback, inRemoveCallback)
{
	console.log ("MDNS.resolveService(" + inType + ")");
	
	var	lowerCaseType = inType.toLowerCase ();
	var	resolutions = this.serviceResolutionsByType [lowerCaseType];
	
	if (!resolutions)
	{
		resolutions = new Array ();
		this.serviceResolutionsByType [lowerCaseType] = resolutions;
	}
	
	var	resolution = 
	{
		type: lowerCaseType,
		addCallback: inAddCallback,
		removeCallback: inRemoveCallback,
		services: new Object ()
	};
	
	resolutions.push (resolution);
	
	this.sendPTRRequest (lowerCaseType);
	
	return resolution;
}

/**
 * Cancel notification of host resolutions.
 *
 * @param {string} inName - name to cancel resolution, eg printer.local
 */
sensible.MDNS.prototype.unregisterHost = function (inName)
{
	var	unregistered = false;
	
	var	host = this.hostsByName [inName.toLowerCase ()];
	
	if (host)
	{
		unregistered = true;
		delete this.hostsByName [name];
	}
	else
	{
		console.error ("name " + name + " not registered...");
	}
	
	return unregistered;
}

/**
 * Cancel the advertisement of a service.
 *
 * @param {string} inHost - host of service to cancel, eg 10.0.1.10
 * @param {integer} inPort - port number of service to cancel, eg 2000
 */
sensible.MDNS.prototype.unregisterService = function (inHost, inPort)
{
	var	unregistered = false;
	
	// passing null or undefined for the host means "just get it"
	if (inHost == null || inHost.length == 0)
	{
		inHost = this.localService.host;
	}
	
	var	key = inHost + ":" + inPort;
	
	var	service = this.servicesByKey [key];

	if (service)
	{
		unregistered = true;
		
		// service.type is lowercased by register()
		var	services = this.servicesByType [service.type];
		
		for (var i = 0; i < services.length; i++)
		{
			if (services [i].host == inHost && services [i].port == inPort)
			{
				services.splice (i, 1);
				break;
			}
		}

		delete this.servicesByKey [key];
	}
	else
	{
		console.error ("service " + key + " not registered...");
	}
	
	return unregistered;
}

// CALLBACKS

/**
 * Called by the configured strategy upon receipt of a packet.
 *
 * @private
 * @param {ArrayBuffer} inPacketBuffer - packet
 * @param {string} inRemoteHost - originating host, eg 10.0.1.11
 * @param {integer} inRemotePort - originating port, eg 2000
 */
sensible.MDNS.prototype.onPacketReceived = function (inPacketBuffer, inRemoteHost, inRemotePort)
{
	var	packet = sensible.DNSPacket.parse (inPacketBuffer);
	
	if (packet.flags & 0x8000)
	{
		// this is a response
		if (packet.answers.length > 0)
		{
			var	answer = null;
			
			for (var i = 0; i < packet.answers.length; i++)
			{
				// dns record type 1 = A
				if (packet.answers [i].type == 1)
				{
					answer = packet.answers [i];

					var	lowerCaseName = answer.name.toLowerCase ();
					// console.log ("received A response for " + lowerCaseName);
					
					var	resolutions = this.hostResolutionsByName [lowerCaseName];
					
					if (resolutions)
					{
						var	host = this.getHostInfo (answer);
						this.foundHosts [lowerCaseName] = host;
						
						// find outstanding resolutions for this host
						for (var i = 0; i < resolutions.length; i++)
						{
							if (resolutions [i].addCallback)
							{
								if (resolutions [i].hosts [host.key])
								{
									// this client already knows about this host
									// console.log ("client already knows about host " + host.key);
								}
								else
								{
									resolutions [i].addCallback (host);
								}
							}
						}
					}
				}
				else
				// dns record type 12 = PTR
				if (packet.answers [i].type == 12)
				{
					answer = packet.answers [i];

					var	lowerCaseType = answer.name.toLowerCase ();
					// console.log ("received PTR response for " + lowerCaseType);
					
					// are we tracking this type?
					var	resolutions = this.serviceResolutionsByType [lowerCaseType];
					
					if (resolutions)
					{
						// ok, remix the packet into something sensible
						var	service = this.getServiceInfo (packet, inRemoteHost, lowerCaseType);
						this.foundServices [service.key] = service;
					
						for (var i = 0; i < resolutions.length; i++)
						{
							if (resolutions [i].addCallback)
							{
								if (resolutions [i].services [service.key])
								{
									// this client already knows about this service
									console.log ("client already knows about service " + service.key);
								}
								else
								{
									resolutions [i].services [service.key] = true;
									resolutions [i].addCallback (service);
								}
							}
						}
					}
					else
					{
						// i think the system works well enough to remove this log now :-)
						// console.log ("no resolutions outstanding for " + lowerCaseType);
					}
				}
			}
		}
	}
	else
	{
		// this is a request, possibly
		if (packet.questions.length > 0)
		{
			var	question = packet.questions [0];
		
			// dns record type 1 = A
			if (question.type == 1)
			{
				var	hostName = question.name.toLowerCase ();
				// console.log ("received A request for " + hostName);

				// console.log ("request for A record for " + hostName);

				// console.log ("checking " + hostName + " against " + this.localService.name);
				
				if (hostName == this.localService.name)
				{
					this.sendAResponse (hostName, this.localService.host);
				}
				else
				{
					// could be a proxy host registration for someone else...
					var	host = this.hostsByName [hostName];
					
					if (host)
					{
						this.sendAResponse (hostName, host);
					}
				}
			}
			else
			// dns record type 12 = PTR
			if (question.type == 12)
			{
				// service type should be something like _appletv._tcp.local
				var	lowerCaseType = question.name.toLowerCase ();
				// console.log ("received PTR request for " + lowerCaseType);

				// console.log ("received request for type " + lowerCaseType + " with length " + inPacketBuffer.byteLength);
	
				var	services = this.servicesByType [lowerCaseType];
				
				if (services)
				{
					// console.log ("found " + services.length + " services registered for " + lowerCaseType);
	
					for (var i = 0; i < services.length; i++)
					{
						this.sendPTRResponse (services [i]);
					}
				}
			}
		}
	}
	
	return packet;	
}

// PRIVATE

/**
 * Inform the clients of this host that it has been removed.
 *
 * @private
 * @param {object} inHost - host to expire
 */

sensible.MDNS.prototype.expireHost = function (inHost)
{
	var	resolutions = this.hostResolutionsByName [inHost.name];
	
	if (resolutions)
	{
		for (var i = 0; i < resolutions.length; i++)
		{
			if (resolutions [i].removeCallback)
			{
				resolutions [i].removeCallback (inHost);
			}
		}
	}
}

/**
 * Inform the clients of this service that it has been removed.
 *
 * @private
 * @param {object} inService - service to expire
 */

sensible.MDNS.prototype.expireService = function (inService)
{
	var	resolutions = this.serviceResolutionsByType [inService.type];
	
	if (resolutions)
	{
		for (var i = 0; i < resolutions.length; i++)
		{
			if (resolutions [i].removeCallback)
			{
				resolutions [i].removeCallback (inService);
			}
		}
	}
}

/**
 * Remove any hosts whose TTL has expired, informing their clients.
 * The assumption is that the TTL period is greater than the polling period.
 *
 * @private
 */

sensible.MDNS.prototype.expireHosts = function ()
{
	var	now = new Date ().getTime ();
	
	for (var name in this.foundHosts)
	{
		var	host = this.foundHosts [name];
		
		// each service has a TTL and a timestamp when it was last discovered
		// so combine the two to find the expiry in ms, and check...
		var	expiryTime = host.timestamp + (host.ttl * 1000);
		
		if (expiryTime < now)
		{
			console.log ("expiring service: " + key);
			
			this.expireHost (service);
			
			delete this.foundHosts [key];
		}
	}
}

/**
 * Remove any services whose TTL has expired, informing their clients.
 * The assumption is that the TTL period is greater than the polling period.
 *
 * @private
 */

sensible.MDNS.prototype.expireServices = function ()
{
	var	now = new Date ().getTime ();
	
	for (var key in this.foundServices)
	{
		var	service = this.foundServices [key];
		
		// each service has a TTL and a timestamp when it was last discovered
		// so combine the two to find the expiry in ms, and check...
		var	expiryTime = service.timestamp + (service.ttl * 1000);
		
		if (expiryTime < now)
		{
			console.log ("expiring service: " + key);
			
			this.expireService (service);
			
			delete this.foundServices [key];
		}
	}
}

/**
 * Extract the pertinent information from an A type DNS packet.
 *
 * @private
 * @param {ArrayBuffer} inPacketBuffer - packet
 * @returns {object} object containing name, address, and TTL
 */

sensible.MDNS.prototype.getHostInfo = function (inARecord)
{
	// note the default TTL should be more than the polling period (60 seconds)
	// we set it to five minutes, here
	var	host = 
	{
		name: inARecord.name.toLowerCase (),
		address: inARecord.a,
		ttl: inARecord.ttl,
		rdata: inARecord.rdata,
		timestamp: new Date ().getTime ()
	};
	
	if ((host.ttl * 1000) < this.pollingPeriod)
	{
		host.ttl = this.defaultTTL;
	}

	// abstracting how we make the key == good
	host.key = host.name;
	
	return host;
}

	
/**
 * Extract the pertinent information from a PTR type DNS packet.
 *
 * @private
 * @param {ArrayBuffer} inPacketBuffer - packet
 * @param {string} inRemoteHost - originating host, eg 10.0.1.11
 * @param {string} inType - type of resolution, eg _printer._tcp.local
 * @returns {object} object containing name, type, host, port, text, and TTL
 */

sensible.MDNS.prototype.getServiceInfo = function (inPacket, inRemoteHost, inType)
{
	// note the default TTL should be more than the polling period (60 seconds)
	// we set it to five minutes, here
	var	service = 
	{
		name: "",
		type: inType,
		host: inRemoteHost,
		port: 0,
		text: "",
		ttl: this.defaultTTL,
		timestamp: new Date ().getTime ()
	};
	
	var	updatedTTL = false;
	
	for (var i = 0; i < inPacket.answers.length; i++)
	{
		var	answer = inPacket.answers [i];
		
		if (answer.type == 12)
		{
			// PTR record
			
			// help out a little here by separating the name from the type
			// the packet combines, but IMHO it's more useful separate
			// remember the stray delimiter between the name & type
			service.name = answer.ptr.replace ("." + inType, "");
			service.ttl = answer.ttl;
			
			updatedTTL = true;
		}
		else if (answer.type == 16)
		{
			// TXT record
			service.text = answer.txt;

			// don't override the PTR record's TTL
			if (!updatedTTL)
			{
				service.ttl = answer.ttl;
			}
		}
	}
	
	if ((service.ttl * 1000) < this.pollingPeriod)
	{
		service.ttl = this.defaultTTL;
	}
	
	// now trawl the additionals for extra bits
	for (var i = 0; i < inPacket.additionals.length; i++)
	{
		var	additional = inPacket.additionals [i];
		
		if (additional.type == 1)
		{
			// A record
			service.host = additional.a;
		}
		else if (additional.type == 16)
		{
			// TXT record
			
			// but if we got a TXT in the answer, don't override it here
			if (service.text == null || service.text.length == 0)
			{
				service.text = additional.txt;
			}
		}
		else if (additional.type == 33)
		{
			// SRV record
			service.port = additional.port;
		}
	}
	
	// rig up a unique key
	service.key = service.name  + "." + service.type + ":" + service.host + ":" + service.port;
	
	return service;
}

sensible.MDNS.prototype.makeARecord = function (inService)
{
	// add an A additional for the host
	var	hostElements = inService.host.split (".");
	var	aBuffer = new ArrayBuffer (4);
	var	aView = new Uint8Array (aBuffer);
	
	for (var j = 0; j < 4; j++)
	{
		var	digit = parseInt (hostElements [j]);
		aView [j] = digit;
	}

	var	aRecord = 
	{
		name: inService.name,
		type: 1,
		clas: 1,
		ttl: this.defaultTTL,
		rdata: aView
	};

	return aRecord;
}

sensible.MDNS.prototype.makeHostNSECRecord = function ()
{
	// calculate the size of the buffer
	var	hostName = this.strategy.getHostName ();
	var	length = 1 + hostName.length + 1;
	
	// 1 for the window block number (0)
	// 1 for the block length (4 for a host NSEC record)
	// 4 bytes of bitmap shit
	length += 6;

	var	buffer = new ArrayBuffer (length);
	var	view = new Uint8Array (buffer);
	
	var	offset = 0;
	
	var	nameElements = hostName.split (".");
	
	for (var i = 0; i < nameElements.length; i++)
	{
		var	nameElement = nameElements [i];
		
		view [offset++] = nameElement.length;
		
		for (var j = 0; j < nameElement.length; j++)
		{
			view [offset++] = nameElement.charCodeAt (j);
		}
	}
	
	// terminating the name
	view [offset++] = 0;
	
	// window block 0 & length
	view [offset++] = 0;
	view [offset++] = 4;
	
	// bitmap for RR types 1 and 28
	view [offset++] = 0x40;
	view [offset++] = 0;
	view [offset++] = 0;
	view [offset++] = 0x08;
	
	if (offset != length)
	{
		console.log ("makeHostNSECRecord() offset length error");
		console.log ("offset is " + offset + ", but length is " + length);
	}
	
	var	record = 
	{
		name: this.strategy.getHostName (),
		type: 47,
		clas: 1,
		ttl: this.defaultTTL,
		rdata: view
	}
	
	return record;
}

// HACK
sensible.MDNS.prototype.makeServiceNSECRecord = function (inService)
{
	// calculate the size of the buffer
	var	fullServiceName = inService.name + "." + inService.type.toLowerCase ();
	var	length = 1 + fullServiceName.length + 1;
	
	// 1 for the window block number (0)
	// 1 for the block length (4 for a host NSEC record)
	// 5 bytes of bitmap shit
	length += 7;

	var	buffer = new ArrayBuffer (length);
	var	view = new Uint8Array (buffer);
	
	var	offset = 0;
	
	var	nameElements = fullServiceName.split (".");
	
	for (var i = 0; i < nameElements.length; i++)
	{
		var	nameElement = nameElements [i];
		
		view [offset++] = nameElement.length;
		
		for (var j = 0; j < nameElement.length; j++)
		{
			view [offset++] = nameElement.charCodeAt (j);
		}
	}
	
	// terminating the name
	view [offset++] = 0;
	
	// window block 0 & length
	view [offset++] = 0;
	view [offset++] = 5;
	
	// bitmap for RR types 16 and 33
	view [offset++] = 0;
	view [offset++] = 0;
	view [offset++] = 0x80;
	view [offset++] = 0;
	view [offset++] = 0x40;
	
	if (offset != length)
	{
		console.log ("makeServiceNSECRecord() offset length error");
		console.log ("offset is " + offset + ", but length is " + length);
	}
	
	var	record = 
	{
		name: inService.name + "." + inService.type.toLowerCase (),
		type: 47,
		clas: 1,
		ttl: 120,
		rdata: view
	}
	
	return record;
}

sensible.MDNS.prototype.makePTRRecord = function (inService)
{
	var	fullServiceName = inService.name + "." + inService.type.toLowerCase ();

	var	ptrBuffer = new ArrayBuffer (1 + fullServiceName.length + 1);
	var	ptrView = new Uint8Array (ptrBuffer);
	
	var	offset = 0;
	
	// god i fucking hate this format
	var	nameElements = fullServiceName.split (".");
	
	for (var i = 0; i < nameElements.length; i++)
	{
		var	nameElement = nameElements [i];
		
		ptrView [offset++] = nameElement.length;
		
		for (var j = 0; j < nameElement.length; j++)
		{
			ptrView [offset++] = nameElement.charCodeAt (j);
		}
	}

	ptrView [offset++] = 0;

	var	ptrRecord =
	{
		name: inService.type.toLowerCase (),
		type: 12,
		clas: 1,
		ttl: inService.ttl,
		rdata: ptrView
	};
	
	return ptrRecord;
}

// ok, this rogered things for a while
// the NAME of the SRV record should be the full service name
// the RDATA should be the port etc and the HOST NAME, not the service name
sensible.MDNS.prototype.makeSRVRecord = function (inService)
{
	// denormalise the server name, fuck the compression
	var	serverName = this.strategy.getHostName ();

	// add an SRV additional for the port (sigh)
	// priority, weight, port, length byte, service name + .local + trailing zero
	var	srvSize = 6 + 1 + serverName.length + 1;
	var	srvBuffer = new ArrayBuffer (srvSize);
	var	srvView = new Uint8Array (srvBuffer);
	
	// priority & weight are zero
	var	offset = 0;
	srvView [offset++] = 0;
	srvView [offset++] = 0;
	srvView [offset++] = 0;
	srvView [offset++] = 0;
	
	// port
	srvView [offset++] = inService.port >> 8;
	srvView [offset++] = inService.port & 0xff;
	
	// god i fucking hate this format
	var	nameElements = serverName.split (".");
	
	for (var i = 0; i < nameElements.length; i++)
	{
		var	nameElement = nameElements [i];
		
		srvView [offset++] = nameElement.length;
		
		for (var j = 0; j < nameElement.length; j++)
		{
			srvView [offset++] = nameElement.charCodeAt (j);
		}
	}

	srvView [offset++] = 0;

	var	srvRecord = 
	{
		name: inService.name + "." + inService.type.toLowerCase (),
		type: 33,
		clas: 1,
		ttl: 120,
		rdata: srvView
	};

	return srvRecord;
}

sensible.MDNS.prototype.makeTXTRecord = function (inService)
{
	if (inService.text == null)
	{
		// this happens all the time
		inService.text = "";
	}
	
	var	txtBuffer = new ArrayBuffer (1 + inService.text.length);
	var	txtView = new Uint8Array (txtBuffer);
	
	var	offset = 0;
	
	txtView [offset++] = inService.text.length;
	
	// this is TXT, so we can not do the fucked format
	for (var i = 0; i < inService.text.length; i++)
	{
		txtView [offset++] = inService.text.charCodeAt (i);
	}

	var	txt =
	{
		name: inService.name + "." + inService.type.toLowerCase (),
		type: 16,
		clas: 1,
		ttl: inService.ttl,
		rdata: txtView
	};
	
	return txt;
}

sensible.MDNS.prototype.logPacket = function (inPacket)
{
	console.log ("LOG PACKET");
	console.log ("id = " + inPacket.id);
	console.log ("flags = " + inPacket.flags.toString (16));
	console.log ("qucount = " + inPacket.questions.length);
	console.log ("ancount = " + inPacket.answers.length);
	console.log ("aucount = " + inPacket.authorities.length);
	console.log ("adcount = " + inPacket.additionals.length);

	for (var i = 0; i < inPacket.questions.length; i++)
	{
		var	question = inPacket.questions [i];
		
		console.log ("question " + i);
		console.log ("name = " + question.name);
	}

	for (var i = 0; i < inPacket.answers.length; i++)
	{
		var	answer = inPacket.answers [i];
		
		console.log ("ANSWER " + i);
		console.log ("name = " + answer.name);
		console.log ("type = " + answer.type);
		console.log ("class = " + answer.clas);
		console.log ("ttl = " + answer.ttl);

		if (answer.type == 1)
		{
			console.log ("address = " + answer.a);
		}
		else
		if (answer.type == 12)
		{
			console.log ("ptr = " + answer.ptr);
		}
		else
		if (answer.type == 16)
		{
			console.log ("text = " + answer.txt);
		}
		else
		if (answer.type == 28)
		{
			console.log ("aaaa = " + answer.aaaa);
		}
		else
		if (answer.type == 33)
		{
			console.log ("port = " + answer.port);
		}
		else
		{
			console.log ("unknown answer type " + answer.type);
		}
	}
	
	for (var i = 0; i < inPacket.additionals.length; i++)
	{
		var	additional = inPacket.additionals [i];
		
		console.log ("ADDITIONAL " + i);
		console.log ("name = " + additional.name);
		console.log ("type = " + additional.type);
		console.log ("ttl = " + additional.ttl);

		if (additional.type == 1)
		{
			console.log ("address = " + additional.a);
		}
		else
		if (additional.type == 12)
		{
			console.log ("ptr = " + additional.ptr);
		}
		else
		if (additional.type == 16)
		{
			console.log ("text = " + additional.txt);
		}
		else
		if (additional.type == 28)
		{
			console.log ("aaaa = " + additional.aaaa);
		}
		else
		if (additional.type == 33)
		{
			console.log ("port = " + additional.port);
		}
		else
		if (additional.type == 47)
		{
			if (false)
			{
				console.log ("nsec hack name = " + additional.name);
			}
			else
			{
				console.log ("nsec name = " + additional.nsec.name);
				
				for (var j = 0; j < additional.nsec.windows.length; j++)
				{
					var	window = additional.nsec.windows [j];
	
					console.log ("nsec window " + window.number);
					console.log ("nsec types " + window.types);
				}
			}
		}
		else
		{
			console.log ("unknown answer type " + additional.type);
		}
	}
}

sensible.MDNS.prototype.sendARequest = function (inName)
{
	// console.log ("sending question for " + inType);
	
	var	packet = new sensible.DNSPacket ();
	packet.id = 0;
	packet.flags = 0;
	
	var	question =
	{
		name: inName,
		type: 1,
		clas: 1
	}
	
	packet.questions.push (question);

	var	buffer = packet.serialise ();
	this.strategy.send (buffer, "224.0.0.251", 5353);
}

sensible.MDNS.prototype.sendAResponse = function (inName, inHost)
{
	var	packet = new sensible.DNSPacket ();
	packet.id = 0;
	packet.flags = 0x8400;
	
	var	service = null;
	
	if (inName)
	{
		service =
		{
			name: inName,
			host: inHost
		};
	}
	else
	{
		service = this.localService;
	}
	
	packet.answers.push (this.makeARecord (service));

	// console.log ("sending A response for " + service.name + " (" + service.host + ")");
	
	var	buffer = packet.serialise ();
	this.strategy.send (buffer, "224.0.0.251", 5353);
}

sensible.MDNS.prototype.sendPTRRequest = function (inType)
{
	console.log ("sending question for " + inType);
	
	var	packet = new sensible.DNSPacket ();
	packet.id = 0;
	packet.flags = 0;
	
	var	question =
	{
		name: inType,
		type: 12,
		clas: 1
	}
	
	packet.questions.push (question);

	var	buffer = packet.serialise ();
	this.strategy.send (buffer, "224.0.0.251", 5353);
}

sensible.MDNS.prototype.sendPTRResponse = function (inService)
{
	var	packet = new sensible.DNSPacket ();
	packet.id = 0;
	packet.flags = 0x8400;

	packet.answers.push (this.makePTRRecord (inService));
	packet.additionals.push (this.makeARecord (inService));
	packet.additionals.push (this.makeTXTRecord (inService));
	packet.additionals.push (this.makeSRVRecord (inService));
	// packet.additionals.push (this.makeHostNSECRecord (inService));
	// packet.additionals.push (this.makeServiceNSECRecord (inService));

	var	buffer = packet.serialise ();

	this.strategy.send (buffer, "224.0.0.251", 5353);
}

/**
 * Poll to refresh the type and host tables.
 * Simplistic, but it works.
 * The only issue is that the polling period has to be less than TTL.
 */

sensible.MDNS.prototype.startPolling = function ()
{
	var	self = this;
	
	this.poller = setInterval
	(
		function ()
		{
			for (var type in self.serviceResolutionsByType)
			{
				self.sendPTRRequest (type);
			}

			for (var name in self.addressResolutionsByType)
			{
				self.sendARequest (name);
			}
			
			self.expireHosts ();
			self.expireServices ();
		},
		this.pollingPeriod
	);
}

sensible.MDNS.prototype.stopPolling = function ()
{
	if (this.poller)
	{
		clearInterval (this.poller);
		this.poller = null;
	}
	else
	{
		console.log ("stopPolling() called with no poller task");
	}
}