fs = require 'fs'
url = require 'url'
http = require 'http'
https = require 'https'
querystring = require 'querystring'Require Node.js core modules.
fs = require 'fs'
url = require 'url'
http = require 'http'
https = require 'https'
querystring = require 'querystring'Keep the version number in sync with package.json.
VERSION = '0.1.6'Regular expression for HTTPS addresses
httpsPattern = new RegExp('^https://', 'i');The in-memory hit store is just a hash. We map unique identifiers to the
number of hits they receive here, and flush the store every interval
seconds.
store = {}Record hits from the remote pixel.
record = (key, count) ->
store[key] or= 0
store[key] += countSerializes the current store to JSON, and creates a fresh one. Add a
secret token to the request object, if configured.
serialize = ->
data = json: JSON.stringify(store)
data.secret = config.secret if config.secret
querystring.stringify dataReset the store.
reset = ->
oldStore = store
store = {}
oldStoreMerge the given store with the current one.
merge = (newStore) ->
for key, count of newStore
record key, count
nullFlushes the store to be saved by an external API. The contents of the store
are sent to the configured endpoint URL via HTTP/HTTPS POST. If no endpoint is
configured, this is a no-op.
flush = ->
log store
return unless config.endpoint
endpointProtocol = if httpsPattern.test(config.endpoint) then https else http
data = serialize()
oldStore = reset()
onError = (message) ->
merge(oldStore) unless config.discard
console.error message
endReqOpts['headers']['Content-Length'] = data.length
request = endpointProtocol.request endReqOpts, (res) ->
if 200 <= res.statusCode < 300
console.info '--- flushed ---'
else
onError "--- flush failed with code:" + res.statusCode
request.on 'error', (e) ->
onError "--- cannot connect to endpoint : #{e.message}"
request.write data
request.end()Log the contents of the store to stdout. Happens on every flush, so that
there’s a record of hits if something goes awry.
log = (hash) ->
for key, hits of hash
console.info "#{hits}:\t#{key}"
nullLoad the configuration and the contents of the tracking pixel. Handle requests for the version number, and usage information.
configPath = process.argv[2]
if configPath in ['-v', '-version', '--version']
console.log "Pixel Ping version #{VERSION}"
process.exit 0
if not configPath or (configPath in ['-h', '-help', '--help'])
console.error "Usage: pixel-ping path/to/config.json"
process.exit 0
config = JSON.parse fs.readFileSync(configPath).toString()
pixel = fs.readFileSync __dirname + '/pixel.gif'HTTP/HTTPS headers for the pixel image.
pixelHeaders =
'Cache-Control': 'private, no-cache, proxy-revalidate, max-age=0'
'Content-Type': 'image/gif'
'Content-Disposition': 'inline'
'Content-Length': pixel.lengthHTTP/HTTPS headers for the 404 response.
emptyHeaders =
'Content-Type': 'text/html'
'Content-Length': '0'If an endpoint has been configured, create an HTTP/HTTPS client connected to it,
and log a warning otherwise.
if config.endpoint
console.info "Flushing hits to #{config.endpoint}"
endParams = url.parse config.endpoint
endReqOpts =
host: endParams.hostname
method: 'POST'
path: endParams.pathname
headers:
'host': endParams.host
'Content-Type': 'application/x-www-form-urlencoded'
endReqOpts.port = endParams.port if endParams.port
else
console.warn "No endpoint set. Hits won't be flushed, add \"endpoint\" to #{configPath}."Sending SIGUSR2 to the Pixel Ping process will force a data flush.
process.on 'SIGUSR2', ->
console.log 'Got SIGUSR2. Forcing a flush:'
flush()Don’t let exceptions kill the server.
process.on 'uncaughtException', (err) ->
console.error "Uncaught Exception: #{err}"When a request comes in, ensure that it’s looking
for pixel.gif. If it is, serve the pixel and record a hit.
handleRequest = (req, res) ->
params = url.parse req.url, true
if params.pathname is '/pixel.gif'
res.writeHead 200, pixelHeaders
res.end pixel
if key = params.query?.key
record key, 1
else
res.writeHead 404, emptyHeaders
res.end ''
nullDetermines the right protocol (HTTP/HTTPS) to be used on the nodejs server
if config.sslkey && config.sslcert && config.sslca
protocolOptions = {
key : fs.readFileSync(config.sslkey),
cert : fs.readFileSync(config.sslcert),
ca : fs.readFileSync(config.sslca),
};
server = https.createServer(protocolOptions, handleRequest)
else if config.sslkey && config.sslcert
protocolOptions = {
key : fs.readFileSync(config.sslkey),
cert : fs.readFileSync(config.sslcert),
};
server = https.createServer(protocolOptions, handleRequest)
else
server = http.createServer(handleRequest)Start the server listening for pixel hits, and begin the periodic data flush.
server.listen config.port, config.host
setInterval flush, config.interval * 1000