Add http agent, update keys to ensure string, allow async promise executors.
This commit is contained in:
parent
552fc41c5e
commit
053f1e6fa1
3 changed files with 130 additions and 5 deletions
|
@ -67,6 +67,9 @@
|
|||
],
|
||||
"jsdoc/require-param-description": [
|
||||
"off"
|
||||
],
|
||||
"no-async-promise-executor": [
|
||||
"off"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
122
lib/http_agent.js
Normal file
122
lib/http_agent.js
Normal 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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
10
lib/keys.js
10
lib/keys.js
|
@ -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()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue