2022-07-19 01:24:04 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const { validateSvg } = require('mailauth/lib/bimi/validate-svg');
|
|
|
|
const { vmc } = require('@postalsys/vmc');
|
|
|
|
const { formatDomain, getAlignment } = require('mailauth/lib/tools');
|
2022-11-03 19:50:17 +08:00
|
|
|
const { bimi: bimiLookup } = require('mailauth/lib/bimi');
|
2022-07-19 04:03:56 +08:00
|
|
|
const crypto = require('crypto');
|
2022-12-15 22:13:28 +08:00
|
|
|
const log = require('npmlog');
|
2022-07-19 01:24:04 +08:00
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
const FETCH_TIMEOUT = 5 * 1000;
|
|
|
|
|
|
|
|
// Use fake User-Agent to pass UA checks for Akamai
|
|
|
|
const USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0';
|
|
|
|
|
|
|
|
const { fetch: fetchCmd, Agent } = require('undici');
|
|
|
|
const fetchAgent = new Agent({ connect: { timeout: FETCH_TIMEOUT } });
|
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
class BimiHandler {
|
|
|
|
static create(options = {}) {
|
|
|
|
return new BimiHandler(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(options) {
|
|
|
|
this.options = options || {};
|
|
|
|
|
|
|
|
this.database = options.database;
|
2022-09-20 16:25:44 +08:00
|
|
|
this.loggelf = options.loggelf || (() => false);
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
2022-09-20 16:25:44 +08:00
|
|
|
async download(url, bimiDocument, bimiType, bimiDomain) {
|
2022-07-19 01:24:04 +08:00
|
|
|
if (!url) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocument = bimiDocument || {};
|
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
const parsedUrl = new URL(url);
|
|
|
|
|
|
|
|
switch (parsedUrl.protocol) {
|
|
|
|
case 'https:':
|
|
|
|
break;
|
2023-08-24 21:06:35 +08:00
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
case 'http:': {
|
|
|
|
let error = new Error(`Only HTTPS addresses are allowed`);
|
|
|
|
error.code = 'PROTO_NOT_HTTPS';
|
2022-07-19 18:26:42 +08:00
|
|
|
|
|
|
|
error.source = 'pre-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
2023-08-24 21:06:35 +08:00
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
default: {
|
|
|
|
let error = new Error(`Unknown protocol ${parsedUrl.protocol}`);
|
|
|
|
error.code = 'UNKNOWN_PROTO';
|
2022-07-19 18:26:42 +08:00
|
|
|
|
|
|
|
error.source = 'pre-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
const headers = {
|
2023-08-24 21:06:35 +08:00
|
|
|
// Comment: AKAMAI does some strange UA based filtering that messes up the request
|
|
|
|
// 'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage}`
|
|
|
|
'User-Agent': USER_AGENT
|
2022-07-19 18:26:42 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (bimiDocument.etag) {
|
|
|
|
headers['If-None-Match'] = bimiDocument.etag;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bimiDocument.lastModified) {
|
|
|
|
headers['If-Modified-Since'] = bimiDocument.lastModified;
|
|
|
|
}
|
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
let res = await fetchCmd(parsedUrl, {
|
2022-07-19 18:26:42 +08:00
|
|
|
headers,
|
2023-08-24 21:06:35 +08:00
|
|
|
redirect: 'manual',
|
|
|
|
dispatcher: fetchAgent
|
|
|
|
});
|
2022-07-19 01:24:04 +08:00
|
|
|
|
2023-08-24 21:32:50 +08:00
|
|
|
if (res.status === 304) {
|
|
|
|
// no changes
|
|
|
|
let err = new Error('No changes');
|
|
|
|
err.code = 'NO_CHANGES';
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
if (!res.ok) {
|
|
|
|
let error = new Error(`Request failed with status ${res.status}`);
|
|
|
|
error.code = 'HTTP_REQUEST_FAILED';
|
|
|
|
|
|
|
|
this.loggelf({
|
|
|
|
short_message: `[BIMI FETCH] ${url}`,
|
|
|
|
_mail_action: 'bimi_fetch',
|
|
|
|
_bimi_url: url,
|
|
|
|
_bimi_type: bimiType,
|
|
|
|
_bimi_domain: bimiDomain,
|
|
|
|
_req_etag: bimiDocument.etag,
|
|
|
|
_req_last_modified: bimiDocument.lastModified,
|
|
|
|
_failure: 'yes',
|
|
|
|
_error: error.message,
|
|
|
|
_err_code: error.code
|
|
|
|
});
|
2022-09-20 16:25:44 +08:00
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
throw error;
|
|
|
|
}
|
2022-07-19 18:26:42 +08:00
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
const arrayBufferValue = await res.arrayBuffer();
|
|
|
|
const content = Buffer.from(arrayBufferValue);
|
|
|
|
|
|
|
|
this.loggelf({
|
|
|
|
short_message: `[BIMI FETCH] ${url}`,
|
|
|
|
_mail_action: 'bimi_fetch',
|
|
|
|
_bimi_url: url,
|
|
|
|
_bimi_type: bimiType,
|
|
|
|
_bimi_domain: bimiDomain,
|
2023-08-24 21:32:50 +08:00
|
|
|
_status_code: res.status,
|
2023-08-24 21:06:35 +08:00
|
|
|
_req_etag: bimiDocument.etag,
|
|
|
|
_req_last_modified: bimiDocument.lastModified,
|
2023-08-24 21:32:50 +08:00
|
|
|
_res_etag: res.headers.get('ETag'),
|
|
|
|
_res_last_modified: res.headers.get('Last-Modified')
|
2023-08-24 21:06:35 +08:00
|
|
|
});
|
2022-07-19 01:24:04 +08:00
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
if (!res.status || res.status < 200 || res.status >= 300) {
|
|
|
|
let err = new Error(`Invalid response code ${res.status || '-'}`);
|
2022-09-20 16:25:44 +08:00
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
if (res.headers.get('Location') && res.status >= 300 && res.status < 400) {
|
|
|
|
err.code = 'REDIRECT_NOT_ALLOWED';
|
|
|
|
} else {
|
|
|
|
err.code = 'HTTP_STATUS_' + (res.status || 'NA');
|
|
|
|
}
|
2022-07-19 01:24:04 +08:00
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
err.details = err.details || {
|
|
|
|
code: res.status,
|
|
|
|
url,
|
|
|
|
etag: bimiDocument.etag,
|
|
|
|
lastModified: bimiDocument.lastModified,
|
|
|
|
location: res.headers.get('Location')
|
|
|
|
};
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
content,
|
|
|
|
etag: res.headers.get('ETag'),
|
|
|
|
lastModified: res.headers.get('Last-Modified')
|
|
|
|
};
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
2022-09-20 16:25:44 +08:00
|
|
|
async getBimiData(url, type, bimiDomain) {
|
2022-07-19 01:24:04 +08:00
|
|
|
if (!url) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-05-25 18:31:14 +08:00
|
|
|
const now = new Date();
|
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
let bimiDocument = await this.database.collection('bimi').findOne({ url, type });
|
|
|
|
|
|
|
|
let bimiTtl = bimiDocument?.error ? 1 * 3600 * 1000 : 24 * 3600 * 1000;
|
|
|
|
|
2023-05-25 18:31:14 +08:00
|
|
|
if (bimiDocument && bimiDocument?.updated > new Date(now.getTime() - bimiTtl)) {
|
|
|
|
if (
|
|
|
|
bimiDocument.error &&
|
|
|
|
// ignore errors if a valid VMC is cached
|
|
|
|
!(type === 'authority' && bimiDocument?.vmc?.certificate?.validTo && new Date(bimiDocument?.vmc?.certificate?.validTo) >= now)
|
|
|
|
) {
|
2022-07-19 01:24:04 +08:00
|
|
|
let error = new Error(bimiDocument.error.message);
|
|
|
|
if (bimiDocument.error.details) {
|
|
|
|
error.details = bimiDocument.error.details;
|
|
|
|
}
|
|
|
|
if (bimiDocument.error.code) {
|
|
|
|
error.code = bimiDocument.error.code;
|
|
|
|
}
|
2022-07-19 18:26:42 +08:00
|
|
|
|
|
|
|
error.source = 'db';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
2022-07-19 04:03:56 +08:00
|
|
|
if (bimiDocument?.content?.buffer) {
|
|
|
|
bimiDocument.content = bimiDocument.content.buffer;
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocument.source = 'db';
|
2023-05-25 18:31:14 +08:00
|
|
|
bimiDocument.error = null; // override existing error if using a cached valid VMC
|
2022-07-19 01:24:04 +08:00
|
|
|
return bimiDocument;
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
let bimiDocumentUpdate = {
|
2023-05-25 18:31:14 +08:00
|
|
|
updated: now
|
2022-07-19 01:24:04 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
// Step 1. Download
|
|
|
|
|
|
|
|
let file;
|
|
|
|
try {
|
2022-09-20 16:25:44 +08:00
|
|
|
let { content, etag, lastModified } = await this.download(url, bimiDocument, type, bimiDomain);
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocumentUpdate.etag = etag || null;
|
|
|
|
bimiDocumentUpdate.lastModified = lastModified || null;
|
2022-07-19 01:24:04 +08:00
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
file = content;
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code === 'NO_CHANGES') {
|
2023-05-25 18:31:14 +08:00
|
|
|
if (bimiDocument?.content?.buffer && bimiDocument.error?.type === 'download') {
|
|
|
|
// download failed last time, so run validations again
|
|
|
|
file = bimiDocument.content.buffer;
|
|
|
|
} else {
|
|
|
|
// existing document is good enough, proceed to checkout
|
|
|
|
let r = await this.database.collection('bimi').findOneAndUpdate(
|
|
|
|
{
|
2022-07-19 18:26:42 +08:00
|
|
|
type,
|
2023-05-25 18:31:14 +08:00
|
|
|
url
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$set: bimiDocumentUpdate,
|
|
|
|
$setOnInsert: {
|
|
|
|
url,
|
|
|
|
type,
|
|
|
|
created: new Date()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ upsert: true, returnDocument: 'after' }
|
|
|
|
);
|
2022-07-19 18:26:42 +08:00
|
|
|
|
2023-05-25 18:31:14 +08:00
|
|
|
let updatedBimiDocument = r?.value;
|
|
|
|
if (updatedBimiDocument?.content?.buffer) {
|
|
|
|
updatedBimiDocument.content = updatedBimiDocument.content.buffer;
|
2022-07-19 18:26:42 +08:00
|
|
|
}
|
2023-05-25 18:31:14 +08:00
|
|
|
|
|
|
|
if (bimiDocument.error) {
|
|
|
|
let error = new Error(bimiDocument.error.message);
|
|
|
|
if (bimiDocument.error.details) {
|
|
|
|
error.details = bimiDocument.error.details;
|
|
|
|
}
|
|
|
|
if (bimiDocument.error.code) {
|
|
|
|
error.code = bimiDocument.error.code;
|
|
|
|
}
|
|
|
|
|
|
|
|
error.source = 'cache-hit';
|
|
|
|
throw error;
|
2022-07-19 18:26:42 +08:00
|
|
|
}
|
|
|
|
|
2023-05-25 18:31:14 +08:00
|
|
|
updatedBimiDocument.source = 'cache-hit';
|
|
|
|
return updatedBimiDocument;
|
2022-07-19 18:26:42 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
bimiDocumentUpdate.error = {
|
|
|
|
message: err.message,
|
|
|
|
details: err.details,
|
2023-05-25 18:31:14 +08:00
|
|
|
code: err.code,
|
|
|
|
type: 'download',
|
|
|
|
time: now
|
2022-07-19 18:26:42 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.database.collection('bimi').updateOne(
|
|
|
|
{
|
|
|
|
url,
|
|
|
|
type
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$set: bimiDocumentUpdate,
|
|
|
|
$setOnInsert: {
|
|
|
|
type,
|
|
|
|
url,
|
|
|
|
created: new Date()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ upsert: true }
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
|
|
|
|
err.source = 'post-request';
|
|
|
|
throw err;
|
|
|
|
}
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Step 2. Validate VMC
|
|
|
|
if (type === 'authority') {
|
|
|
|
try {
|
|
|
|
let vmcData = await vmc(file);
|
|
|
|
|
|
|
|
if (!vmcData.logoFile) {
|
|
|
|
let error = new Error('VMC does not contain a logo file');
|
|
|
|
error.code = 'MISSING_VMC_LOGO';
|
2022-07-19 18:26:42 +08:00
|
|
|
|
|
|
|
error.source = 'post-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (vmcData?.mediaType?.toLowerCase() !== 'image/svg+xml') {
|
|
|
|
let error = new Error('Invalid media type for the logo file');
|
|
|
|
error.details = {
|
|
|
|
mediaType: vmcData.mediaType
|
|
|
|
};
|
|
|
|
error.code = 'INVALID_MEDIATYPE';
|
2022-07-19 18:26:42 +08:00
|
|
|
|
|
|
|
error.source = 'post-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!vmcData.validHash) {
|
|
|
|
let error = new Error('VMC hash does not match logo file');
|
|
|
|
error.details = {
|
|
|
|
hashAlgo: vmcData.hashAlgo,
|
|
|
|
hashValue: vmcData.hashValue,
|
|
|
|
logoFile: vmcData.logoFile
|
|
|
|
};
|
|
|
|
error.code = 'INVALID_LOGO_HASH';
|
2022-07-19 18:26:42 +08:00
|
|
|
|
|
|
|
error.source = 'post-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocumentUpdate.content = Buffer.from(vmcData.logoFile, 'base64');
|
|
|
|
bimiDocumentUpdate.vmc = vmcData;
|
2022-07-19 01:24:04 +08:00
|
|
|
} catch (err) {
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocumentUpdate.error = {
|
2022-07-19 01:24:04 +08:00
|
|
|
message: err.message,
|
|
|
|
details: err.details,
|
2023-05-25 18:31:14 +08:00
|
|
|
code: err.code,
|
|
|
|
type: 'vmc',
|
|
|
|
time: now
|
2022-07-19 01:24:04 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.database.collection('bimi').updateOne(
|
|
|
|
{
|
|
|
|
type,
|
|
|
|
url
|
|
|
|
},
|
|
|
|
{
|
2022-07-19 18:26:42 +08:00
|
|
|
$set: bimiDocumentUpdate,
|
2022-07-19 01:24:04 +08:00
|
|
|
$setOnInsert: {
|
2022-07-19 18:26:42 +08:00
|
|
|
type,
|
|
|
|
url,
|
2022-07-19 01:24:04 +08:00
|
|
|
created: new Date()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ upsert: true }
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
err.source = err.source || 'post-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
} else {
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocumentUpdate.content = file;
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Step 3. Validate SVG
|
|
|
|
|
|
|
|
try {
|
2022-07-19 18:26:42 +08:00
|
|
|
validateSvg(bimiDocumentUpdate.content);
|
2022-07-19 01:24:04 +08:00
|
|
|
} catch (err) {
|
|
|
|
let error = new Error('VMC logo SVG validation failed');
|
|
|
|
error.details = Object.assign(
|
|
|
|
{
|
|
|
|
message: err.message
|
|
|
|
},
|
|
|
|
error.details || {},
|
|
|
|
err.code ? { code: err.code } : {}
|
|
|
|
);
|
|
|
|
error.code = 'SVG_VALIDATION_FAILED';
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocumentUpdate.error = {
|
2022-07-19 01:24:04 +08:00
|
|
|
message: error.message,
|
|
|
|
details: error.details,
|
2023-05-25 18:31:14 +08:00
|
|
|
code: error.code,
|
|
|
|
type: 'svg',
|
|
|
|
time: now
|
2022-07-19 01:24:04 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.database.collection('bimi').updateOne(
|
|
|
|
{
|
|
|
|
type,
|
|
|
|
url
|
|
|
|
},
|
|
|
|
{
|
2022-07-19 18:26:42 +08:00
|
|
|
$set: bimiDocumentUpdate,
|
2022-07-19 01:24:04 +08:00
|
|
|
$setOnInsert: {
|
2022-07-19 18:26:42 +08:00
|
|
|
type,
|
|
|
|
url,
|
2022-07-19 01:24:04 +08:00
|
|
|
created: new Date()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ upsert: true }
|
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
error.source = 'post-request';
|
2022-07-19 01:24:04 +08:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
|
|
|
// clear pending errors
|
2022-07-19 18:26:42 +08:00
|
|
|
bimiDocumentUpdate.error = null;
|
2022-07-19 01:24:04 +08:00
|
|
|
|
|
|
|
let r = await this.database.collection('bimi').findOneAndUpdate(
|
|
|
|
{
|
|
|
|
type,
|
|
|
|
url
|
|
|
|
},
|
|
|
|
{
|
2022-07-19 18:26:42 +08:00
|
|
|
$set: bimiDocumentUpdate,
|
2022-07-19 01:24:04 +08:00
|
|
|
$setOnInsert: {
|
2022-07-19 18:26:42 +08:00
|
|
|
type,
|
|
|
|
url,
|
2022-07-19 01:24:04 +08:00
|
|
|
created: new Date()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{ upsert: true, returnDocument: 'after' }
|
|
|
|
);
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
let updatedBimiDocument = r?.value;
|
2022-07-19 04:03:56 +08:00
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
if (updatedBimiDocument?.content?.buffer) {
|
|
|
|
updatedBimiDocument.content = updatedBimiDocument.content.buffer;
|
2022-07-19 04:03:56 +08:00
|
|
|
}
|
|
|
|
|
2022-07-19 18:26:42 +08:00
|
|
|
updatedBimiDocument.source = 'new';
|
|
|
|
return updatedBimiDocument;
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
async getInfo(bimiData) {
|
|
|
|
let [
|
|
|
|
{ reason: locationError, value: locationValue, status: locationStatus },
|
|
|
|
{ reason: authorityError, value: authorityValue, status: authorityStatus }
|
2022-09-20 16:25:44 +08:00
|
|
|
] = await Promise.allSettled([
|
|
|
|
this.getBimiData(bimiData.location, 'location', bimiData.status?.header?.d),
|
|
|
|
this.getBimiData(bimiData.authority, 'authority', bimiData.status?.header?.d)
|
|
|
|
]);
|
2022-07-19 01:24:04 +08:00
|
|
|
|
|
|
|
if (authorityError) {
|
2022-07-19 18:26:42 +08:00
|
|
|
throw authorityError;
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (authorityStatus === 'fulfilled' && authorityValue) {
|
|
|
|
let selector = bimiData.status?.header?.selector;
|
|
|
|
let d = bimiData.status?.header?.d;
|
|
|
|
|
|
|
|
// validate domain
|
|
|
|
let selectorSet = [];
|
|
|
|
let domainSet = [];
|
|
|
|
authorityValue.vmc?.certificate?.subjectAltName?.map(formatDomain)?.forEach(domain => {
|
|
|
|
if (/\b_bimi\./.test(domain)) {
|
|
|
|
selectorSet.push(domain);
|
|
|
|
} else {
|
|
|
|
domainSet.push(domain);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let domainVerified = false;
|
|
|
|
|
|
|
|
if (selector && selectorSet.includes(formatDomain(`${selector}._bimi.${d}`))) {
|
|
|
|
domainVerified = true;
|
|
|
|
} else {
|
|
|
|
let alignedDomain = getAlignment(d, domainSet, false);
|
|
|
|
if (alignedDomain) {
|
|
|
|
domainVerified = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!domainVerified) {
|
|
|
|
let error = new Error('Domain can not be verified');
|
|
|
|
error.details = {
|
|
|
|
subjectAltName: authorityValue.vmc?.certificate?.subjectAltName,
|
|
|
|
selector,
|
|
|
|
d
|
|
|
|
};
|
|
|
|
error.code = 'VMC_DOMAIN_MISMATCH';
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
|
2022-07-19 04:03:56 +08:00
|
|
|
if (locationStatus === 'fulfilled' && locationValue?.content && authorityValue.vmc?.hashAlgo && authorityValue.vmc?.validHash) {
|
|
|
|
let hash = crypto
|
|
|
|
.createHash(authorityValue.vmc.hashAlgo)
|
|
|
|
//sss
|
|
|
|
.update(locationValue.content)
|
|
|
|
.digest('hex');
|
|
|
|
if (hash === authorityValue.vmc.hashValue) {
|
|
|
|
// logo files match, so location URL is safe to use
|
|
|
|
authorityValue.locationUrl = bimiData.location;
|
|
|
|
} else {
|
2022-12-15 22:13:28 +08:00
|
|
|
log.info(
|
|
|
|
'BIMI',
|
|
|
|
'Logo files from l= and a= do not match lh=%s ah=%s algo=%s d=%s',
|
|
|
|
hash,
|
|
|
|
authorityValue.vmc.hashValue,
|
|
|
|
authorityValue.vmc.hashAlgo,
|
|
|
|
d
|
|
|
|
);
|
|
|
|
authorityValue.locationUrl = bimiData.location;
|
2022-07-19 04:03:56 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
return authorityValue;
|
|
|
|
}
|
|
|
|
|
2022-08-18 03:13:02 +08:00
|
|
|
// If signed VMC was ok, then ignore any errors from regular SVG as this would not be used anyway
|
|
|
|
if (locationError) {
|
|
|
|
throw locationError;
|
|
|
|
}
|
|
|
|
|
2022-07-19 01:24:04 +08:00
|
|
|
return locationStatus === 'fulfilled' && locationValue;
|
|
|
|
}
|
2022-11-03 19:50:17 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper method to fetch BIMI info for a domain name and selector
|
|
|
|
* @param {String} domain
|
|
|
|
* @param {String} [selector]
|
|
|
|
* @returns {Object} BIMI record
|
|
|
|
*/
|
|
|
|
async fetchByDomain(domain, selector) {
|
|
|
|
const bimiVerificationResults = await bimiLookup({
|
|
|
|
dmarc: {
|
|
|
|
status: {
|
|
|
|
result: 'pass',
|
|
|
|
header: {
|
|
|
|
from: domain
|
|
|
|
}
|
|
|
|
},
|
|
|
|
domain,
|
|
|
|
policy: 'reject'
|
|
|
|
},
|
|
|
|
|
|
|
|
headers: {
|
|
|
|
parsed:
|
|
|
|
selector && selector !== 'default'
|
|
|
|
? [
|
|
|
|
{
|
|
|
|
key: 'bimi-selector',
|
|
|
|
line: `v=BIMI1; s=${selector}`
|
|
|
|
}
|
|
|
|
]
|
|
|
|
: []
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return await this.getInfo(bimiVerificationResults);
|
|
|
|
}
|
2022-07-19 01:24:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = BimiHandler;
|
|
|
|
|
2023-08-24 21:08:53 +08:00
|
|
|
/*
|
2022-07-19 01:24:04 +08:00
|
|
|
const db = require('./db');
|
|
|
|
db.connect(() => {
|
|
|
|
let bimi = BimiHandler.create({
|
|
|
|
database: db.database
|
|
|
|
});
|
|
|
|
|
2023-08-24 21:06:35 +08:00
|
|
|
let zoneBimi = {
|
2022-07-19 01:24:04 +08:00
|
|
|
status: {
|
|
|
|
header: {
|
|
|
|
selector: 'default',
|
|
|
|
d: 'zone.ee'
|
|
|
|
},
|
|
|
|
result: 'pass'
|
|
|
|
},
|
|
|
|
rr: 'v=BIMI1; l=https://zone.ee/common/img/zone_profile_square_bimi.svg;a=https://zone.ee/.well-known/bimi.pem',
|
|
|
|
location: 'https://zone.ee/common/img/zone_profile_square_bimi.svg',
|
|
|
|
authority: 'https://zone.ee/.well-known/bimi.pem',
|
|
|
|
info: 'bimi=pass header.selector=default header.d=zone.ee'
|
2023-08-24 21:06:35 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
zoneBimi = {
|
|
|
|
status: {
|
|
|
|
header: {
|
|
|
|
selector: 'default',
|
|
|
|
d: 'ups.com'
|
|
|
|
},
|
|
|
|
result: 'pass'
|
|
|
|
},
|
|
|
|
rr: 'v=BIMI1; l=https://www.ups.com/assets/resources/bimi/ups_bimi_logo.svg; a=https://www.ups.com/assets/resources/bimi/ups_bimi_vmc.pem;',
|
|
|
|
location: 'https://www.ups.com/assets/resources/bimi/ups_bimi_logo.svg',
|
|
|
|
authority: 'https://www.ups.com/assets/resources/bimi/ups_bimi_vmc.pem',
|
|
|
|
info: 'bimi=pass header.selector=default header.d=ups.com'
|
|
|
|
};
|
|
|
|
|
|
|
|
bimi.getInfo(zoneBimi)
|
2022-07-19 01:24:04 +08:00
|
|
|
.then(result => console.log(require('util').inspect(result, false, 22)))
|
|
|
|
.catch(err => console.error(err))
|
|
|
|
.finally(() => process.exit());
|
|
|
|
});
|
2023-08-24 21:08:53 +08:00
|
|
|
*/
|