haraka-plugin-p0f
Advanced tools
Comparing version 1.0.2 to 1.0.3
@@ -0,6 +1,9 @@ | ||
## 1.0.3 - 2018-05-30 | ||
## 1.0.1 - 2017-09-11 | ||
- add_header option visible at config | ||
- when socket_path is not configured, emit an error | ||
## 1.0.2 - 2017-09-11 | ||
- when socket_path is not configured, emit an error | ||
## 1.0.1 - 2017-09-01 | ||
@@ -7,0 +10,0 @@ |
318
index.js
@@ -7,177 +7,168 @@ 'use strict'; | ||
function P0FClient (path) { | ||
const self = this; | ||
class P0FClient { | ||
constructor (path) { | ||
this.sock = null; | ||
this.send_queue = []; | ||
this.receive_queue = []; | ||
this.connected = false; | ||
this.ready = false; | ||
this.socket_has_error = false; | ||
this.restart_interval = false; | ||
this.sock = null; | ||
this.send_queue = []; | ||
this.receive_queue = []; | ||
this.connected = false; | ||
this.ready = false; | ||
this.socket_has_error = false; | ||
this.restart_interval = false; | ||
function connect () { | ||
self.sock = net.createConnection(path); | ||
self.sock.setTimeout(5 * 1000); | ||
this.connect(path); | ||
} | ||
self.sock.on('connect', function () { | ||
self.sock.setTimeout(30 * 1000); | ||
self.connected = true; | ||
self.socket_has_error = false; | ||
self.ready = true; | ||
if (self.restart_interval) clearInterval(self.restart_interval); | ||
self.process_send_queue(); | ||
}); | ||
connect (path) { | ||
this.sock = net.createConnection(path); | ||
this.sock.setTimeout(5 * 1000); | ||
self.sock.on('data', function (data) { | ||
this.sock.on('connect', () => { | ||
this.sock.setTimeout(30 * 1000); | ||
this.connected = true; | ||
this.socket_has_error = false; | ||
this.ready = true; | ||
if (this.restart_interval) clearInterval(this.restart_interval); | ||
this.process_send_queue(); | ||
}) | ||
this.sock.on('data', (data) => { | ||
for (let i=0; i<data.length/232; i++) { | ||
self.decode_response(data.slice(((i) ? 232*i : 0), 232*(i+1))); | ||
this.decode_response(data.slice(((i) ? 232*i : 0), 232*(i+1))); | ||
} | ||
}); | ||
}) | ||
self.sock.on('drain', function () { | ||
self.ready = true; | ||
self.process_send_queue(); | ||
}); | ||
this.sock.on('drain', () => { | ||
this.ready = true; | ||
this.process_send_queue(); | ||
}) | ||
self.sock.on('error', function (error) { | ||
self.connected = false; | ||
error.message = error.message + ' (socket: ' + path + ')'; | ||
self.socket_has_error = error; | ||
self.sock.destroy(); | ||
this.sock.on('error', (error) => { | ||
this.connected = false; | ||
error.message = `${error.message} (socket: ${path})`; | ||
this.socket_has_error = error; | ||
this.sock.destroy(); | ||
// Try and reconnect | ||
if (!self.restart_interval) { | ||
self.restart_interval = setInterval(function () { | ||
connect(); | ||
}, 5 * 1000); | ||
if (!this.restart_interval) { | ||
this.restart_interval = setInterval(() => { connect(); }, 5 * 1000); | ||
} | ||
// Clear the receive queue | ||
for (let i=0; i<self.receive_queue.length; i++) { | ||
const item = self.receive_queue.shift(); | ||
item.cb(self.socket_has_error); | ||
for (let i=0; i<this.receive_queue.length; i++) { | ||
const item = this.receive_queue.shift(); | ||
item.cb(this.socket_has_error); | ||
continue; | ||
} | ||
self.process_send_queue(); | ||
}); | ||
this.process_send_queue(); | ||
}) | ||
} | ||
connect(); | ||
} | ||
P0FClient.prototype.shutdown = function () { | ||
if (this.restart_interval) { | ||
clearInterval(this.restart_interval); | ||
shutdown () { | ||
if (this.restart_interval) { | ||
clearInterval(this.restart_interval); | ||
} | ||
} | ||
} | ||
P0FClient.prototype.decode_response = function (data) { | ||
decode_response (data) { | ||
function decode_string (data2, start, end) { | ||
let str = ''; | ||
for (let a=start; a<end; a++) { | ||
const b = data2.readUInt8(a); | ||
if (b === 0x0) break; | ||
str = str + String.fromCharCode(b); | ||
function decode_string (data2, start, end) { | ||
let str = ''; | ||
for (let a=start; a<end; a++) { | ||
const b = data2.readUInt8(a); | ||
if (b === 0x0) break; | ||
str = str + String.fromCharCode(b); | ||
} | ||
return str; | ||
} | ||
return str; | ||
} | ||
if (this.receive_queue.length <= 0) { | ||
throw new Error('unexpected data received'); | ||
} | ||
const item = this.receive_queue.shift(); | ||
if (this.receive_queue.length <= 0) { | ||
throw new Error('unexpected data received'); | ||
} | ||
const item = this.receive_queue.shift(); | ||
/////////////////// | ||
// Decode packet // | ||
/////////////////// | ||
/////////////////// | ||
// Decode packet // | ||
/////////////////// | ||
// Response magic dword (0x50304602), native endian. | ||
if (data.readUInt32LE(0) !== 0x50304602) { | ||
return item.cb(new Error('bad response magic!')); | ||
} | ||
// Response magic dword (0x50304602), native endian. | ||
if (data.readUInt32LE(0) !== 0x50304602) { | ||
return item.cb(new Error('bad response magic!')); | ||
} | ||
// Status dword: 0x00 for 'bad query', 0x10 for 'OK', and 0x20 for 'no match' | ||
const st = data.readUInt32LE(4); | ||
switch (st) { | ||
case (0x00): | ||
return item.cb(new Error('bad query')); | ||
case (0x10): { | ||
const p0f = { | ||
query: item.ip, | ||
first_seen: data.readUInt32LE(8), | ||
last_seen: data.readUInt32LE(12), | ||
total_conn: data.readUInt32LE(16), | ||
uptime_min: data.readUInt32LE(20), | ||
up_mod_days: data.readUInt32LE(24), | ||
last_nat: data.readUInt32LE(28), | ||
last_chg: data.readUInt32LE(32), | ||
distance: data.readInt16LE(36), | ||
bad_sw: data.readUInt8(38), | ||
os_match_q: data.readUInt8(39), | ||
os_name: decode_string(data, 40, 72), | ||
os_flavor: decode_string(data, 72, 104), | ||
http_name: decode_string(data, 104, 136), | ||
http_flavor: decode_string(data, 136, 168), | ||
link_type: decode_string(data, 168, 200), | ||
language: decode_string(data, 200, 232), | ||
// Status dword: 0x00 for 'bad query', 0x10 for 'OK', and 0x20 for 'no match' | ||
const st = data.readUInt32LE(4); | ||
switch (st) { | ||
case (0x00): | ||
return item.cb(new Error('bad query')); | ||
case (0x10): { | ||
const p0f = { | ||
query: item.ip, | ||
first_seen: data.readUInt32LE(8), | ||
last_seen: data.readUInt32LE(12), | ||
total_conn: data.readUInt32LE(16), | ||
uptime_min: data.readUInt32LE(20), | ||
up_mod_days: data.readUInt32LE(24), | ||
last_nat: data.readUInt32LE(28), | ||
last_chg: data.readUInt32LE(32), | ||
distance: data.readInt16LE(36), | ||
bad_sw: data.readUInt8(38), | ||
os_match_q: data.readUInt8(39), | ||
os_name: decode_string(data, 40, 72), | ||
os_flavor: decode_string(data, 72, 104), | ||
http_name: decode_string(data, 104, 136), | ||
http_flavor: decode_string(data, 136, 168), | ||
link_type: decode_string(data, 168, 200), | ||
language: decode_string(data, 200, 232), | ||
} | ||
return item.cb(null, p0f); | ||
} | ||
return item.cb(null, p0f); | ||
case (0x20): | ||
return item.cb(null, null); | ||
default: | ||
throw new Error(`unknown status: ${st}`); | ||
} | ||
case (0x20): | ||
return item.cb(null, null); | ||
default: | ||
throw new Error('unknown status: ' + st); | ||
} | ||
}; | ||
P0FClient.prototype.query = function (ip, cb) { | ||
if (this.socket_has_error) { | ||
return cb(this.socket_has_error); | ||
query (ip, cb) { | ||
if (this.socket_has_error) { | ||
return cb(this.socket_has_error); | ||
} | ||
if (!this.connected) { | ||
return cb(new Error('socket not connected')); | ||
} | ||
const addr = ipaddr.parse(ip); | ||
const bytes = addr.toByteArray(); | ||
const buf = new Buffer(21); | ||
buf.writeUInt32LE(0x50304601, 0); // query magic | ||
buf.writeUInt8(((addr.kind() === 'ipv6') ? 0x6 : 0x4), 4); | ||
for (let i=0; i < bytes.length; i++) { | ||
buf.writeUInt8(bytes[i], 5 + i); | ||
} | ||
if (!this.ready) { | ||
this.send_queue.push({ip: ip, cb: cb, buf: buf}); | ||
} | ||
else { | ||
this.receive_queue.push({ip: ip, cb: cb}); | ||
if (!this.sock.write(buf)) this.ready = false; | ||
} | ||
} | ||
if (!this.connected) { | ||
return cb(new Error('socket not connected')); | ||
} | ||
const addr = ipaddr.parse(ip); | ||
const bytes = addr.toByteArray(); | ||
const buf = new Buffer(21); | ||
buf.writeUInt32LE(0x50304601, 0); // query magic | ||
buf.writeUInt8(((addr.kind() === 'ipv6') ? 0x6 : 0x4), 4); | ||
for (let i=0; i < bytes.length; i++) { | ||
buf.writeUInt8(bytes[i], 5 + i); | ||
} | ||
if (!this.ready) { | ||
this.send_queue.push({ip: ip, cb: cb, buf: buf}); | ||
} | ||
else { | ||
this.receive_queue.push({ip: ip, cb: cb}); | ||
if (!this.sock.write(buf)) this.ready = false; | ||
} | ||
}; | ||
P0FClient.prototype.process_send_queue = function () { | ||
if (this.send_queue.length === 0) { return; } | ||
process_send_queue () { | ||
if (this.send_queue.length === 0) { return; } | ||
for (let i=0; i<this.send_queue.length; i++) { | ||
let item; | ||
if (this.socket_has_error) { | ||
for (let i=0; i<this.send_queue.length; i++) { | ||
let item; | ||
if (this.socket_has_error) { | ||
item = this.send_queue.shift(); | ||
item.cb(this.socket_has_error); | ||
continue; | ||
} | ||
if (!this.ready) break; | ||
item = this.send_queue.shift(); | ||
item.cb(this.socket_has_error); | ||
continue; | ||
this.receive_queue.push({ip: item.ip, cb: item.cb}); | ||
if (!this.sock.write(item.buf)) { | ||
this.ready = false; | ||
} | ||
} | ||
if (!this.ready) break; | ||
item = this.send_queue.shift(); | ||
this.receive_queue.push({ip: item.ip, cb: item.cb}); | ||
if (!this.sock.write(item.buf)) { | ||
this.ready = false; | ||
} | ||
} | ||
}; | ||
function start_p0f_client (sp, server, next) { | ||
if (!sp) { | ||
server.logerror("main.socket_path not defined in p0f.ini!"); | ||
return next(); | ||
} | ||
// Start p0f process | ||
server.notes.p0f_client = new P0FClient(sp); | ||
next(); | ||
} | ||
@@ -189,4 +180,10 @@ | ||
this.load_p0f_ini(); | ||
}; | ||
this.register_hook('init_master', 'start_p0f_client') | ||
this.register_hook('init_child', 'start_p0f_client') | ||
this.register_hook('lookup_rdns', 'query_p0f') | ||
this.register_hook('data_post', 'add_p0f_header') | ||
} | ||
exports.load_p0f_ini = function () { | ||
@@ -199,11 +196,14 @@ const plugin = this; | ||
exports.hook_init_master = function (next, server) { | ||
start_p0f_client(this.cfg.main.socket_path, server, next); | ||
exports.start_p0f_client = function (next, server) { | ||
if (!this.cfg.main.socket_path) { | ||
server.logerror("main.socket_path not defined in p0f.ini!"); | ||
return next(); | ||
} | ||
// Start p0f process | ||
server.notes.p0f_client = new P0FClient(this.cfg.main.socket_path); | ||
next(); | ||
} | ||
exports.hook_init_child = function (next, server) { | ||
start_p0f_client(this.cfg.main.socket_path, server, next); | ||
}; | ||
exports.hook_lookup_rdns = function onLookup (next, connection) { | ||
exports.query_p0f = function onLookup (next, connection) { | ||
const plugin = this; | ||
@@ -213,3 +213,3 @@ if (connection.remote.is_private) return next(); | ||
if (!connection.server.notes.p0f_client) { | ||
connection.logerror(plugin, 'missing server'); | ||
connection.logerror(plugin, 'missing p0f client'); | ||
return next(); | ||
@@ -232,16 +232,16 @@ } | ||
next(); | ||
}); | ||
}; | ||
}) | ||
} | ||
function format_results (r) { | ||
const data = []; | ||
if (r.os_name) data.push('os="' + r.os_name + ' ' + r.os_flavor + '"'); | ||
if (r.link_type) data.push('link_type="' + r.link_type + '"'); | ||
if (r.distance) data.push('distance=' + r.distance); | ||
if (r.total_conn) data.push('total_conn=' + r.total_conn); | ||
if (r.last_nat) data.push('shared_ip=' + ((r.last_nat === 0) ? 'N' : 'Y')); | ||
if (r.os_name) data.push(`os="${r.os_name} ${r.os_flavor}"`); | ||
if (r.link_type) data.push(`link_type="${r.link_type}"`); | ||
if (r.distance) data.push(`distance=${r.distance}`); | ||
if (r.total_conn) data.push(`total_conn=${r.total_conn}`); | ||
if (r.last_nat) data.push(`shared_ip=${((r.last_nat === 0) ? 'N' : 'Y')}`); | ||
return data.join(' '); | ||
} | ||
exports.hook_data_post = function (next, connection) { | ||
exports.add_p0f_header = function (next, connection) { | ||
const plugin = this; | ||
@@ -248,0 +248,0 @@ if (connection.remote.is_private) return next(); |
{ | ||
"name": "haraka-plugin-p0f", | ||
"version": "1.0.2", | ||
"version": "1.0.3", | ||
"description": "Haraka plugin that adds TCP fingerprinting", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -33,3 +33,3 @@ | ||
describe('lookup_rdns', () => { | ||
it.skip('retrieves TCP fingerprint data from p0f server', function (done) { | ||
it.skip('retrieves TCP fingerprint data from p0f server', (done) => { | ||
done() | ||
@@ -53,3 +53,3 @@ }) | ||
this.plugin.hook_data_post((code, value) => { | ||
this.plugin.add_p0f_header((code, value) => { | ||
assert.ok(Object.keys(this.connection.transaction.header.headers)) | ||
@@ -63,3 +63,3 @@ done() | ||
this.connection.remote.is_private=true; | ||
this.plugin.hook_data_post((code, value) => { | ||
this.plugin.add_p0f_header((code, value) => { | ||
assert.equal(code, undefined) | ||
@@ -66,0 +66,0 @@ assert.equal(value, undefined) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
18001
295