Skip to content

Commit

Permalink
feat: extract server port in ConnectionDetails (#11)
Browse files Browse the repository at this point in the history
* chore: update dependencies

* feat: extract server port

* ci: update node versions and used actions

* test: support different local IPs

* ci: cancel concurrent workflow
  • Loading branch information
getlarge authored Sep 20, 2023
1 parent 5ff2fa4 commit 0407ff7
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 24 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
69 changes: 66 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -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')

Expand Down Expand Up @@ -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) {
Expand All @@ -54,13 +82,21 @@ 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
}
return proto
}

/**
*
* @param {Buffer} buffer
* @param {ConnectionDetails} proto
* @returns {ConnectionDetails}
*/
function extractProxyDetails (buffer, proto = {}) {
let proxyProto
if (isValidV1ProxyProtocol(buffer)) {
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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) {
Expand Down
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@
"decoder",
"parser"
],
"author": "Get Large <https://github.com/getlarge>",
"author": "Edouard Maleix <https://github.com/getlarge>",
"contributors": [
{
"name": "Get Large",
"name": "Edouard Maleix",
"url": "https://github.com/getlarge"
},
{
Expand All @@ -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"
}
}
8 changes: 5 additions & 3 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand Down Expand Up @@ -257,15 +258,16 @@ 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({
broker: {
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')
}
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 0407ff7

Please sign in to comment.