95 lines
3.8 KiB
JavaScript
95 lines
3.8 KiB
JavaScript
// Outbound signed peer requests -- site-sync's binding of the shared client
|
|
// (lib/common/peerClient.js, which documents the protocol: every response
|
|
// signature is verified BEFORE the body is parsed or used, bodies are byte-capped
|
|
// and every exchange is bounded by a timeout). The target side mutates itself
|
|
// from what a pull returns, so an unauthenticated response must never be acted on.
|
|
|
|
const { createPeerClient, parseJson } = require("./common/peerClient");
|
|
const { HEADERS, PEER_CONTENT_TYPE } = require("./constants");
|
|
const { peerSecret } = require("./peers");
|
|
|
|
|
|
// The machine API (routes.js registers these; index.js exempts the prefix from CSRF).
|
|
const API_PREFIX = "/site-sync/api/";
|
|
const API_PATHS = {
|
|
HEALTH: `${API_PREFIX}health`,
|
|
STATE: `${API_PREFIX}state`,
|
|
PLAN: `${API_PREFIX}plan`,
|
|
APPLY: `${API_PREFIX}apply`,
|
|
FILE_ROUTE: `${API_PREFIX}file/:hash`,
|
|
file: (hash) => `${API_PREFIX}file/${hash}`
|
|
};
|
|
|
|
// Wire limits shared by both directions of the peer protocol.
|
|
const PEER_LIMITS = {
|
|
// Whole-request timeout for an outbound peer call (headers + body).
|
|
TIMEOUT_MS: 2 * 60 * 1000,
|
|
// Largest signed request body a peer will accept (a pushed desired-state pack).
|
|
REQUEST_BODY_BYTES: 64 * 1024 * 1024,
|
|
// Largest JSON response a client will read (/api/state: pack + row manifest).
|
|
JSON_RESPONSE_BYTES: 128 * 1024 * 1024,
|
|
// Largest file served / fetched over GET /api/file/:hash.
|
|
FILE_BYTES: 256 * 1024 * 1024
|
|
};
|
|
|
|
const client = createPeerClient({ headers: HEADERS, contentType: PEER_CONTENT_TYPE });
|
|
|
|
|
|
// The one signed round trip (see lib/common/peerClient.js signedRequest). The
|
|
// timeout is read per call so it can be tuned at runtime (tests shorten it).
|
|
const signedRequest = async (opts) => {
|
|
return await client.signedRequest({ ...opts, timeoutMs: PEER_LIMITS.TIMEOUT_MS });
|
|
};
|
|
|
|
|
|
// Shared per-peer wiring: the paired secret + TLS flag come from the peer row.
|
|
const requestPeer = async (peerRow, sourceEnvId, method, path, body, maxBytes) => {
|
|
return await signedRequest({
|
|
baseUrl: peerRow.base_url,
|
|
method: method,
|
|
path: path,
|
|
body: body,
|
|
sourceEnvId: sourceEnvId,
|
|
secret: await peerSecret(peerRow.peer_id),
|
|
requireTls: peerRow.require_tls,
|
|
maxBytes: maxBytes
|
|
});
|
|
};
|
|
|
|
|
|
// Signed JSON call to a paired peer. Returns the parsed body of a 2xx response;
|
|
// throws on a non-2xx status (with the peer's signed error message), on a body
|
|
// that is not JSON, and when a response naming an env_id names a different one
|
|
// than the paired peer's (the responder is not who this pairing says it is).
|
|
const peerJson = async (peerRow, sourceEnvId, method, path, body) => {
|
|
const r = await requestPeer(peerRow, sourceEnvId, method, path, body, PEER_LIMITS.JSON_RESPONSE_BYTES);
|
|
const parsed = parseJson(r);
|
|
if (!r.ok) {
|
|
throw new Error(`peer ${path} returned status ${r.status}: ${parsed && parsed.error ? parsed.error : "(no error message)"}`);
|
|
}
|
|
if (parsed && parsed.env_id !== undefined && parsed.env_id !== peerRow.env_id) {
|
|
throw new Error(`peer ${path} answered as env_id ${parsed.env_id}, but this pairing is for ${peerRow.env_id}`);
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
|
|
// Signed binary GET from a paired peer (GET /site-sync/api/file/:hash). Returns the
|
|
// verified bytes of a 2xx response; throws otherwise.
|
|
const peerBytes = async (peerRow, sourceEnvId, path) => {
|
|
const r = await requestPeer(peerRow, sourceEnvId, "GET", path, null, PEER_LIMITS.FILE_BYTES);
|
|
if (!r.ok) {
|
|
throw new Error(`peer ${path} returned status ${r.status}`);
|
|
}
|
|
return r.bytes;
|
|
};
|
|
|
|
|
|
module.exports = {
|
|
API_PREFIX,
|
|
API_PATHS,
|
|
PEER_LIMITS,
|
|
signedRequest,
|
|
peerJson,
|
|
peerBytes
|
|
};
|