Add http agent, update keys to ensure string, allow async promise executors.

This commit is contained in:
Andrew Pietila 2023-03-30 08:39:16 -05:00
parent 552fc41c5e
commit 053f1e6fa1
3 changed files with 130 additions and 5 deletions

View file

@ -67,6 +67,9 @@
],
"jsdoc/require-param-description": [
"off"
],
"no-async-promise-executor": [
"off"
]
}
}

122
lib/http_agent.js Normal file
View file

@ -0,0 +1,122 @@
'use strict';
const crypto = require('crypto');
const https = require('https');
const http = require('http');
const zlib = require('zlib');
const { getKeyPair } = require('./keys');
const handleDataDecode = (requestUrl, options, res, resolve, reject, data) => {
if ( options && (!options["headers"] || !options["headers"]["accept-encoding"]) && res.headers["content-encoding"] && res.headers["content-encoding"].toLowerCase() === "br") {
delete res.headers["content-encoding"];
zlib.brotliDecompress(data, (error, result) => {
if ( error ) {
reject(error);
} else {
handleDataDecode(requestUrl, options, res, resolve, reject, result);
}
});
} else if ( options && (!options["headers"] || !options["headers"]["accept-encoding"]) && res.headers["content-encoding"] && res.headers["content-encoding"].toLowerCase() === "gzip") {
delete res.headers["content-encoding"];
zlib.gunzip(data, (error, result) => {
if ( error ) {
reject(error);
} else {
handleDataDecode(requestUrl, options, res, resolve, reject, result);
}
});
} else {
let isBuffer = true;
if ( (!options["headers"] || !options["headers"]["accept-encoding"]) && res.headers["content-type"] ) {
var charset = /.*charset=(\S+)/.exec(res.headers["content-type"])[1];
if ( charset.toLowerCase() === "iso-8859-1" ) {
charset = "latin1";
}
if ( charset ) {
isBuffer = false;
data = data.toString(charset);
}
}
resolve({headers: res.headers, data, isBuffer});
}
};
const handleCallback = (requestUrl, options, res, resolve, reject) => {
let data;
data = Buffer.alloc(0);
res.on('data', (chunk) => {
data = Buffer.concat([data, chunk]);
});
res.on('end', () => {
handleDataDecode(requestUrl, options, res, resolve, reject, data);
});
res.on('error', (error) => reject(error));
};
module.exports = {
request: (requestUrl, options) => {
options = options??{};
// TODO: Support following redirects.
options["follow-redirects"] = options["follow-redirects"]??true;
var headers = {"accept-encoding": "br, gzip", ...options["headers"]};
return new Promise(async (resolve, reject) => {
const parsedUrl = new URL(requestUrl);
var method = "GET";
if ( options["method"] ) {
method = options["method"];
}
var body = "";
if ( method === "POST" ) {
if ( typeof options["body"] === "string" ) {
body = options["body"];
} else if ( Buffer.isBuffer(options["body"]) ) {
body = options["body"];
} else if ( options["headers"]["content-type"].toLowerCase() === "application/ld+json" ||
options["headers"]["content-type"].toLowerCase() === "application/activity+json" ||
options["headers"]["content-type"].toLowerCase() === "application/json") {
body = JSON.stringify(options["body"]);
} else {
return reject(new Error("Unrecognized body content-type, passed as object."));
}
}
if ( body !== "" ) {
options["headers"]["content-length"] = Buffer.byteLength(body);
}
if ( options["actor"] ) {
var keyPair = getKeyPair("actor");
var rsaSha256Sign = crypto.createSign("RSA-SHA256");
headers["date"] = headers["date"]??(new Date()).toUTCString();
var toSign = `(request-target): ${method.toLowerCase()} ${parsedUrl.pathname}?${parsedUrl.search}\nhost: ${parsedUrl.host}\ndate: ${headers["date"]}`;
if ( method === "POST" ) {
var digest = crypto.createHash("sha256").update(body).end().digest("hex");
headers["digest"] = digest;
toSign = `${toSign}\ndigest: ${headers["digest"]}`;
}
headers["signature"] = `keyId=${options["actor"]}#main-key,headers="(request-target) host date${method === "post" ? " digest":""}",signature=${rsaSha256Sign.update(toSign).end().sign((await keyPair).privateKey, "base64")}`;
}
if ( parsedUrl.protocol.toLowerCase() === "https:" ) {
const httpsRequest = https.request(parsedUrl, {headers}, (res) => {
handleCallback(requestUrl, options, res, resolve, reject);
}).on('error', (error) => reject(error));
if ( body !== "" ) {
httpsRequest.write(body);
}
httpsRequest.end();
} else if ( parsedUrl.protocol.toLowerCase() === "http:" ) {
const httpRequest = http.request(parsedUrl, {headers}, (res) => {
handleCallback(requestUrl, options, res, resolve, reject);
}).on('error', (error) => reject(error));
if ( body !== "" ) {
httpRequest.write(body);
}
httpRequest.end();
} else {
reject("Unrecognized protocol.");
}
});
}
};

View file

@ -19,8 +19,8 @@ var getKeyPair = async (actor) => {
var result = await db('keys').where({actor}).andWhere('expiry', '>', (new Date()).getTime()/1000);
if ( result.length != 0 ) {
return {
publicKey: result[0].public,
privateKey: result[0].private
publicKey: result[0].public.toString(),
privateKey: result[0].private.toString()
};
} else {
var {publicKey, privateKey} = await generateKeyPair('rsa', {
@ -34,12 +34,12 @@ var getKeyPair = async (actor) => {
});
await db('keys').insert({
expiry: (new Date().setDate(new Date().getDate()+7).getTime()/1000),
public: publicKey,
private: privateKey,
public: publicKey.toString(),
private: privateKey.toString(),
actor: actor
});
return {
publicKey, privateKey
publicKey: publicKey.toString(), privateKey: publicKey.toString()
};
}
};