|
1 | 1 | 'use strict'; |
2 | 2 |
|
| 3 | +const crypto = require('crypto'); |
3 | 4 | const dns = require('dns'); |
4 | 5 | const { formatAuthHeaderRow, parseDkimHeaders } = require('../tools'); |
5 | 6 | const Joi = require('joi'); |
| 7 | +const packageData = require('../../package.json'); |
6 | 8 | const httpsSchema = Joi.string().uri({ |
7 | 9 | scheme: ['https'] |
8 | 10 | }); |
9 | 11 |
|
| 12 | +const https = require('https'); |
| 13 | +const http = require('http'); |
| 14 | +const { vmc } = require('@postalsys/vmc'); |
| 15 | + |
10 | 16 | const lookup = async data => { |
11 | 17 | let { dmarc, headers, resolver } = data; |
12 | 18 | let headerRows = (headers && headers.parsed) || []; |
@@ -161,4 +167,171 @@ const lookup = async data => { |
161 | 167 | return response; |
162 | 168 | }; |
163 | 169 |
|
164 | | -module.exports = { bimi: lookup }; |
| 170 | +const downloadPromise = (url, cachedFile) => { |
| 171 | + if (cachedFile) { |
| 172 | + return cachedFile; |
| 173 | + } |
| 174 | + |
| 175 | + if (!url) { |
| 176 | + return false; |
| 177 | + } |
| 178 | + |
| 179 | + const parsedUrl = new URL(url); |
| 180 | + |
| 181 | + const options = { |
| 182 | + protocol: parsedUrl.protocol, |
| 183 | + host: parsedUrl.host, |
| 184 | + headers: { |
| 185 | + host: parsedUrl.host, |
| 186 | + 'User-Agent': `mailauth/${packageData.version} (+${packageData.homepage}` |
| 187 | + }, |
| 188 | + servername: parsedUrl.hostname, |
| 189 | + port: 443, |
| 190 | + path: parsedUrl.pathname, |
| 191 | + method: 'GET', |
| 192 | + rejectUnauthorized: true |
| 193 | + }; |
| 194 | + |
| 195 | + return new Promise((resolve, reject) => { |
| 196 | + let protoHandler; |
| 197 | + switch (parsedUrl.protocol) { |
| 198 | + case 'https:': |
| 199 | + protoHandler = https; |
| 200 | + break; |
| 201 | + case 'http:': |
| 202 | + protoHandler = http; |
| 203 | + break; |
| 204 | + default: |
| 205 | + reject(new Error(`Unknown protocol ${parsedUrl.protocol}`)); |
| 206 | + } |
| 207 | + const req = protoHandler.request(options, res => { |
| 208 | + let chunks = [], |
| 209 | + chunklen = 0; |
| 210 | + res.on('readable', () => { |
| 211 | + let chunk; |
| 212 | + while ((chunk = res.read()) !== null) { |
| 213 | + chunks.push(chunk); |
| 214 | + chunklen += chunk.length; |
| 215 | + } |
| 216 | + }); |
| 217 | + res.on('end', () => { |
| 218 | + let data = Buffer.concat(chunks, chunklen); |
| 219 | + if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) { |
| 220 | + let err = new Error(`Invalid response code ${res.statusCode || '-'}`); |
| 221 | + err.code = 'http_status_' + (res.statusCode || 'na'); |
| 222 | + if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) { |
| 223 | + err.redirect = { |
| 224 | + code: res.statusCode, |
| 225 | + location: res.headers.location |
| 226 | + }; |
| 227 | + } |
| 228 | + return reject(err); |
| 229 | + } |
| 230 | + resolve(data); |
| 231 | + }); |
| 232 | + res.on('error', err => reject(err)); |
| 233 | + }); |
| 234 | + |
| 235 | + req.on('error', err => { |
| 236 | + reject(err); |
| 237 | + }); |
| 238 | + req.end(); |
| 239 | + }); |
| 240 | +}; |
| 241 | + |
| 242 | +const validateVMC = async bimiData => { |
| 243 | + if (!bimiData) { |
| 244 | + return false; |
| 245 | + } |
| 246 | + |
| 247 | + let promises = []; |
| 248 | + |
| 249 | + promises.push(downloadPromise(bimiData.location, bimiData.locationFile)); |
| 250 | + promises.push(downloadPromise(bimiData.authority, bimiData.authorityFile)); |
| 251 | + |
| 252 | + if (!promises.length) { |
| 253 | + return false; |
| 254 | + } |
| 255 | + |
| 256 | + let results = await Promise.allSettled(promises); |
| 257 | + |
| 258 | + let result = {}; |
| 259 | + if (results[0].value || results[0].reason) { |
| 260 | + result.location = { |
| 261 | + url: bimiData.location, |
| 262 | + success: results[0].status === 'fulfilled' |
| 263 | + }; |
| 264 | + |
| 265 | + if (results[0].reason) { |
| 266 | + let err = results[0].reason; |
| 267 | + result.location.error = { message: err.message }; |
| 268 | + if (err.redirect) { |
| 269 | + result.location.error.redirect = err.redirect; |
| 270 | + } |
| 271 | + if (err.code) { |
| 272 | + result.location.error.code = err.code; |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + if (result.location.success) { |
| 277 | + result.location.logoFile = results[0].value.toString('base64'); |
| 278 | + } |
| 279 | + } |
| 280 | + |
| 281 | + if (results[1].value || results[1].reason) { |
| 282 | + result.authority = { |
| 283 | + url: bimiData.authority, |
| 284 | + success: results[1].status === 'fulfilled' |
| 285 | + }; |
| 286 | + |
| 287 | + if (results[1].reason) { |
| 288 | + let err = results[1].reason; |
| 289 | + result.authority.error = { message: err.message }; |
| 290 | + if (err.redirect) { |
| 291 | + result.authority.error.redirect = err.redirect; |
| 292 | + } |
| 293 | + if (err.code) { |
| 294 | + result.authority.error.code = err.code; |
| 295 | + } |
| 296 | + } |
| 297 | + |
| 298 | + if (results[1].value) { |
| 299 | + try { |
| 300 | + result.authority.vmc = await vmc(results[1].value); |
| 301 | + } catch (err) { |
| 302 | + result.authority.success = false; |
| 303 | + result.authority.error = { message: err.message }; |
| 304 | + if (err.details) { |
| 305 | + result.authority.error.details = err.details; |
| 306 | + } |
| 307 | + if (err.code) { |
| 308 | + result.authority.error.code = err.code; |
| 309 | + } |
| 310 | + } |
| 311 | + } |
| 312 | + |
| 313 | + if (result.location && result.location.success && result.authority.success) { |
| 314 | + try { |
| 315 | + if (result.location.success && result.authority.vmc.hashAlgo && result.authority.vmc.validHash) { |
| 316 | + let hash = crypto.createHash(result.authority.vmc.hashAlgo).update(results[0].value).digest('hex'); |
| 317 | + result.location.hashAlgo = result.authority.vmc.hashAlgo; |
| 318 | + result.location.hashValue = hash; |
| 319 | + result.authority.hashMatch = hash === result.authority.vmc.hashValue; |
| 320 | + } |
| 321 | + } catch (err) { |
| 322 | + result.authority.success = false; |
| 323 | + result.authority.error = { message: err.message }; |
| 324 | + if (err.details) { |
| 325 | + result.authority.error.details = err.details; |
| 326 | + } |
| 327 | + if (err.code) { |
| 328 | + result.authority.error.code = err.code; |
| 329 | + } |
| 330 | + } |
| 331 | + } |
| 332 | + } |
| 333 | + |
| 334 | + return result; |
| 335 | +}; |
| 336 | + |
| 337 | +module.exports = { bimi: lookup, validateVMC }; |
0 commit comments