From 0407ff740a26f617b60596b69ab1a8ae3cb13c24 Mon Sep 17 00:00:00 2001 From: Edouard Maleix Date: Wed, 20 Sep 2023 12:27:47 +0200 Subject: [PATCH] feat: extract server port in `ConnectionDetails` (#11) * chore: update dependencies * feat: extract server port * ci: update node versions and used actions * test: support different local IPs * ci: cancel concurrent workflow --- .github/workflows/ci.yml | 10 ++++-- index.js | 69 ++++++++++++++++++++++++++++++++++++++-- package.json | 30 ++++++++--------- test/test.js | 8 +++-- types/index.d.ts | 2 ++ 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e81d86..550dee1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,19 +2,23 @@ name: ci on: [push, pull_request] +concurrency: + group: ci-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: test: timeout-minutes: 5 runs-on: ubuntu-latest strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [16.x, 18.x, 20.x] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} diff --git a/index.js b/index.js index bd0ac9d..dc797e0 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,25 @@ 'use strict' +/** + * @typedef ConnectionDetails + * @property {string} ipAddress + * @property {number} port + * @property {string} serverIpAddress + * @property {number} serverPort + * @property {4 | 6 | 0} ipFamily + * @property {boolean} isWebsocket + * @property {boolean} isTls + * @property {0 | 1 | 2} isProxy + * @property {boolean | undefined} certAuthorized + * @property {object | import('tls').PeerCertificate | undefined} cert + * @property {Buffer | undefined} data + */ + +/** + * @typedef HttpConnection + * @property { import('net').Socket } _socket + */ + const proxyProtocol = require('proxy-protocol-js') const forwarded = require('forwarded') @@ -43,6 +63,14 @@ function getProtoIpFamily (ipFamily) { return 0 } +/** + * + * @param {import('http').IncomingMessage} req + * @param {HttpConnection} socket + * @param {ConnectionDetails} proto + * @returns {ConnectionDetails} + * @todo replace socket arg by req.socket ? + */ function extractHttpDetails (req, socket, proto = {}) { const headers = req && req.headers ? req.headers : null if (headers) { @@ -54,6 +82,8 @@ function extractHttpDetails (req, socket, proto = {}) { if (headers['x-real-ip']) { proto.ipAddress = headers['x-real-ip'] } + // ? should we try to parse req.url to get the port first and then fallback to socket.address()?.port ? + proto.serverPort = socket._socket.address()?.port proto.port = socket._socket.remotePort proto.ipFamily = getProtoIpFamily(socket._socket.remoteFamily) proto.isWebsocket = true @@ -61,6 +91,12 @@ function extractHttpDetails (req, socket, proto = {}) { return proto } +/** + * + * @param {Buffer} buffer + * @param {ConnectionDetails} proto + * @returns {ConnectionDetails} + */ function extractProxyDetails (buffer, proto = {}) { let proxyProto if (isValidV1ProxyProtocol(buffer)) { @@ -70,6 +106,7 @@ function extractProxyDetails (buffer, proto = {}) { proto.ipAddress = proxyProto.source.ipAddress proto.port = proxyProto.source.port proto.serverIpAddress = proxyProto.destination.ipAddress + proto.serverPort = proxyProto.destination.port proto.data = proxyProto.data proto.isProxy = 1 } @@ -80,11 +117,13 @@ function extractProxyDetails (buffer, proto = {}) { proto.ipAddress = proxyProto.proxyAddress.sourceAddress.address.join('.') proto.port = proxyProto.proxyAddress.sourcePort proto.serverIpAddress = proxyProto.proxyAddress.destinationAddress.address.join('.') + proto.serverPort = proxyProto.proxyAddress.destinationPort proto.ipFamily = 4 } else if (proxyProto.proxyAddress instanceof proxyProtocol.IPv6ProxyAddress) { proto.ipAddress = parseIpV6Array(proxyProto.proxyAddress.sourceAddress.address) proto.port = proxyProto.proxyAddress.sourcePort proto.serverIpAddress = parseIpV6Array(proxyProto.proxyAddress.destinationAddress.address) + proto.serverPort = proxyProto.proxyAddress.destinationPort proto.ipFamily = 6 } proto.isProxy = 2 @@ -94,38 +133,62 @@ function extractProxyDetails (buffer, proto = {}) { return proto } +/** + * + * @param {import('net').Socket | HttpConnection} socket + * @param {ConnectionDetails} proto + * @returns {ConnectionDetails} + */ function extractSocketTLSDetails (socket, proto = {}) { socket = socket._socket || socket if (socket.getPeerCertificate && typeof socket.getPeerCertificate === 'function') { proto.certAuthorized = socket.authorized proto.cert = socket.getPeerCertificate(true) + proto.isTls = true } return proto } +/** + * + * @param {import('net').Socket | HttpConnection} socket + * @param {ConnectionDetails} proto + * @returns {ConnectionDetails} + */ function extractSocketDetails (socket, proto = {}) { if (socket._socket && socket._socket.address) { proto.isWebsocket = true proto.ipAddress = socket._socket.remoteAddress proto.port = socket._socket.remotePort proto.serverIpAddress = socket._socket.address().address + proto.serverPort = socket._socket.address().port proto.ipFamily = getProtoIpFamily(socket._socket.remoteFamily) } else if (socket.address) { proto.ipAddress = socket.remoteAddress proto.port = socket.remotePort proto.serverIpAddress = socket.address().address + proto.serverPort = socket.address().port proto.ipFamily = getProtoIpFamily(socket.remoteFamily) } extractSocketTLSDetails(socket, proto) return proto } +/** + * + * @param {import('aedes').Connection} conn + * @param {Buffer} buffer + * @param {import('http').IncomingMessage} req + * @returns {ConnectionDetails} + */ function protocolDecoder (conn, buffer, req) { - const proto = {} + const proto = { + isProxy: 0, + isWebsocket: false, + isTls: false + } if (!buffer) return proto const socket = conn.socket || conn - proto.isProxy = 0 - proto.isWebsocket = false extractHttpDetails(req, socket, proto) extractProxyDetails(buffer, proto) if (!proto.ipAddress) { diff --git a/package.json b/package.json index 5bc25f4..15f5f12 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,10 @@ "decoder", "parser" ], - "author": "Get Large ", + "author": "Edouard Maleix ", "contributors": [ { - "name": "Get Large", + "name": "Edouard Maleix", "url": "https://github.com/getlarge" }, { @@ -64,25 +64,25 @@ ], "license": "MIT", "devDependencies": { - "@types/node": "^14.0.1", - "@typescript-eslint/eslint-plugin": "^4.28.0", - "@typescript-eslint/parser": "^4.28.0", - "aedes": "^0.46.0", - "aedes-server-factory": "0.1.3", - "faucet": "0.0.1", + "@types/node": "^18.17.0", + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "aedes": "^0.50.0", + "aedes-server-factory": "0.2.1", + "faucet": "0.0.4", "license-checker": "^25.0.1", - "mqtt": "^4.2.8", - "mqtt-packet": "^7.0.0", + "mqtt": "^5.0.5", + "mqtt-packet": "^8.2.0", "nyc": "^15.1.0", "pre-commit": "^1.2.2", - "release-it": "^14.10.0", + "release-it": "^16.1.5", "snazzy": "^9.0.0", - "standard": "^16.0.3", - "tape": "^5.2.0", - "typescript": "^4.3.4" + "standard": "^17.1.0", + "tape": "^5.6.6", + "typescript": "^5.2.2" }, "dependencies": { "forwarded": "^0.2.0", - "proxy-protocol-js": "^4.0.5" + "proxy-protocol-js": "^4.0.6" } } diff --git a/test/test.js b/test/test.js index 85fc59d..3e62ea5 100644 --- a/test/test.js +++ b/test/test.js @@ -94,12 +94,13 @@ test('tcp clients have access to the ipAddress from the socket', function (t) { t.plan(2) const port = 4883 + const clientIps = ['::ffff:127.0.0.1', '::1'] const setup = start({ broker: { preConnect: function (client, packet, done) { if (client && client.connDetails && client.connDetails.ipAddress) { client.ip = client.connDetails.ipAddress - t.equal('::ffff:127.0.0.1', client.ip) + t.equal(clientIps.includes(client.ip), true) } else { t.fail('no ip address present') } @@ -257,7 +258,8 @@ test('tcp proxied (protocol v2) clients have access to the ipAddress(v6)', funct test('websocket clients have access to the ipAddress from the socket (if no ip header)', function (t) { t.plan(2) - const clientIp = '::ffff:127.0.0.1' + // local client IPs might resolve slightly differently + const clientIps = ['::ffff:127.0.0.1', '::1'] const port = 4883 const setup = start({ @@ -265,7 +267,7 @@ test('websocket clients have access to the ipAddress from the socket (if no ip h preConnect: function (client, packet, done) { if (client.connDetails && client.connDetails.ipAddress) { client.ip = client.connDetails.ipAddress - t.equal(clientIp, client.ip) + t.equal(clientIps.includes(client.ip), true) } else { t.fail('no ip address present') } diff --git a/types/index.d.ts b/types/index.d.ts index bf0fcd1..b1cea2d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -13,8 +13,10 @@ export interface ConnectionDetails { port: number ipFamily: number serverIpAddress: string + serverPort: number isWebsocket: boolean isProxy: number + isTls: boolean certAuthorized?: boolean, cert?: PeerCertificate | {} | null, data?: Buffer