fix(patches): rewrite micropub self-fetch to localhost for jailed setup
Node can't reach its own public HTTPS URL (ECONNREFUSED 127.0.0.1:443) because port 443 only exists on the nginx jail. Rewrite self-referential fetch URLs to http://localhost:3000 in endpoint-posts, endpoint-syndicate, and endpoint-share. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,25 @@
|
||||
/**
|
||||
* Patch: rewrite self-referential fetch URLs to use localhost and add
|
||||
* diagnostic logging for fetch failures.
|
||||
*
|
||||
* When behind a reverse proxy (e.g. nginx in a separate FreeBSD jail),
|
||||
* the endpoint-posts form controller fetches the micropub endpoint via
|
||||
* the public URL (https://...). But the Node process doesn't listen on
|
||||
* 443 — only nginx does. This causes ECONNREFUSED on the Node jail.
|
||||
*
|
||||
* Fix: rewrite the URL to http://localhost:<PORT> before fetching, so
|
||||
* the request stays inside the Node jail. The public URL is preserved
|
||||
* for everything else (HTML link headers, external clients, etc.).
|
||||
*
|
||||
* Controlled by INTERNAL_FETCH_URL env var (e.g. "http://localhost:3000").
|
||||
* Falls back to http://localhost:${PORT || 3000} automatically.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const filePath = "node_modules/@indiekit/endpoint-posts/lib/endpoint.js";
|
||||
|
||||
const marker = "// [patch] fetch-diagnostic";
|
||||
const marker = "// [patch] fetch-internal-rewrite";
|
||||
|
||||
async function exists(p) {
|
||||
try {
|
||||
@@ -14,28 +31,71 @@ async function exists(p) {
|
||||
}
|
||||
|
||||
if (!(await exists(filePath))) {
|
||||
console.log("[postinstall] endpoint-posts endpoint.js not found — skipping fetch-diagnostic patch");
|
||||
console.log("[postinstall] endpoint-posts endpoint.js not found — skipping fetch-rewrite patch");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
|
||||
if (source.includes(marker)) {
|
||||
console.log("[postinstall] endpoint-posts fetch-diagnostic patch already applied");
|
||||
console.log("[postinstall] endpoint-posts fetch-rewrite patch already applied");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Wrap the fetch calls to log the underlying cause on failure
|
||||
const oldPost = ` async post(url, accessToken, jsonBody = false) {
|
||||
const endpointResponse = await fetch(url, {`;
|
||||
// Also handle the case where the old diagnostic-only patch was applied
|
||||
const oldMarker = "// [patch] fetch-diagnostic";
|
||||
let cleanSource = source;
|
||||
if (cleanSource.includes(oldMarker)) {
|
||||
// Strip old patch — we'll re-apply from scratch on the original structure.
|
||||
// Safest approach: bail and let the user re-run after npm install.
|
||||
console.log("[postinstall] Old fetch-diagnostic patch detected — stripping before re-patching");
|
||||
// We can't cleanly reverse the old patch, so we need to check if the
|
||||
// original structure is still recognisable. If not, warn and skip.
|
||||
}
|
||||
|
||||
const newPost = ` ${marker}
|
||||
const original = `import { IndiekitError } from "@indiekit/error";
|
||||
|
||||
export const endpoint = {
|
||||
/**
|
||||
* Micropub query
|
||||
* @param {string} url - URL
|
||||
* @param {string} accessToken - Access token
|
||||
* @returns {Promise<object>} Response data
|
||||
*/
|
||||
async get(url, accessToken) {
|
||||
const endpointResponse = await fetch(url, {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
authorization: \`Bearer \${accessToken}\`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!endpointResponse.ok) {
|
||||
throw await IndiekitError.fromFetch(endpointResponse);
|
||||
}
|
||||
|
||||
const body = await endpointResponse.json();
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
/**
|
||||
* Micropub action
|
||||
* @param {string} url - URL
|
||||
* @param {string} accessToken - Access token
|
||||
* @param {object} [jsonBody] - JSON body
|
||||
* @returns {Promise<object>} Response data
|
||||
*/
|
||||
async post(url, accessToken, jsonBody = false) {
|
||||
let endpointResponse;
|
||||
try {
|
||||
endpointResponse = await fetch(url, {`;
|
||||
|
||||
const oldPostEnd = ` });
|
||||
const endpointResponse = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
authorization: \`Bearer \${accessToken}\`,
|
||||
...(jsonBody && { "content-type": "application/json" }),
|
||||
},
|
||||
...(jsonBody && { body: JSON.stringify(jsonBody) }),
|
||||
});
|
||||
|
||||
if (!endpointResponse.ok) {
|
||||
throw await IndiekitError.fromFetch(endpointResponse);
|
||||
@@ -47,11 +107,78 @@ const oldPostEnd = ` });
|
||||
},
|
||||
};`;
|
||||
|
||||
const newPostEnd = ` });
|
||||
const patched = `import { IndiekitError } from "@indiekit/error";
|
||||
|
||||
${marker}
|
||||
const _internalBase = (() => {
|
||||
if (process.env.INTERNAL_FETCH_URL) return process.env.INTERNAL_FETCH_URL.replace(/\\/+$/, "");
|
||||
const port = process.env.PORT || "3000";
|
||||
return \`http://localhost:\${port}\`;
|
||||
})();
|
||||
const _publicBase = (
|
||||
process.env.PUBLICATION_URL || process.env.SITE_URL || ""
|
||||
).replace(/\\/+$/, "");
|
||||
|
||||
function _toInternalUrl(url) {
|
||||
if (!_publicBase || !url.startsWith(_publicBase)) return url;
|
||||
return _internalBase + url.slice(_publicBase.length);
|
||||
}
|
||||
|
||||
export const endpoint = {
|
||||
/**
|
||||
* Micropub query
|
||||
* @param {string} url - URL
|
||||
* @param {string} accessToken - Access token
|
||||
* @returns {Promise<object>} Response data
|
||||
*/
|
||||
async get(url, accessToken) {
|
||||
const fetchUrl = _toInternalUrl(url);
|
||||
let endpointResponse;
|
||||
try {
|
||||
endpointResponse = await fetch(fetchUrl, {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
authorization: \`Bearer \${accessToken}\`,
|
||||
},
|
||||
});
|
||||
} catch (fetchError) {
|
||||
const cause = fetchError.cause || fetchError;
|
||||
console.error("[endpoint-posts] fetch failed for POST %s — %s: %s", url, cause.code || cause.name, cause.message);
|
||||
if (cause.cause) console.error("[endpoint-posts] nested cause: %s", cause.cause.message || cause.cause);
|
||||
console.error("[endpoint-posts] fetch failed for GET %s (internal: %s) — %s: %s", url, fetchUrl, cause.code || cause.name, cause.message);
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
if (!endpointResponse.ok) {
|
||||
throw await IndiekitError.fromFetch(endpointResponse);
|
||||
}
|
||||
|
||||
const body = await endpointResponse.json();
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
/**
|
||||
* Micropub action
|
||||
* @param {string} url - URL
|
||||
* @param {string} accessToken - Access token
|
||||
* @param {object} [jsonBody] - JSON body
|
||||
* @returns {Promise<object>} Response data
|
||||
*/
|
||||
async post(url, accessToken, jsonBody = false) {
|
||||
const fetchUrl = _toInternalUrl(url);
|
||||
let endpointResponse;
|
||||
try {
|
||||
endpointResponse = await fetch(fetchUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
authorization: \`Bearer \${accessToken}\`,
|
||||
...(jsonBody && { "content-type": "application/json" }),
|
||||
},
|
||||
...(jsonBody && { body: JSON.stringify(jsonBody) }),
|
||||
});
|
||||
} catch (fetchError) {
|
||||
const cause = fetchError.cause || fetchError;
|
||||
console.error("[endpoint-posts] fetch failed for POST %s (internal: %s) — %s: %s", url, fetchUrl, cause.code || cause.name, cause.message);
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
@@ -65,52 +192,20 @@ const newPostEnd = ` });
|
||||
},
|
||||
};`;
|
||||
|
||||
const oldGet = ` async get(url, accessToken) {
|
||||
const endpointResponse = await fetch(url, {`;
|
||||
|
||||
const newGet = ` async get(url, accessToken) {
|
||||
let endpointResponse;
|
||||
try {
|
||||
endpointResponse = await fetch(url, {`;
|
||||
|
||||
const oldGetEnd = ` });
|
||||
|
||||
if (!endpointResponse.ok) {
|
||||
throw await IndiekitError.fromFetch(endpointResponse);
|
||||
}
|
||||
|
||||
const body = await endpointResponse.json();
|
||||
|
||||
return body;
|
||||
},`;
|
||||
|
||||
const newGetEnd = ` });
|
||||
} catch (fetchError) {
|
||||
const cause = fetchError.cause || fetchError;
|
||||
console.error("[endpoint-posts] fetch failed for GET %s — %s: %s", url, cause.code || cause.name, cause.message);
|
||||
if (cause.cause) console.error("[endpoint-posts] nested cause: %s", cause.cause.message || cause.cause);
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
if (!endpointResponse.ok) {
|
||||
throw await IndiekitError.fromFetch(endpointResponse);
|
||||
}
|
||||
|
||||
const body = await endpointResponse.json();
|
||||
|
||||
return body;
|
||||
},`;
|
||||
|
||||
let updated = source;
|
||||
updated = updated.replace(oldPost, newPost);
|
||||
updated = updated.replace(oldPostEnd, newPostEnd);
|
||||
updated = updated.replace(oldGet, newGet);
|
||||
updated = updated.replace(oldGetEnd, newGetEnd);
|
||||
|
||||
if (!updated.includes(marker)) {
|
||||
console.warn("[postinstall] Skipping endpoint-posts fetch-diagnostic patch: upstream format changed");
|
||||
// Try matching the original (unpatched) file first
|
||||
if (cleanSource.includes(original.trim())) {
|
||||
const updated = cleanSource.replace(original.trim(), patched.trim());
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
console.log("[postinstall] Patched endpoint-posts: fetch URL rewrite + diagnostic logging");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
console.log("[postinstall] Patched endpoint-posts with fetch diagnostic logging");
|
||||
// If old diagnostic patch was applied, try matching that version
|
||||
if (cleanSource.includes(oldMarker)) {
|
||||
// Overwrite the whole file with the new patched version
|
||||
await writeFile(filePath, patched + "\n", "utf8");
|
||||
console.log("[postinstall] Replaced old fetch-diagnostic patch with fetch-rewrite + diagnostic");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.warn("[postinstall] Skipping endpoint-posts fetch-rewrite patch: upstream format changed");
|
||||
|
||||
113
scripts/patch-micropub-fetch-internal-url.mjs
Normal file
113
scripts/patch-micropub-fetch-internal-url.mjs
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Patch: rewrite micropub self-fetch URLs to localhost in endpoint-syndicate
|
||||
* and endpoint-share.
|
||||
*
|
||||
* Same issue as endpoint-posts: behind a reverse proxy (nginx in a separate
|
||||
* FreeBSD jail), Node can't reach its own public HTTPS URL because port 443
|
||||
* only exists on the nginx jail.
|
||||
*
|
||||
* Rewrites fetch(application.micropubEndpoint, ...) to use
|
||||
* http://localhost:<PORT> instead.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const marker = "// [patch] micropub-fetch-internal-url";
|
||||
|
||||
const helperBlock = `${marker}
|
||||
const _mpInternalBase = (() => {
|
||||
if (process.env.INTERNAL_FETCH_URL) return process.env.INTERNAL_FETCH_URL.replace(/\\/+$/, "");
|
||||
const port = process.env.PORT || "3000";
|
||||
return \`http://localhost:\${port}\`;
|
||||
})();
|
||||
const _mpPublicBase = (
|
||||
process.env.PUBLICATION_URL || process.env.SITE_URL || ""
|
||||
).replace(/\\/+$/, "");
|
||||
function _toInternalUrl(url) {
|
||||
if (!_mpPublicBase || !url.startsWith(_mpPublicBase)) return url;
|
||||
return _mpInternalBase + url.slice(_mpPublicBase.length);
|
||||
}
|
||||
`;
|
||||
|
||||
const targets = [
|
||||
{
|
||||
paths: [
|
||||
"node_modules/@indiekit/endpoint-syndicate/lib/controllers/syndicate.js",
|
||||
],
|
||||
oldSnippet: ` const micropubResponse = await fetch(application.micropubEndpoint, {`,
|
||||
newSnippet: ` const micropubResponse = await fetch(_toInternalUrl(application.micropubEndpoint), {`,
|
||||
},
|
||||
{
|
||||
paths: [
|
||||
"node_modules/@indiekit/endpoint-share/lib/controllers/share.js",
|
||||
],
|
||||
oldSnippet: ` const micropubResponse = await fetch(application.micropubEndpoint, {`,
|
||||
newSnippet: ` const micropubResponse = await fetch(_toInternalUrl(application.micropubEndpoint), {`,
|
||||
},
|
||||
];
|
||||
|
||||
async function exists(filePath) {
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let totalPatched = 0;
|
||||
|
||||
for (const target of targets) {
|
||||
for (const filePath of target.paths) {
|
||||
if (!(await exists(filePath))) continue;
|
||||
|
||||
const source = await readFile(filePath, "utf8");
|
||||
|
||||
if (source.includes(marker)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!source.includes(target.oldSnippet)) {
|
||||
console.warn(`[postinstall] micropub-fetch-internal-url: snippet not found in ${filePath} — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Insert helper block after the last import statement.
|
||||
// Find the last "from" keyword followed by a string and semicolon,
|
||||
// which marks the end of the last import.
|
||||
const importEndPattern = /;\s*\n/g;
|
||||
const allImportMatches = [...source.matchAll(/^import\s/gm)];
|
||||
if (allImportMatches.length === 0) {
|
||||
console.warn(`[postinstall] micropub-fetch-internal-url: no imports found in ${filePath} — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the semicolon+newline that ends the last import block
|
||||
const lastImportStart = allImportMatches.at(-1).index;
|
||||
const afterLastImport = source.slice(lastImportStart);
|
||||
const fromMatch = afterLastImport.match(/from\s+["'][^"']+["']\s*;\s*\n/);
|
||||
if (!fromMatch) {
|
||||
console.warn(`[postinstall] micropub-fetch-internal-url: can't find end of last import in ${filePath} — skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const insertAt = lastImportStart + fromMatch.index + fromMatch[0].length;
|
||||
const beforeHelper = source.slice(0, insertAt);
|
||||
const afterHelper = source.slice(insertAt);
|
||||
|
||||
let updated = beforeHelper + "\n" + helperBlock + "\n" + afterHelper;
|
||||
|
||||
// Now replace the fetch call
|
||||
updated = updated.replace(target.oldSnippet, target.newSnippet);
|
||||
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
console.log(`[postinstall] Patched micropub-fetch-internal-url in ${filePath}`);
|
||||
totalPatched++;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPatched === 0) {
|
||||
console.log("[postinstall] micropub-fetch-internal-url patches already applied or no targets found");
|
||||
} else {
|
||||
console.log(`[postinstall] micropub-fetch-internal-url: patched ${totalPatched} file(s)`);
|
||||
}
|
||||
Reference in New Issue
Block a user