109 lines
3.4 KiB
JavaScript
109 lines
3.4 KiB
JavaScript
// Outbound signed peer requests.
|
|
//
|
|
// Build a canonical string, sign it with the peer's shared secret, and send
|
|
// it via fetch. Body is JSON-serialized once and used for both the HTTP body
|
|
// and the signature payload to keep client/server hash agreement trivial.
|
|
|
|
const {
|
|
buildCanonical,
|
|
normalizeHost,
|
|
sign,
|
|
randomNonce
|
|
} = require("./crypto");
|
|
|
|
|
|
const signedFetch = async ({ baseUrl, method, path, body, sourceEnvId, secret, requireTls }) => {
|
|
if (requireTls && new URL(baseUrl).protocol !== "https:") {
|
|
throw new Error(`peer requires TLS but base_url is not https: ${baseUrl}`);
|
|
}
|
|
const timestamp = String(Date.now());
|
|
const nonce = randomNonce().toString("hex");
|
|
const bodyStr = body ? JSON.stringify(body) : "";
|
|
const targetHost = normalizeHost(new URL(baseUrl).host);
|
|
const canonical = buildCanonical({
|
|
timestamp: timestamp,
|
|
nonce: nonce,
|
|
method: method,
|
|
path: path,
|
|
targetHost: targetHost,
|
|
body: bodyStr
|
|
});
|
|
const signature = sign(secret, canonical);
|
|
|
|
const url = baseUrl.replace(/\/+$/, "") + path;
|
|
const headers = {
|
|
"X-DD-Env-Id": sourceEnvId,
|
|
"X-DD-Timestamp": timestamp,
|
|
"X-DD-Nonce": nonce,
|
|
"X-DD-Signature": signature
|
|
};
|
|
if (bodyStr) {
|
|
// Custom Content-Type so Saltcorn's express.json() middleware leaves the
|
|
// request stream untouched; the server reads exact bytes for HMAC.
|
|
headers["Content-Type"] = "application/vnd.dev-deploy+json";
|
|
}
|
|
|
|
const init = { method: method, headers: headers };
|
|
if (bodyStr) {
|
|
init.body = bodyStr;
|
|
}
|
|
|
|
const res = await fetch(url, init);
|
|
const text = await res.text();
|
|
let parsed = null;
|
|
if (text) {
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch (e) {
|
|
parsed = { raw: text };
|
|
}
|
|
}
|
|
return { status: res.status, ok: res.ok, body: parsed };
|
|
};
|
|
|
|
|
|
// Like signedFetch but returns the response body as a Buffer (raw bytes).
|
|
// For binary endpoints like GET /dev-deploy/api/file/:uuid.
|
|
const signedFetchBinary = async ({ baseUrl, method, path, body, sourceEnvId, secret, requireTls }) => {
|
|
if (requireTls && new URL(baseUrl).protocol !== "https:") {
|
|
throw new Error(`peer requires TLS but base_url is not https: ${baseUrl}`);
|
|
}
|
|
const timestamp = String(Date.now());
|
|
const nonce = randomNonce().toString("hex");
|
|
const bodyStr = body ? JSON.stringify(body) : "";
|
|
const targetHost = normalizeHost(new URL(baseUrl).host);
|
|
const canonical = buildCanonical({
|
|
timestamp: timestamp,
|
|
nonce: nonce,
|
|
method: method,
|
|
path: path,
|
|
targetHost: targetHost,
|
|
body: bodyStr
|
|
});
|
|
const signature = sign(secret, canonical);
|
|
|
|
const url = baseUrl.replace(/\/+$/, "") + path;
|
|
const headers = {
|
|
"X-DD-Env-Id": sourceEnvId,
|
|
"X-DD-Timestamp": timestamp,
|
|
"X-DD-Nonce": nonce,
|
|
"X-DD-Signature": signature
|
|
};
|
|
if (bodyStr) {
|
|
headers["Content-Type"] = "application/vnd.dev-deploy+json";
|
|
}
|
|
|
|
const init = { method: method, headers: headers };
|
|
if (bodyStr) {
|
|
init.body = bodyStr;
|
|
}
|
|
const res = await fetch(url, init);
|
|
const bytes = Buffer.from(await res.arrayBuffer());
|
|
return { status: res.status, ok: res.ok, bytes: bytes };
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
signedFetch,
|
|
signedFetchBinary
|
|
};
|