Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/rfc-8252-http-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { SinonSandbox } from 'sinon';
import sinon from 'sinon';
import { promisify } from 'util';
import { randomBytes } from 'crypto';
import { promises as dns } from 'dns';

// node-fetch@3 is ESM-only...
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
Expand Down Expand Up @@ -485,4 +486,64 @@ describe('RFC8252HTTPServer', function () {
});
});
});

context('with dns duplicates', function () {
let dnsLookupStub: sinon.SinonStub;
const _getAllInterfaces = RFC8252HTTPServer['_getAllInterfaces'];

this.beforeEach(function () {
dnsLookupStub = sinon.stub(dns, 'lookup');
});

this.afterEach(function () {
sinon.restore();
});

it('only filters exact duplicates', async function () {
dnsLookupStub.resolves([
{ address: '127.0.0.1', family: 4 },
{ address: '127.0.0.1', family: 4 },
{ address: '[::1]', family: 6 },
{ address: '[::1]', family: 6 },
]);

const interfaces = await _getAllInterfaces('localhost');

expect(interfaces).to.have.lengthOf(2);
expect(interfaces[0].address).to.equal('127.0.0.1');
expect(interfaces[1].address).to.equal('[::1]');
expect(interfaces[0].family).to.equal(4);
expect(interfaces[1].family).to.equal(6);
});

it('keeps same addresses, different family', async function () {
dnsLookupStub.resolves([
{ address: '127.0.0.1', family: 4 },
{ address: '127.0.0.1', family: 6 },
]);

const interfaces = await _getAllInterfaces('localhost');

expect(interfaces).to.have.lengthOf(2);
expect(interfaces[0].address).to.equal('127.0.0.1');
expect(interfaces[1].address).to.equal('127.0.0.1');
expect(interfaces[0].family).to.equal(4);
expect(interfaces[1].family).to.equal(6);
});

it('keeps same familes, different address', async function () {
dnsLookupStub.resolves([
{ address: '127.0.0.1', family: 4 },
{ address: '192.168.1.15', family: 4 },
]);

const interfaces = await _getAllInterfaces('localhost');

expect(interfaces).to.have.lengthOf(2);
expect(interfaces[0].address).to.equal('127.0.0.1');
expect(interfaces[1].address).to.equal('192.168.1.15');
expect(interfaces[0].family).to.equal(4);
expect(interfaces[1].family).to.equal(4);
});
});
});
30 changes: 23 additions & 7 deletions src/rfc-8252-http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,25 @@ export class RFC8252HTTPServer {
});
};

private static async _getAllInterfaces(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional suggestions: Feels a bit silly to mark this private and then access it as if it weren't private, no? It's not exported from the package anyway, don't be shy to mark this public to make it a bit more obvious that it's tested directly

And while you're at it, you might as well pass in dns.lookup as an argument instead of mocking it in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was debating with myself whether I want to make it public or if I want to instead call .listen() and then inspect the logs to see what interfaces were discovered. I feel a bit iffy about exposing methods publicly only for the sake of testing, but perhaps that's a bit more acceptable in the JS world 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that feels iffy, I think you could also just export it as a separate method, it's fairly standalone and not really tied to the rest of the class semantically 🙂

hostname: string
): Promise<{ address: string; family: number }[]> {
const dnsResults = await dns.lookup(hostname, {
all: true,
hints: ADDRCONFIG,
});

return dnsResults
.filter(
(dns, index, arr) =>
arr.findIndex(
(otherDns) =>
dns.address === otherDns.address && dns.family === otherDns.family
) === index
)
.map(({ address, family }) => ({ address, family }));
}

/**
* Add a redirect from a local URL served on the server to an external URL.
*/
Expand Down Expand Up @@ -368,14 +387,11 @@ export class RFC8252HTTPServer {
// to do what Node.js does by default when only a host is provided,
// namely listening on all interfaces.
let hostname = this.redirectUrl.hostname;
if (hostname.startsWith('[') && hostname.endsWith(']'))
if (hostname.startsWith('[') && hostname.endsWith(']')) {
hostname = hostname.slice(1, -1);
const dnsResults = (
await dns.lookup(hostname, {
all: true,
hints: ADDRCONFIG,
})
).map(({ address, family }) => ({ address, family }));
}

const dnsResults = await RFC8252HTTPServer._getAllInterfaces(hostname);

this.logger.emit('mongodb-oidc-plugin:local-listen-resolved-hostname', {
url: this.redirectUrl.toString(),
Expand Down
Loading