UNPKG

htcp-purge

Version:
143 lines (130 loc) 4.24 kB
'use strict'; const P = require( 'bluebird' ); const dgram = P.promisifyAll( require( 'dgram' ) ); /** * Creates a new cache purger instance * * @param {Object} options object containing options for a cache purger: * - log: logging function (default no-op) * - routes: array of route objects to map a resource url to the cache endpoint * - rule: ether regex for a resource url, or 'undefined' if it's a default endpoint * - host: cache endpoint host * - port: cache endpount port * - multicast_ttl: standard UDP multicast TTL option (default 8) * @constructor */ class HTCPPurger { constructor( options ) { this.options = options || {}; this.log = this.options.log || ( () => {} ); if ( !this.options.routes ) { throw new Error( 'Config error. At least one route must be specified' ); } this.options.routes.forEach( ( routeSpec ) => { if ( routeSpec.rule && /^\/.+\/$/.test( routeSpec.rule ) ) { const regExp = new RegExp( routeSpec.rule .substring( 1, routeSpec.rule.length - 1 ) ); routeSpec.rule = ( url ) => regExp.test( url ); } else { routeSpec.rule = () => true; } } ); this.options.multicast_ttl = this.options.multicast_ttl || 8; this.seqReqId = 1; this.socket = dgram.createSocket( 'udp4' ); } bind() { return this.socket.bindAsync( { exclusive: true } ) .then( () => { this.socket.setMulticastLoopback( false ); this.socket.setMulticastTTL( this.options.multicast_ttl ); } ); } /** * Purge a list of resources cahced under provided URLs * @param {Array} urls array of urls to purge * @return {Promise} */ purge( urls ) { return P.all( urls.map( ( url ) => { const datagram = this._constructHTCPRequest( url ); const route = this._lookupRoute( url ); if ( route ) { return this.socket .sendAsync( datagram, 0, datagram.length, route.port, route.host ); } else { return P.resolve(); } } ) ); } close() { try { return this.socket.close(); } catch ( e ) { // We've tried, but seems like socket is already closed, so swallow the error. } } /** * Construct a UDP datagram with HTCP packet for Varnish flush of the url * @param {string} url a url of the resource that should be flushed * @return {Buffer} resulting HTCP packet bytes * @private */ _constructHTCPRequest( url ) { const urlByteLen = Buffer.byteLength( url ); const htcpSpecifierLen = 2 + 4 + 2 + urlByteLen + 2 + 8 + 2; const htcpDataLen = 8 + 2 + htcpSpecifierLen; const htcpLen = 4 + htcpDataLen + 2; const result = Buffer.alloc( htcpLen ); // Length result.writeUInt16BE( htcpLen, 0 ); // Major-minor version result.writeUInt16BE( 0, 2 ); // Data length result.writeUInt16BE( htcpDataLen, 4 ); // Op code & response result.writeUInt8( 4, 6 ); // Reserved & flags result.writeUInt8( 0, 7 ); // Transaction Id - seq number of a request this.seqReqId &= 0xFFFFFFFF; result.writeUInt32BE( this.seqReqId++, 8 ); // HTCP packet contents - CLR specifier // Reserved & reason result.writeUInt16BE( 0, 12 ); // COUNTSTR method: length + method (HEAD & GET are equivalent) result.writeUInt16BE( 4, 14 ); result.write( 'HEAD', 16, 4 ); // COUNTSTR uri: length + URI result.writeUInt16BE( urlByteLen, 20 ); result.write( url, 22, urlByteLen ); // COUNTSTR version: length + http version result.writeUInt16BE( 8, 22 + urlByteLen ); result.write( 'HTTP/1.0', 24 + urlByteLen, 8 ); // COUNTSTR headers: empty, use just as padding result.writeUInt16BE( 0, 32 + urlByteLen ); result.writeUInt16BE( 2, 14 + htcpSpecifierLen ); return result; } /** * Lookup a cache endpoint for a concrete URL, based on options * supplied in constructor * @param {string} url URL to lookup cache endpoint for * @return {Object} an opbject with host and port keys * @private */ _lookupRoute( url ) { const route = this.options.routes.find( ( route ) => route.rule( url ) ); if ( !route ) { this.log( 'error/htcp-purge', { msg: `Could not find route for ${url}` } ); return undefined; } return { host: route.host, port: route.port }; } } module.exports = HTCPPurger;