diff --git a/.codeclimate.yml b/.codeclimate.yml
index dfe3bb6..563da6b 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,10 +1,10 @@
engines:
eslint:
enabled: true
- channel: "eslint-6"
+ channel: 'eslint-9'
config:
- config: ".eslintrc.yaml"
+ config: 'eslint.config.mjs'
ratings:
- paths:
- - "**.js"
+ paths:
+ - '**.js'
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
deleted file mode 100644
index 85e8420..0000000
--- a/.eslintrc.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-env:
- node: true
- es6: true
- mocha: true
-
-plugins:
- - haraka
-
-extends:
- - eslint:recommended
- - plugin:haraka/recommended
-
-root: true
-
-globals:
- OK: true
- CONT: true
- DENY: true
- DENYSOFT: true
- DENYDISCONNECT: true
- DENYSOFTDISCONNECT: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0449e4a..d450132 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -2,9 +2,9 @@
version: 2
updates:
- - package-ecosystem: "npm"
- directory: "/"
+ - package-ecosystem: 'npm'
+ directory: '/'
schedule:
- interval: "weekly"
+ interval: 'weekly'
allow:
- dependency-type: production
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5231a37..b33132b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,11 @@
name: CI
-on: [ push ]
+on: [push, pull_request]
env:
CI: true
jobs:
-
lint:
uses: haraka/.github/.github/workflows/lint.yml@master
@@ -15,5 +14,5 @@ jobs:
# secrets: inherit
ubuntu:
- needs: [ lint ]
+ needs: [lint]
uses: haraka/.github/.github/workflows/ubuntu.yml@master
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 383aca2..816e8c3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,10 +1,10 @@
-name: "CodeQL"
+name: 'CodeQL'
on:
push:
- branches: [ master ]
+ branches: [master]
pull_request:
- branches: [ master ]
+ branches: [master]
schedule:
- cron: '18 7 * * 4'
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d489fbd..e81c15f 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,4 +13,4 @@ env:
jobs:
publish:
uses: haraka/.github/.github/workflows/publish.yml@master
- secrets: inherit
\ No newline at end of file
+ secrets: inherit
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..8ded5e0
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,2 @@
+singleQuote: true
+semi: false
diff --git a/.release b/.release
index 029586b..0bf2a09 160000
--- a/.release
+++ b/.release
@@ -1 +1 @@
-Subproject commit 029586bf3a8693bd8f46fd441e05ba1596578d3a
+Subproject commit 0bf2a098d4792848c2103dfce0f911e00a14709e
diff --git a/Changes.md b/CHANGELOG.md
similarity index 63%
rename from Changes.md
rename to CHANGELOG.md
index 5322076..50898b3 100644
--- a/Changes.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
+# Changelog
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/).
+
### Unreleased
+### [1.0.10] - 2025-01-14
+
+- automated code formatting with prettier
+- doc: mv Changes.md CHANGELOG.md
+- doc(CONTRIBUTORS): added
+- populate [files] in package.json.
+- deps: bump versions
+- dep(eslint): upgrade to v9
+- dep: eslint-plugin-haraka -> @haraka/eslint-config
+
### [1.0.9] - 2022-11-10
- fix connect path argument causing server crash when socket missing
@@ -10,7 +24,6 @@
- ci: only publish when package.json changes
- Added systemd.service file (#25)
-
### [1.0.7] - 2022-06-05
- ci: update GHA workflow with shared
@@ -18,42 +31,37 @@
- ci: add submodule .release
- test: require mocha >= 9
-
### 1.0.6 - 2021-11-10
- bump eslint 6 -> 8
-
### 1.0.5 - 2020-12-30
- es6: use object shorthand
- update dep ipaddr.js version
-
### 1.0.4 - 2019-12-23
- update to es6 classes
-
### 1.0.3 - 2018-05-30
- add_header option visible at config
-
### 1.0.2 - 2017-09-11
- when socket_path is not configured, emit an error
-
### 1.0.1 - 2017-09-01
- repackaged as p0f, added contrib scripts, test release
-
### 1.0.0 - 2017-07-27
- import from Haraka
-
[1.0.7]: https://github.com/haraka/haraka-plugin-p0f/releases/tag/1.0.7
[1.0.8]: https://github.com/haraka/haraka-plugin-p0f/releases/tag/1.0.8
+[1.0.10]: https://github.com/haraka/haraka-plugin-p0f/releases/tag/v1.0.10
+[1.0.6]: https://github.com/haraka/haraka-plugin-p0f/releases/tag/1.0.6
+[1.0.9]: https://github.com/haraka/haraka-plugin-p0f/releases/tag/1.0.9
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000..bce0698
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,9 @@
+# Contributors
+
+This handcrafted artisinal software is brought to you by:
+
+|
msimerson (23) |
analogic (2) |
wioxjk (1) |
+| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
+
+this file is generated by [.release](https://github.com/msimerson/.release).
+Contribute to this project to get your GitHub profile included here.
diff --git a/README.md b/README.md
index 544ce81..69e0d2e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,5 @@
[![Build Status][ci-img]][ci-url]
[![Code Climate][clim-img]][clim-url]
-[![NPM][npm-img]][npm-url]
# haraka-plugin-p0f
@@ -12,25 +11,23 @@ This plugin inserts a _p0f_ connection note with information deduced
from the TCP fingerprint. The note typically includes at least the link,
detail, distance, uptime, genre. Here's an example:
- 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)
The following additional values may also be available in
the _p0f_ connection note:
magic, status, first_seen, last_seen, total_conn, uptime_min, up_mod_days, last_nat, last_chg, distance, bad_sw, os_match_q, os_name, os_flavor, http_name, http_flavor, link_type, and language.
-
-Configuration
------------------
+## Configuration
1. start p0f
@@ -46,7 +43,6 @@ add an entry to config/plugins to enable p0f:
p0f
-
3. review settings in config/p0f.ini
At a minimum, `[main]socket_path` must be defined.
@@ -57,11 +53,9 @@ In the contrib/ubuntu-upstart directory is a config file (p0f.conf) for Ubuntu.
In the contrib/bsd-rc.d directory is a startup file for FreeBSD.
-
+
[ci-img]: https://github.com/haraka/haraka-plugin-p0f/actions/workflows/ci.yml/badge.svg
[ci-url]: https://github.com/haraka/haraka-plugin-p0f/actions/workflows/ci.yml
[clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-p0f/badges/gpa.svg
[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
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..7272162
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,34 @@
+import globals from 'globals'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import js from '@eslint/js'
+import { FlatCompat } from '@eslint/eslintrc'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
+})
+
+export default [
+ ...compat.extends('@haraka'),
+ {
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ ...globals.mocha,
+ OK: true,
+ CONT: true,
+ DENY: true,
+ DENYSOFT: true,
+ DENYDISCONNECT: true,
+ DENYSOFTDISCONNECT: true,
+ },
+ },
+ rules: {
+ 'no-unused-vars': 'warn',
+ },
+ },
+]
diff --git a/index.js b/index.js
index 5d627d1..fcfbd1a 100644
--- a/index.js
+++ b/index.js
@@ -1,262 +1,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.connect(path)
+ }
+
+ 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 = null;
- this.send_queue = [];
- this.receive_queue = [];
- this.connected = false;
- this.ready = false;
- this.socket_has_error = false;
- this.restart_interval = false;
+ 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.connect(path);
- }
+ this.sock.on('drain', () => {
+ this.ready = true
+ this.process_send_queue()
+ })
- 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('data', (data) => {
- for (let i=0; i {
- 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();
-
- // 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.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()
+ })
+ }
- 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 {
- if (err) {
- connection.results.add(plugin, {err: err.message});
- 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();
- })
+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()
+ }
+
+ 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()
+ }
+
+ 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 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.logdebug(plugin, 'adding header');
- connection.transaction.add_header(header_name, format_results(result));
-
- 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()
+ }
+
+ 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))
+
+ next()
}
diff --git a/package.json b/package.json
index c0f51d7..a46e7b9 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,23 @@
{
"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"
},
"repository": {
"type": "git",
@@ -25,12 +35,11 @@
},
"homepage": "https://github.com/haraka/haraka-plugin-p0f#readme",
"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"
}
}
diff --git a/test/index.js b/test/index.js
index 1dd8da5..b86befe 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,6 +1,5 @@
-
// node.js built-in modules
-const assert = require('assert')
+const assert = require('assert')
// npm modules
const fixtures = require('haraka-test-fixtures')
@@ -10,60 +9,59 @@ const fixtures = require('haraka-test-fixtures')
// mocha: http://mochajs.org
beforeEach((done) => {
- this.plugin = new fixtures.plugin('p0f')
- done()
+ this.plugin = new fixtures.plugin('p0f')
+ done()
})
describe('p0f', () => {
- it('loads', (done) => {
- assert.ok(this.plugin)
- done()
- })
+ it('loads', (done) => {
+ assert.ok(this.plugin)
+ done()
+ })
})
describe('load_p0f_ini', () => {
- it('loads p0f.ini from config/p0f.ini', (done) => {
- this.plugin.load_p0f_ini()
- assert.ok(this.plugin.cfg)
- done()
- })
+ it('loads p0f.ini from config/p0f.ini', (done) => {
+ this.plugin.load_p0f_ini()
+ assert.ok(this.plugin.cfg)
+ done()
+ })
})
describe('lookup_rdns', () => {
- it.skip('retrieves TCP fingerprint data from p0f server', (done) => {
- done()
- })
+ it.skip('retrieves TCP fingerprint data from p0f server', (done) => {
+ done()
+ })
})
describe('data_post', () => {
+ beforeEach((done) => {
+ this.plugin = new fixtures.plugin('p0f')
+ this.plugin.load_p0f_ini()
+ this.plugin.cfg.main.add_header = 'X-p0f-Result'
+ this.connection = new fixtures.connection.createConnection()
+ this.connection.init_transaction()
+ this.connection.results.add(
+ { name: 'p0f' },
+ { os_name: 'BeOS', os_flavor: 'forever' },
+ )
+ done()
+ })
- beforeEach((done) => {
- this.plugin = new fixtures.plugin('p0f')
- this.plugin.load_p0f_ini()
- this.plugin.cfg.main.add_header = 'X-p0f-Result'
- this.connection = new fixtures.connection.createConnection()
- this.connection.transaction = new fixtures.transaction.createTransaction()
- this.connection.results.add({ name: 'p0f' }, { os_name: 'BeOS', os_flavor: 'forever'})
- done()
- })
-
- it('adds a header when data exists', (done) => {
-
- this.plugin.add_p0f_header((code, value) => {
- assert.ok(Object.keys(this.connection.transaction.header.headers))
- done()
- },
- this.connection)
- })
+ it('adds a header when data exists', (done) => {
+ this.plugin.add_p0f_header((code, value) => {
+ assert.ok(Object.keys(this.connection.transaction.header.headers))
+ done()
+ }, this.connection)
+ })
- it('ignores private IPs', (done) => {
- this.connection.remote.is_private=true;
- this.plugin.add_p0f_header((code, value) => {
- assert.equal(code, undefined)
- assert.equal(value, undefined)
- assert.equal(Object.keys(this.connection.transaction.header.headers), 0)
- done()
- },
- this.connection)
- })
-})
\ No newline at end of file
+ it('ignores private IPs', (done) => {
+ this.connection.remote.is_private = true
+ this.plugin.add_p0f_header((code, value) => {
+ assert.equal(code, undefined)
+ assert.equal(value, undefined)
+ assert.equal(Object.keys(this.connection.transaction.header.headers), 0)
+ done()
+ }, this.connection)
+ })
+})