Skip to content

Commit

Permalink
Use Fetch API for BIMI downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
andris9 committed Aug 24, 2023
1 parent f4a3111 commit b42748a
Showing 1 changed file with 96 additions and 97 deletions.
193 changes: 96 additions & 97 deletions lib/bimi-handler.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
'use strict';

const packageData = require('../package.json');
const https = require('https');
const { validateSvg } = require('mailauth/lib/bimi/validate-svg');
const { vmc } = require('@postalsys/vmc');
const { formatDomain, getAlignment } = require('mailauth/lib/tools');
const { bimi: bimiLookup } = require('mailauth/lib/bimi');
const crypto = require('crypto');
const log = require('npmlog');

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 } });

class BimiHandler {
static create(options = {}) {
return new BimiHandler(options);
Expand All @@ -30,18 +36,18 @@ class BimiHandler {

const parsedUrl = new URL(url);

let protoHandler;
switch (parsedUrl.protocol) {
case 'https:':
protoHandler = https;
break;

case 'http:': {
let error = new Error(`Only HTTPS addresses are allowed`);
error.code = 'PROTO_NOT_HTTPS';

error.source = 'pre-request';
throw error;
}

default: {
let error = new Error(`Unknown protocol ${parsedUrl.protocol}`);
error.code = 'UNKNOWN_PROTO';
Expand All @@ -52,8 +58,9 @@ class BimiHandler {
}

const headers = {
host: parsedUrl.host,
'User-Agent': `${packageData.name}/${packageData.version} (+${packageData.homepage}`
// 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
};

if (bimiDocument.etag) {
Expand All @@ -64,101 +71,79 @@ class BimiHandler {
headers['If-Modified-Since'] = bimiDocument.lastModified;
}

const options = {
protocol: parsedUrl.protocol,
host: parsedUrl.host,
let res = await fetchCmd(parsedUrl, {
headers,
servername: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname,
method: 'GET',
rejectUnauthorized: true
};
redirect: 'manual',
dispatcher: fetchAgent
});

return new Promise((resolve, reject) => {
const req = protoHandler.request(options, res => {
let chunks = [],
chunklen = 0;
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
});

res.on('readable', () => {
let chunk;
while ((chunk = res.read()) !== null) {
chunks.push(chunk);
chunklen += chunk.length;
}
});

res.on('end', () => {
let content = Buffer.concat(chunks, chunklen);

this.loggelf({
short_message: `[BIMI FETCH] ${url}`,
_mail_action: 'bimi_fetch',
_bimi_url: url,
_bimi_type: bimiType,
_bimi_domain: bimiDomain,
_status_code: res?.statusCode,
_req_etag: bimiDocument.etag,
_req_last_modified: bimiDocument.lastModified,
_res_etag: res?.headers?.etag,
_res_last_modified: res?.headers['last-modified']
});

if (res?.statusCode === 304) {
// no changes
let err = new Error('No changes');
err.code = 'NO_CHANGES';
return reject(err);
}
throw error;
}

if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
let err = new Error(`Invalid response code ${res.statusCode || '-'}`);
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,
_status_code: res?.status,
_req_etag: bimiDocument.etag,
_req_last_modified: bimiDocument.lastModified,
_res_etag: res?.headers?.get('ETag'),
_res_last_modified: res?.headers?.get('Last-Modified')
});

if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) {
err.code = 'REDIRECT_NOT_ALLOWED';
} else {
err.code = 'HTTP_STATUS_' + (res.statusCode || 'NA');
}
if (res?.status === 304) {
// no changes
let err = new Error('No changes');
err.code = 'NO_CHANGES';
throw err;
}

err.details = err.details || {
code: res.statusCode,
url,
etag: bimiDocument.etag,
lastModified: bimiDocument.lastModified,
location: res.headers?.location
};
if (!res.status || res.status < 200 || res.status >= 300) {
let err = new Error(`Invalid response code ${res.status || '-'}`);

return reject(err);
}
resolve({
content,
etag: res.headers.etag,
lastModified: res.headers['last-modified']
});
});
res.on('error', err => {
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: err.message,
_err_code: err.code
});

reject(err);
});
});
if (res.headers.get('Location') && res.status >= 300 && res.status < 400) {
err.code = 'REDIRECT_NOT_ALLOWED';
} else {
err.code = 'HTTP_STATUS_' + (res.status || 'NA');
}

req.on('error', err => {
reject(err);
});
req.end();
});
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')
};
}

async getBimiData(url, type, bimiDomain) {
Expand Down Expand Up @@ -562,14 +547,13 @@ class BimiHandler {

module.exports = BimiHandler;

/*
const db = require('./db');
db.connect(() => {
let bimi = BimiHandler.create({
database: db.database
});

bimi.getInfo({
let zoneBimi = {
status: {
header: {
selector: 'default',
Expand All @@ -581,9 +565,24 @@ db.connect(() => {
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'
})
};

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)
.then(result => console.log(require('util').inspect(result, false, 22)))

Check failure on line 585 in lib/bimi-handler.js

View workflow job for this annotation

GitHub Actions / test (20.x, ubuntu-20.04)

Unexpected require()

Check failure on line 585 in lib/bimi-handler.js

View workflow job for this annotation

GitHub Actions / test (16.x, ubuntu-20.04)

Unexpected require()

Check failure on line 585 in lib/bimi-handler.js

View workflow job for this annotation

GitHub Actions / test (18.x, ubuntu-20.04)

Unexpected require()

Check failure on line 585 in lib/bimi-handler.js

View workflow job for this annotation

GitHub Actions / test (20.x, ubuntu-20.04)

Unexpected require()
.catch(err => console.error(err))
.finally(() => process.exit());
});
*/

0 comments on commit b42748a

Please sign in to comment.