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] += count
Serializes 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 data
Reset the store
.
reset = ->
oldStore = store
store = {}
oldStore
Merge the given store
with the current one.
merge = (newStore) ->
for key, count of newStore
record key, count
null
Flushes 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}"
null
Load 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.length
HTTP/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 ''
null
Determines 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