haraka-plugin-p0f
Advanced tools
Comparing version 1.0.9 to 1.0.10
419
index.js
@@ -1,262 +0,265 @@ | ||
'use strict'; | ||
'use strict' | ||
// p0f v3 client - http://lcamtuf.coredump.cx/p0f3/ | ||
const net = require('net'); | ||
const ipaddr = require('ipaddr.js'); | ||
const net = require('net') | ||
const ipaddr = require('ipaddr.js') | ||
class P0FClient { | ||
constructor (path) { | ||
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; | ||
this.connect(path) | ||
} | ||
this.connect(path); | ||
} | ||
connect(path) { | ||
this.sock = net.createConnection(path) | ||
this.sock.setTimeout(5 * 1000) | ||
connect (path) { | ||
this.sock = net.createConnection(path); | ||
this.sock.setTimeout(5 * 1000); | ||
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('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++) { | ||
this.decode_response(data.slice(i ? 232 * i : 0, 232 * (i + 1))) | ||
} | ||
}) | ||
this.sock.on('data', (data) => { | ||
for (let i=0; i<data.length/232; i++) { | ||
this.decode_response(data.slice(((i) ? 232*i : 0), 232*(i+1))); | ||
} | ||
}) | ||
this.sock.on('drain', () => { | ||
this.ready = true | ||
this.process_send_queue() | ||
}) | ||
this.sock.on('drain', () => { | ||
this.ready = true; | ||
this.process_send_queue(); | ||
}) | ||
this.sock.on('error', (error) => { | ||
this.connected = false | ||
error.message = `${error.message} (socket: ${path})` | ||
this.socket_has_error = error | ||
this.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 (!this.restart_interval) { | ||
this.restart_interval = setInterval(() => { | ||
this.connect(path) | ||
}, 5 * 1000) | ||
} | ||
// Clear the receive queue | ||
for (let i = 0; i < this.receive_queue.length; i++) { | ||
const item = this.receive_queue.shift() | ||
item.cb(this.socket_has_error) | ||
continue | ||
} | ||
this.process_send_queue() | ||
}) | ||
} | ||
// Try and reconnect | ||
if (!this.restart_interval) { | ||
this.restart_interval = setInterval(() => { this.connect(path); }, 5 * 1000); | ||
} | ||
// Clear the receive queue | ||
for (let i=0; i<this.receive_queue.length; i++) { | ||
const item = this.receive_queue.shift(); | ||
item.cb(this.socket_has_error); | ||
continue; | ||
} | ||
this.process_send_queue(); | ||
}) | ||
shutdown() { | ||
if (this.restart_interval) { | ||
clearInterval(this.restart_interval) | ||
} | ||
} | ||
shutdown () { | ||
if (this.restart_interval) { | ||
clearInterval(this.restart_interval); | ||
} | ||
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) | ||
} | ||
return str | ||
} | ||
decode_response (data) { | ||
if (this.receive_queue.length <= 0) { | ||
throw new Error('unexpected data received') | ||
} | ||
const item = this.receive_queue.shift() | ||
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; | ||
} | ||
/////////////////// | ||
// Decode packet // | ||
/////////////////// | ||
if (this.receive_queue.length <= 0) { | ||
throw new Error('unexpected data received'); | ||
} | ||
const item = this.receive_queue.shift(); | ||
// Response magic dword (0x50304602), native endian. | ||
if (data.readUInt32LE(0) !== 0x50304602) { | ||
return item.cb(new Error('bad response magic!')) | ||
} | ||
/////////////////// | ||
// Decode packet // | ||
/////////////////// | ||
// 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), | ||
} | ||
return item.cb(null, p0f) | ||
} | ||
case 0x20: | ||
return item.cb(null, null) | ||
default: | ||
throw new Error(`unknown status: ${st}`) | ||
} | ||
} | ||
// 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); | ||
} | ||
case (0x20): | ||
return item.cb(null, null); | ||
default: | ||
throw new Error(`unknown status: ${st}`); | ||
} | ||
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, cb, buf }) | ||
} else { | ||
this.receive_queue.push({ ip, cb }) | ||
if (!this.sock.write(buf)) this.ready = false | ||
} | ||
} | ||
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, cb, buf}); | ||
} | ||
else { | ||
this.receive_queue.push({ip, cb}); | ||
if (!this.sock.write(buf)) this.ready = false; | ||
} | ||
process_send_queue() { | ||
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) { | ||
item = this.send_queue.shift(); | ||
item.cb(this.socket_has_error); | ||
continue; | ||
} | ||
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; | ||
} | ||
} | ||
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() | ||
this.receive_queue.push({ ip: item.ip, cb: item.cb }) | ||
if (!this.sock.write(item.buf)) { | ||
this.ready = false | ||
} | ||
} | ||
} | ||
} | ||
exports.P0FClient = P0FClient; | ||
exports.P0FClient = P0FClient | ||
exports.register = function () { | ||
this.load_p0f_ini(); | ||
this.load_p0f_ini() | ||
this.register_hook('init_master', 'start_p0f_client') | ||
this.register_hook('init_child', 'start_p0f_client') | ||
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') | ||
this.register_hook('lookup_rdns', 'query_p0f') | ||
this.register_hook('data_post', 'add_p0f_header') | ||
} | ||
exports.load_p0f_ini = function () { | ||
const plugin = this; | ||
plugin.cfg = plugin.config.get('p0f.ini', function () { | ||
plugin.load_p0f_ini(); | ||
}) | ||
const plugin = this | ||
plugin.cfg = plugin.config.get('p0f.ini', function () { | ||
plugin.load_p0f_ini() | ||
}) | ||
} | ||
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(); | ||
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.query_p0f = function onLookup (next, connection) { | ||
const plugin = this; | ||
if (connection.remote.is_private) return next(); | ||
exports.query_p0f = function onLookup(next, connection) { | ||
const plugin = this | ||
if (connection.remote.is_private) return next() | ||
if (!connection.server.notes.p0f_client) { | ||
connection.logerror(plugin, 'missing p0f client'); | ||
return next(); | ||
} | ||
if (!connection.server.notes.p0f_client) { | ||
connection.logerror(plugin, 'missing p0f client') | ||
return next() | ||
} | ||
connection.server.notes.p0f_client.query(connection.remote.ip, (err, result) => { | ||
if (err) { | ||
connection.results.add(plugin, {err: err.message}); | ||
return next(); | ||
} | ||
connection.server.notes.p0f_client.query( | ||
connection.remote.ip, | ||
(err, result) => { | ||
if (err) { | ||
connection.results.add(plugin, { err: err.message }) | ||
return next() | ||
} | ||
if (!result) { | ||
connection.results.add(plugin, {err: 'no p0f results'}); | ||
return next(); | ||
} | ||
if (!result) { | ||
connection.results.add(plugin, { err: 'no p0f results' }) | ||
return next() | ||
} | ||
connection.loginfo(plugin, format_results(result)); | ||
connection.results.add(plugin, result); | ||
next(); | ||
}) | ||
connection.loginfo(plugin, format_results(result)) | ||
connection.results.add(plugin, result) | ||
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')}`); | ||
return data.join(' '); | ||
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'}`) | ||
return data.join(' ') | ||
} | ||
exports.add_p0f_header = function (next, connection) { | ||
const plugin = this; | ||
if (connection.remote.is_private) return next(); | ||
const plugin = this | ||
if (connection.remote.is_private) return next() | ||
const header_name = plugin.cfg.main.add_header; | ||
if (!header_name) { | ||
connection.logdebug(plugin, 'header disabled in ini' ); | ||
return next(); | ||
} | ||
const header_name = plugin.cfg.main.add_header | ||
if (!header_name) { | ||
connection.logdebug(plugin, 'header disabled in ini') | ||
return next() | ||
} | ||
connection.transaction.remove_header(header_name); | ||
const result = connection.results.get('p0f'); | ||
if (!result || !result.os_name) { | ||
connection.results.add(plugin, {err: 'no p0f note'}); | ||
return next(); | ||
} | ||
connection.transaction.remove_header(header_name) | ||
const result = connection.results.get('p0f') | ||
if (!result || !result.os_name) { | ||
connection.results.add(plugin, { err: 'no p0f note' }) | ||
return next() | ||
} | ||
connection.logdebug(plugin, 'adding header'); | ||
connection.transaction.add_header(header_name, format_results(result)); | ||
connection.logdebug(plugin, 'adding header') | ||
connection.transaction.add_header(header_name, format_results(result)) | ||
next(); | ||
next() | ||
} |
{ | ||
"name": "haraka-plugin-p0f", | ||
"version": "1.0.9", | ||
"version": "1.0.10", | ||
"description": "Haraka plugin that adds TCP fingerprinting", | ||
"files": [ | ||
"config", | ||
"contrib", | ||
"CHANGELOG.md" | ||
], | ||
"main": "index.js", | ||
"scripts": { | ||
"cover": "NODE_ENV=cov npx nyc --reporter=lcovonly npm run test", | ||
"format": "npm run prettier:fix && npm run lint:fix", | ||
"lint": "npx eslint *.js test", | ||
"lintfix": "npx eslint --fix *.js test", | ||
"test": "npx mocha" | ||
"lint:fix": "npx eslint --fix *.js test", | ||
"prettier": "npx prettier . --check", | ||
"prettier:fix": "npx prettier . --write --log-level=warn", | ||
"test": "npx mocha", | ||
"versions": "npx dependency-version-checker check", | ||
"versions:fix": "npx dependency-version-checker update" | ||
}, | ||
@@ -28,10 +38,9 @@ "repository": { | ||
"devDependencies": { | ||
"eslint": ">=8", | ||
"eslint-plugin-haraka": "*", | ||
"haraka-test-fixtures": "*", | ||
"mocha": ">=9" | ||
"@haraka/eslint-config": "^2.0.2", | ||
"haraka-test-fixtures": "^1.3.8", | ||
"mocha": "^11.1.0" | ||
}, | ||
"dependencies": { | ||
"ipaddr.js": "^2.0.1" | ||
"ipaddr.js": "^2.2.0" | ||
} | ||
} |
[![Build Status][ci-img]][ci-url] | ||
[![Code Climate][clim-img]][clim-url] | ||
[![NPM][npm-img]][npm-url] | ||
@@ -15,12 +14,12 @@ # haraka-plugin-p0f | ||
genre => FreeBSD | ||
detail => 8.x (1) | ||
uptime => 1390 | ||
link => ethernet/modem | ||
distance => 17 | ||
genre => FreeBSD | ||
detail => 8.x (1) | ||
uptime => 1390 | ||
link => ethernet/modem | ||
distance => 17 | ||
Which was parsed from this p0f fingerprint: | ||
24.18.227.2:39435 - FreeBSD 8.x (1) (up: 1390 hrs) | ||
-> 208.75.177.101:25 (distance 17, link: ethernet/modem) | ||
24.18.227.2:39435 - FreeBSD 8.x (1) (up: 1390 hrs) | ||
-> 208.75.177.101:25 (distance 17, link: ethernet/modem) | ||
@@ -32,6 +31,4 @@ The following additional values may also be available in | ||
## Configuration | ||
Configuration | ||
----------------- | ||
1. start p0f | ||
@@ -51,3 +48,2 @@ | ||
3. review settings in config/p0f.ini | ||
@@ -63,4 +59,4 @@ | ||
<!-- leave these buried at the bottom of the document --> | ||
<!-- leave these buried at the bottom of the document --> | ||
[ci-img]: https://github.com/haraka/haraka-plugin-p0f/actions/workflows/ci.yml/badge.svg | ||
@@ -70,3 +66,1 @@ [ci-url]: https://github.com/haraka/haraka-plugin-p0f/actions/workflows/ci.yml | ||
[clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-p0f | ||
[npm-img]: https://nodei.co/npm/haraka-plugin-p0f.png | ||
[npm-url]: https://www.npmjs.com/package/haraka-plugin-p0f |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
3
14707
9
230
62
2
Updatedipaddr.js@^2.2.0