haraka-net-utils
Advanced tools
Comparing version 1.6.0 to 1.7.0
@@ -7,5 +7,14 @@ # Changelog | ||
### [1.7.0] - 2024-04-29 | ||
- feat: added HarakaMx #89 | ||
- feat: add add_line_processor, aka line_socket.setup_line_processor | ||
- fix(get_public_ip): set timeout in stun request, fixes #84 | ||
- test: added get_implicit_mx tests #89 | ||
- change: get_mx: don't filter implicit MX errors #89 | ||
### [1.6.0] - 2024-04-17 | ||
- feat: normalizeDomain, for punycode/IDN names | ||
<!-- prettier-ignore --> | ||
- feat: get_mx now _also_ returns implicit MX records | ||
@@ -66,3 +75,3 @@ - feat: added get_implicit_mx | ||
#### [1.3.5] - 2022-05-27 | ||
### [1.3.5] - 2022-05-27 | ||
@@ -74,15 +83,15 @@ - chore(ci): use shared GHA workflows | ||
#### [1.3.4] - 2022-01-05 | ||
### [1.3.4] - 2022-01-05 | ||
- promisify get_ips_by_host (backwards compatible) | ||
#### [1.3.3] - 2020-01-05 | ||
### [1.3.3] - 2020-01-05 | ||
- refactored is_local_host function to return a promise instead of using a callback #65 | ||
#### [1.3.2] - 2021-12-20 | ||
### [1.3.2] - 2021-12-20 | ||
- add is_local_host function #63 | ||
#### [1.3.1] - 2021-10-13 | ||
### [1.3.1] - 2021-10-13 | ||
@@ -93,7 +102,7 @@ - get_mx: wrap dns.resolveMx in a try haraka/Haraka#2985 | ||
#### 1.3.0 - 2021-01-23 | ||
### 1.3.0 - 2021-01-23 | ||
- Support passing an array to ip_in_list #60 | ||
#### 1.2.4 - 2021-01-14 | ||
### 1.2.4 - 2021-01-14 | ||
@@ -103,15 +112,15 @@ - add "any" IP to is_local_ip | ||
#### 1.2.3 - 2020-12-19 | ||
### 1.2.3 - 2020-12-19 | ||
- fix: restore the tests wrapping the resolveMX iterable | ||
#### 1.2.2 - 2020-12-15 | ||
### 1.2.2 - 2020-12-15 | ||
- get_mx: do not include implicit MX | ||
#### [1.2.1] - 2020-11-17 | ||
### [1.2.1] - 2020-11-17 | ||
- bump ipaddr.js to 2.0.0 #56 | ||
#### [1.2.0] - 2020-06-23 | ||
### [1.2.0] - 2020-06-23 | ||
@@ -122,3 +131,3 @@ - added get_mx | ||
#### 1.1.5 - 2020-04-11 | ||
### 1.1.5 - 2020-04-11 | ||
@@ -128,27 +137,27 @@ - ipv6_bogus: handle parsing broken ipv6 addresses #49 | ||
#### 1.1.4 - 2019-04-04 | ||
### 1.1.4 - 2019-04-04 | ||
- stop is_private_ip from checking if the IP is bound to a local network interface | ||
#### 1.1.3 - 2019-03-01 | ||
### 1.1.3 - 2019-03-01 | ||
- is_local_ip checks local network interfaces too | ||
#### 1.1.2 - 2018-11-03 | ||
### 1.1.2 - 2018-11-03 | ||
- add is_local_ip | ||
#### 1.1.1 - 2018-07-19 | ||
### 1.1.1 - 2018-07-19 | ||
- ip_in_list doesn't throw on empty list | ||
#### 1.1.0 - 2018-04-11 | ||
### 1.1.0 - 2018-04-11 | ||
- add get_primary_host_name haraka/Haraka#2380 | ||
#### 1.0.14 - 2018-01-25 | ||
### 1.0.14 - 2018-01-25 | ||
- restore tls_ini_section_with_defaults function (deprecated since Haraka 2.0.17) | ||
#### 1.0.13 - 2018-01-19 | ||
### 1.0.13 - 2018-01-19 | ||
@@ -162,11 +171,11 @@ - get_public_ip: assign timer before calling connect #29 | ||
#### 1.0.10 - 2017-07-27 | ||
### 1.0.10 - 2017-07-27 | ||
- added vs-stun as optional dep (from Haraka) #21 | ||
#### 1.0.9 - 2017-06-16 | ||
### 1.0.9 - 2017-06-16 | ||
- lint fixes for compat with eslint 4 #18 | ||
#### 1.0.8 - 2017-03-08 | ||
### 1.0.8 - 2017-03-08 | ||
@@ -178,7 +187,7 @@ - skip loading expired x509 (TLS) certs | ||
#### 1.0.7 - 2017-03-08 | ||
### 1.0.7 - 2017-03-08 | ||
- handle undefined tls.ini section | ||
#### 1.0.6 - 2017-03-04 | ||
### 1.0.6 - 2017-03-04 | ||
@@ -189,11 +198,11 @@ - add tls_ini_section_with_defaults() | ||
#### 1.0.5 - 2016-11-20 | ||
### 1.0.5 - 2016-11-20 | ||
- add enableSNI TLS option | ||
#### 1.0.4 - 2016-10-25 | ||
### 1.0.4 - 2016-10-25 | ||
- initialize TLS opts in (section != main) as booleans | ||
#### 1.0.3 - 2016-10-25 | ||
### 1.0.3 - 2016-10-25 | ||
@@ -210,3 +219,3 @@ - added tls.ini loading | ||
[1.3.6]: https://github.com/haraka/haraka-net-utils/releases/tag/1.3.6 | ||
[1.3.7]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.3.7 | ||
[1.3.7]: https://github.com/haraka/haraka-net-utils/releases/tag/1.3.7 | ||
[1.4.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.4.0 | ||
@@ -220,1 +229,2 @@ [1.4.1]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.4.1 | ||
[1.6.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.6.0 | ||
[1.7.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.7.0 |
235
index.js
'use strict' | ||
// node.js built-ins | ||
const { Resolver } = require('dns').promises | ||
const { Resolver } = require('node:dns').promises | ||
const dns = new Resolver({ timeout: 25000, tries: 1 }) | ||
const net = require('net') | ||
const os = require('os') | ||
const punycode = require('punycode.js') | ||
const net = require('node:net') | ||
const os = require('node:os') | ||
@@ -262,93 +260,3 @@ // npm modules | ||
exports.get_public_ip_async = async function () { | ||
if (this.public_ip !== undefined) return this.public_ip // cache | ||
// manual config override, for the cases where we can't figure it out | ||
const smtpIni = exports.config.get('smtp.ini').main | ||
if (smtpIni.public_ip) { | ||
this.public_ip = smtpIni.public_ip | ||
return this.public_ip | ||
} | ||
// Initialise cache value to null to prevent running | ||
// should we hit a timeout or the module isn't installed. | ||
this.public_ip = null | ||
try { | ||
this.stun = require('@msimerson/stun') | ||
} catch (e) { | ||
e.install = 'Please install stun: "npm install -g stun"' | ||
console.error(`${e.msg}\n${e.install}`) | ||
return | ||
} | ||
const timeout = 10 | ||
const timer = setTimeout(() => { | ||
return new Error('STUN timeout') | ||
}, timeout * 1000) | ||
// Connect to STUN Server | ||
const res = await this.stun.request(get_stun_server()) | ||
this.public_ip = res.getXorAddress().address | ||
clearTimeout(timer) | ||
return this.public_ip | ||
} | ||
exports.get_public_ip = async function (cb) { | ||
if (!cb) return exports.get_public_ip_async() | ||
const nu = this | ||
if (nu.public_ip !== undefined) return cb(null, nu.public_ip) // cache | ||
// manual config override, for the cases where we can't figure it out | ||
const smtpIni = exports.config.get('smtp.ini').main | ||
if (smtpIni.public_ip) { | ||
nu.public_ip = smtpIni.public_ip | ||
return cb(null, nu.public_ip) | ||
} | ||
// Initialise cache value to null to prevent running | ||
// should we hit a timeout or the module isn't installed. | ||
nu.public_ip = null | ||
try { | ||
nu.stun = require('@msimerson/stun') | ||
} catch (e) { | ||
e.install = 'Please install stun: "npm install -g stun"' | ||
console.error(`${e.msg}\n${e.install}`) | ||
return cb(e) | ||
} | ||
const timeout = 10 | ||
const timer = setTimeout(() => { | ||
return cb(new Error('STUN timeout')) | ||
}, timeout * 1000) | ||
// Connect to STUN Server | ||
nu.stun.request(get_stun_server(), (error, res) => { | ||
if (timer) clearTimeout(timer) | ||
if (error) return cb(error) | ||
nu.public_ip = res.getXorAddress().address | ||
cb(null, nu.public_ip) | ||
}) | ||
} | ||
function get_stun_server() { | ||
// STUN servers by Google | ||
const servers = [ | ||
'stun.l.google.com:19302', | ||
'stun1.l.google.com:19302', | ||
'stun2.l.google.com:19302', | ||
'stun3.l.google.com:19302', | ||
'stun4.l.google.com:19302', | ||
] | ||
return servers[Math.floor(Math.random() * servers.length)] | ||
} | ||
exports.get_ipany_re = function (prefix, suffix, modifier) { | ||
if (prefix === undefined) prefix = '' | ||
if (suffix === undefined) suffix = '' | ||
if (modifier === undefined) modifier = 'mg' | ||
/* eslint-disable prefer-template */ | ||
exports.get_ipany_re = function (prefix = '', suffix = '', modifier = 'mg') { | ||
return new RegExp( | ||
@@ -450,125 +358,32 @@ prefix + | ||
function normalizeDomain(raw_domain) { | ||
let domain = raw_domain | ||
if (/@/.test(domain)) { | ||
domain = domain.split('@').pop() | ||
// console.log(`\treduced ${raw_domain} to ${domain}.`) | ||
} | ||
if (/^xn--/.test(domain)) { | ||
// is punycode IDN with ACE, ASCII Compatible Encoding | ||
} else if (domain !== punycode.toASCII(domain)) { | ||
domain = punycode.toASCII(domain) | ||
console.log(`\tACE encoded '${raw_domain}' to '${domain}'`) | ||
} | ||
return domain | ||
for (const l of ['get_mx', 'get_implicit_mx', 'resolve_mx_hosts']) { | ||
exports[l] = require('./lib/get_mx')[l] | ||
} | ||
function fatal_mx_err(err) { | ||
// Possible DNS errors | ||
// NODATA | ||
// FORMERR | ||
// BADRESP | ||
// NOTFOUND | ||
// BADNAME | ||
// TIMEOUT | ||
// CONNREFUSED | ||
// NOMEM | ||
// DESTRUCTION | ||
// NOTIMP | ||
// EREFUSED | ||
// SERVFAIL | ||
exports.get_public_ip = require('./lib/get_public_ip').get_public_ip | ||
switch (err.code) { | ||
case 'ENODATA': | ||
case 'ENOTFOUND': | ||
// likely a hostname with no MX record, drop through | ||
return false | ||
default: | ||
return err | ||
} | ||
} | ||
exports.get_public_ip_async = require('./lib/get_public_ip').get_public_ip_async | ||
exports.get_mx = async (raw_domain, cb) => { | ||
const domain = normalizeDomain(raw_domain) | ||
exports.HarakaMx = require('./lib/HarakaMx') | ||
try { | ||
const exchanges = await dns.resolveMx(domain) | ||
if (exchanges && exchanges.length) { | ||
exchanges.map((e) => (e.from_dns = domain)) | ||
if (cb) return cb(null, exchanges) | ||
return exchanges | ||
} | ||
} catch (err) { | ||
// console.error(err.message) | ||
if (fatal_mx_err(err)) { | ||
if (cb) return cb(err, []) | ||
throw err | ||
} | ||
} | ||
exports.add_line_processor = (socket) => { | ||
const line_regexp = /^([^\n]*\n)/ // utils.line_regexp | ||
let current_data = '' | ||
// no MX or non-fatal DNS failure | ||
try { | ||
const exchanges = await this.get_implicit_mx(domain) | ||
if (cb) return cb(null, exchanges) | ||
return exchanges | ||
} catch (err) { | ||
if (fatal_mx_err(err)) { | ||
if (cb) return cb(err, []) | ||
throw err | ||
socket.on('data', (data) => { | ||
current_data += data | ||
let results | ||
while ((results = line_regexp.exec(current_data))) { | ||
const this_line = results[1] | ||
current_data = current_data.slice(this_line.length) | ||
socket.emit('line', this_line) | ||
} | ||
} | ||
} | ||
}) | ||
exports.get_implicit_mx = async (domain) => { | ||
// console.log(`No MX for ${domain}, trying AAAA & A records`) | ||
const promises = [dns.resolve6(domain), dns.resolve4(domain)] | ||
const r = await Promise.allSettled(promises) | ||
return r | ||
.filter((a) => a.status === 'fulfilled') | ||
.flatMap((a) => | ||
a.value.map((ip) => ({ priority: 0, exchange: ip, from_dns: domain })), | ||
) | ||
} | ||
exports.resolve_mx_hosts = async (mxes) => { | ||
// for the given list of MX exchanges, resolve the hostnames to IPs | ||
const promises = [] | ||
for (const mx of mxes) { | ||
if (!mx.exchange) { | ||
promises.push(mx) | ||
continue | ||
socket.on('end', () => { | ||
if (current_data.length) { | ||
socket.emit('line', current_data) | ||
} | ||
if (net.isIP(mx.exchange)) { | ||
promises.push(mx) // already resolved | ||
continue | ||
} | ||
// resolve AAAA and A since mx.exchange is a hostname | ||
promises.push( | ||
dns | ||
.resolve6(mx.exchange) | ||
.then((ips) => | ||
ips.map((ip) => ({ ...mx, exchange: ip, from_dns: mx.exchange })), | ||
), | ||
) | ||
promises.push( | ||
dns | ||
.resolve4(mx.exchange) | ||
.then((ips) => | ||
ips.map((ip) => ({ ...mx, exchange: ip, from_dns: mx.exchange })), | ||
), | ||
) | ||
} | ||
const settled = await Promise.allSettled(promises) | ||
return settled.filter((s) => s.status === 'fulfilled').flatMap((s) => s.value) | ||
current_data = '' | ||
}) | ||
} |
{ | ||
"name": "haraka-net-utils", | ||
"version": "1.6.0", | ||
"version": "1.7.0", | ||
"description": "haraka network utilities", | ||
"main": "index.js", | ||
"files": [ | ||
"CHANGELOG.md" | ||
"CHANGELOG.md", | ||
"lib" | ||
], | ||
@@ -39,8 +40,8 @@ "scripts": { | ||
"devDependencies": { | ||
"@haraka/eslint-config": "^1.1.3" | ||
"@haraka/eslint-config": "^1.1.5" | ||
}, | ||
"dependencies": { | ||
"haraka-config": "^1.1.0", | ||
"haraka-config": "^1.2.4", | ||
"haraka-tld": "^1.2.1", | ||
"ipaddr.js": "^2.1.0", | ||
"ipaddr.js": "^2.2.0", | ||
"punycode.js": "^2.3.1", | ||
@@ -47,0 +48,0 @@ "openssl-wrapper": "^0.3.4", |
@@ -123,2 +123,46 @@ [![CI][ci-img]][ci-url] | ||
### HarakaMx | ||
An object class representing a MX. HarakaMx objects may contain the following properties: | ||
```js | ||
{ | ||
exchange: '', // required: a FQDN or IP address | ||
path: '', // the file path to a socket | ||
priority: 0, // integer, a MX priority. | ||
port: 25, // integer: an alternate port | ||
bind: '', // an outbound IP address to bind to | ||
bind_helo: '', // an outbound helo hostname | ||
using_lmtp: false, // boolean, specify LMTP delivery | ||
auth_user: '', // an AUTH username (required if AUTH is desired) | ||
auth_pass: '', // an AUTH password (required if AUTH is desired) | ||
auth_type: '', // an AUTH type that should be used with the MX. | ||
from_dns: '', // the DNS name from which the MX was queried | ||
} | ||
``` | ||
Create a HarakaMx object in The Usual Way: | ||
```js | ||
const nu = require('haraka-net-utils') | ||
const myMx = new nu.HarakaMx(parameter) | ||
``` | ||
The parameter can be one of: | ||
- A string in any of the following formats: | ||
- hostname | ||
- hostname:port | ||
- IPv4 | ||
- IPv4:port | ||
- [IPv6] | ||
- [IPv6]: port | ||
- A [URL](https://nodejs.org/docs/latest-v20.x/api/url.html) string | ||
- smtp://mail.example.com:25 | ||
- lmtp://int-mail.example.com:24 | ||
- smtp://user:pass@host.example.com:587 | ||
- An object, containing at least an exchange, and any of the other properties listed at the top of this section. | ||
An optional second parameter is an alias for from_dns. | ||
[ci-img]: https://github.com/haraka/haraka-net-utils/actions/workflows/ci.yml/badge.svg | ||
@@ -125,0 +169,0 @@ [ci-url]: https://github.com/haraka/haraka-net-utils/actions/workflows/ci.yml |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
33023
8
585
175
0
1
Updatedharaka-config@^1.2.4
Updatedipaddr.js@^2.2.0