feat(agent,server): v0.2.7 - mu-plugin WordPress robuste, réparation wp-config, proxy cookies/headers

- Agent: mu-plugin embarqué amélioré (HTTPS forcé, filtres URL, localhost:port)
- Agent: suppression des WP_HOME/WP_SITEURL hardcodés au démarrage des instances
- Server/proxy: envoi X-Forwarded-Port, réécriture headers/body élargie
- Server/proxy: sanitization des Set-Cookie (Secure, SameSite, Domain)
- Dashboard: version agent 0.2.7, action Supprimer complète
- Cleanup: binaires agent 0.2.3-0.2.6 remplacés par 0.2.7
This commit is contained in:
EduBox Dev
2026-06-17 18:23:06 +00:00
parent 2feea2d063
commit b383b11ae2
21 changed files with 396 additions and 46 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
const AGENT_VERSION = "0.2.3";
const AGENT_VERSION = "0.2.7";
export async function GET() {
return NextResponse.json({
+2 -2
View File
@@ -99,7 +99,7 @@ export async function PATCH(req: NextRequest) {
const publicUrl = domain ? `https://${instance.id}.${domain}` : null;
if (action === "stop") {
sendToNode(instance.nodeId, { action: "stop", instanceId: instance.id });
sendToNode(instance.nodeId, { action: "delete", instanceId: instance.id });
await prisma.instance.update({ where: { id }, data: { status: "stopped" } });
} else if (action === "start") {
const sent = sendToNode(instance.nodeId, {
@@ -126,7 +126,7 @@ export async function DELETE(req: NextRequest) {
const id = searchParams.get("id");
if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 });
const instance = await prisma.instance.findUnique({ where: { id } });
if (instance) sendToNode(instance.nodeId, { action: "stop", instanceId: instance.id });
if (instance) sendToNode(instance.nodeId, { action: "delete", instanceId: instance.id });
await prisma.instance.delete({ where: { id } });
return NextResponse.json({ ok: true });
}
+83 -2
View File
@@ -25,12 +25,22 @@ async function proxyRequest(req: NextRequest) {
);
}
const publicUrl = `https://${cleanHost}`;
const targetUrl = new URL(req.url);
const upstream = `http://${instance.node.tailscaleIp}:${instance.port}${targetUrl.pathname}${targetUrl.search}`;
// The middleware rewrites /foo to /api/proxy/foo; strip the prefix before forwarding
let pathname = targetUrl.pathname;
if (pathname.startsWith("/api/proxy")) {
pathname = pathname.slice("/api/proxy".length) || "/";
}
const upstream = `http://${instance.node.tailscaleIp}:${instance.port}${pathname}${targetUrl.search}`;
const headers = new Headers(req.headers);
headers.delete("host");
headers.set("host", cleanHost);
headers.set("x-forwarded-host", cleanHost);
headers.set("x-forwarded-proto", "https");
headers.set("x-forwarded-port", "443");
headers.set("x-forwarded-for", req.headers.get("x-forwarded-for") || "unknown");
const upstreamRes = await fetch(upstream, {
method: req.method,
@@ -47,7 +57,78 @@ async function proxyRequest(req: NextRequest) {
responseHeaders.delete("content-encoding");
responseHeaders.delete("content-length");
return new Response(upstreamRes.body, {
// Rewrite any header values that point to localhost or the internal Tailscale address
const localPatterns = [
`http://${instance.node.tailscaleIp}:${instance.port}`,
`https://${instance.node.tailscaleIp}:${instance.port}`,
`http://localhost:${instance.port}`,
`https://localhost:${instance.port}`,
`http://localhost`,
`https://localhost`,
];
responseHeaders.forEach((value, key) => {
let newValue = value;
for (const pattern of localPatterns) {
newValue = newValue.replaceAll(pattern, publicUrl);
}
if (newValue !== value) {
responseHeaders.set(key, newValue);
}
});
// Sanitize Set-Cookie headers so sessions work through the public domain.
// WordPress may issue cookies for "localhost" or without Secure flag; fix it.
const setCookies = responseHeaders.getSetCookie();
if (setCookies.length > 0) {
responseHeaders.delete("set-cookie");
for (let cookie of setCookies) {
// Drop any Domain=... set for localhost/internal domains
cookie = cookie.replace(/;\s*Domain=[^;]+/gi, "");
// Ensure Secure is present for HTTPS public URLs
if (!/;\s*Secure\b/i.test(cookie)) {
cookie += "; Secure";
}
// Make sure cookies are sent on sub-domain navigations
if (!/;\s*SameSite\b/i.test(cookie)) {
cookie += "; SameSite=Lax";
}
responseHeaders.append("set-cookie", cookie);
}
}
const contentType = responseHeaders.get("content-type") || "";
const shouldRewriteBody =
contentType.includes("text/html") ||
contentType.includes("text/css") ||
contentType.includes("application/javascript") ||
contentType.includes("application/json");
if (!shouldRewriteBody) {
return new Response(upstreamRes.body, {
status: upstreamRes.status,
statusText: upstreamRes.statusText,
headers: responseHeaders,
});
}
// For text responses, rewrite localhost/internal URLs to the public URL
let body = await upstreamRes.text();
const localBase = `http://${instance.node.tailscaleIp}:${instance.port}`;
const localBaseHttps = `https://${instance.node.tailscaleIp}:${instance.port}`;
const localLocalhostHttp = `http://localhost:${instance.port}`;
const localLocalhostHttps = `https://localhost:${instance.port}`;
const localLocalhostPlainHttp = `http://localhost`;
const localLocalhostPlainHttps = `https://localhost`;
body = body
.replaceAll(localBase, publicUrl)
.replaceAll(localBaseHttps, publicUrl)
.replaceAll(localLocalhostHttp, publicUrl)
.replaceAll(localLocalhostHttps, publicUrl)
.replaceAll(localLocalhostPlainHttp, publicUrl)
.replaceAll(localLocalhostPlainHttps, publicUrl);
return new Response(body, {
status: upstreamRes.status,
statusText: upstreamRes.statusText,
headers: responseHeaders,
+1 -1
View File
@@ -1,6 +1,6 @@
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
const AGENT_VERSION = "0.2.3";
const AGENT_VERSION = "0.2.7";
export const dynamic = "force-dynamic";
@@ -8,11 +8,19 @@ export default function InstanceActions({ instanceId, status }: { instanceId: st
async function action(type: string) {
setLoading(type);
await fetch("/api/instances", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: instanceId, action: type }),
});
if (type === "delete") {
if (!confirm("Voulez-vous vraiment supprimer cette instance ?")) {
setLoading(null);
return;
}
await fetch(`/api/instances?id=${instanceId}`, { method: "DELETE" });
} else {
await fetch("/api/instances", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: instanceId, action: type }),
});
}
setLoading(null);
window.location.reload();
}
@@ -32,6 +40,9 @@ export default function InstanceActions({ instanceId, status }: { instanceId: st
<Button size="sm" variant="outline" onClick={() => action("reset")} disabled={!!loading}>
{loading === "reset" ? "..." : "Réinitialiser"}
</Button>
<Button size="sm" variant="destructive" onClick={() => action("delete")} disabled={!!loading}>
{loading === "delete" ? "..." : "Supprimer"}
</Button>
</div>
);
}