0.2.2 onlyoffice修复
This commit is contained in:
@@ -60,6 +60,39 @@ function buildUpstreamRequestHead(req, targetUrl) {
|
||||
lines.push(`${req.method || "GET"} ${upstreamPath} HTTP/1.1`);
|
||||
|
||||
const headers = req.headers || {};
|
||||
const headerValue = (name) => {
|
||||
const v = headers[name];
|
||||
if (!v) return "";
|
||||
return Array.isArray(v) ? v[0] : String(v);
|
||||
};
|
||||
|
||||
// 说明:ONLYOFFICE 在反向代理下会依赖 X-Forwarded-* 推导“对外地址”,用于拼出 ws/wss 与缓存资源 URL。
|
||||
// 这里尽量沿用上游(frp/nginx)注入的 x-forwarded-proto/host;若缺失,则回退到本机 http。
|
||||
const originLike = headerValue("origin") || headerValue("referer") || "";
|
||||
const originUrl = (() => {
|
||||
try {
|
||||
if (!originLike) return null;
|
||||
return new URL(originLike);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const forwardedHostRaw =
|
||||
headerValue("x-forwarded-host") || (originUrl ? originUrl.host : "") || headerValue("host") || "";
|
||||
const forwardedProto =
|
||||
(headerValue("x-forwarded-proto") || "").split(",")[0].trim() ||
|
||||
(originUrl ? originUrl.protocol.replace(":", "") : "") ||
|
||||
"http";
|
||||
const forwardedPort = (() => {
|
||||
const fromHeader = (headerValue("x-forwarded-port") || "").split(",")[0].trim();
|
||||
if (fromHeader) return fromHeader;
|
||||
const hostHasPort = forwardedHostRaw.includes(":") ? forwardedHostRaw.split(":").pop() : "";
|
||||
if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort;
|
||||
return forwardedProto === "https" ? "443" : "80";
|
||||
})();
|
||||
const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost";
|
||||
|
||||
for (const [k, v] of Object.entries(headers)) {
|
||||
if (!v) continue;
|
||||
const key = String(k);
|
||||
@@ -71,6 +104,12 @@ function buildUpstreamRequestHead(req, targetUrl) {
|
||||
}
|
||||
}
|
||||
|
||||
// 说明:补齐/覆盖 forward 信息,避免 ONLYOFFICE 返回指向内部端口的绝对 URL。
|
||||
lines.push(`x-forwarded-host: ${forwardedHost}`);
|
||||
lines.push(`x-forwarded-proto: ${forwardedProto}`);
|
||||
lines.push(`x-forwarded-port: ${forwardedPort}`);
|
||||
lines.push(`x-forwarded-prefix: ${ONLYOFFICE_PREFIX}`);
|
||||
|
||||
// 说明:Host 必须指向 ONLYOFFICE_INTERNAL_URL,否则上游可能拒绝 Upgrade。
|
||||
lines.push(`Host: ${targetUrl.host}`);
|
||||
lines.push("");
|
||||
@@ -144,7 +183,16 @@ async function main() {
|
||||
if (isOnlyOfficePath(req.url || "/")) {
|
||||
try {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[dev-server][onlyoffice-ws] upgrade", req.url);
|
||||
console.log(
|
||||
"[dev-server][onlyoffice-ws] upgrade",
|
||||
req.url,
|
||||
"host=",
|
||||
req.headers.host,
|
||||
"xfp=",
|
||||
req.headers["x-forwarded-proto"],
|
||||
"xfh=",
|
||||
req.headers["x-forwarded-host"],
|
||||
);
|
||||
} catch {}
|
||||
proxyOnlyOfficeUpgrade(req, socket, head);
|
||||
return;
|
||||
|
||||
@@ -81,51 +81,57 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
if (isConvexEnabled()) {
|
||||
// 说明:Convex 模式下,保存写回 Convex Files,并更新 media_assets.storage_id/file_url。
|
||||
const userId = String(process.env.DEV_USER_ID || "dev-user").trim() || "dev-user";
|
||||
const client = getConvexHttpClient();
|
||||
try {
|
||||
// 说明:Convex 模式下,保存写回 Convex Files,并更新 media_assets.storage_id/file_url。
|
||||
const userId = String(process.env.DEV_USER_ID || "dev-user").trim() || "dev-user";
|
||||
const client = getConvexHttpClient();
|
||||
|
||||
const asset = await client.query(api.mediaAssets.getById, { userId, id: assetId });
|
||||
if (!asset) {
|
||||
const asset = await client.query(api.mediaAssets.getById, { userId, id: assetId });
|
||||
if (!asset) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const downloadUrl = tryRewriteOnlyOfficeDownloadUrl(body.url);
|
||||
const upstream = await fetch(downloadUrl, { method: "GET", redirect: "follow" });
|
||||
if (!upstream.ok) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await upstream.arrayBuffer());
|
||||
|
||||
const uploadUrl = await client.mutation(api.mediaAssets.generateUploadUrl, { userId });
|
||||
if (!uploadUrl || typeof uploadUrl !== "string") {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const uploadRes = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": asset.mime_type || "application/octet-stream" },
|
||||
body: buf,
|
||||
});
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null;
|
||||
const storageId = String(uploadJson?.storageId || "");
|
||||
if (!storageId) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
await client.mutation(api.mediaAssets.replaceStorageFromUpload, {
|
||||
userId,
|
||||
id: assetId,
|
||||
storageId: storageId as any,
|
||||
});
|
||||
|
||||
return NextResponse.json({ error: 0 });
|
||||
} catch (error) {
|
||||
// 说明:避免异常导致 ONLYOFFICE 重试/阻塞(例如 Convex 未部署新 mutation)。
|
||||
console.error("[onlyoffice/callback] convex writeback failed:", error);
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const downloadUrl = tryRewriteOnlyOfficeDownloadUrl(body.url);
|
||||
const upstream = await fetch(downloadUrl, { method: "GET", redirect: "follow" });
|
||||
if (!upstream.ok) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const buf = Buffer.from(await upstream.arrayBuffer());
|
||||
|
||||
const uploadUrl = await client.mutation(api.mediaAssets.generateUploadUrl, { userId });
|
||||
if (!uploadUrl || typeof uploadUrl !== "string") {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const uploadRes = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": asset.mime_type || "application/octet-stream" },
|
||||
body: buf,
|
||||
});
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null;
|
||||
const storageId = String(uploadJson?.storageId || "");
|
||||
if (!storageId) {
|
||||
return NextResponse.json({ error: 1 });
|
||||
}
|
||||
|
||||
await client.mutation(api.mediaAssets.replaceStorageFromUpload, {
|
||||
userId,
|
||||
id: assetId,
|
||||
storageId: storageId as any,
|
||||
});
|
||||
|
||||
return NextResponse.json({ error: 0 });
|
||||
}
|
||||
|
||||
const { data: asset, error: assetError } = await supabaseAdmin
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -44,12 +45,23 @@ window.__MNOTE_ONLYOFFICE_XHR_REWRITE__ = true;
|
||||
var proxyPrefix = location.origin.replace(/\\/+$/, '') + '/onlyoffice-server';
|
||||
var internal = {
|
||||
'http://127.0.0.1:8081': true,
|
||||
'http://localhost:8081': true
|
||||
'http://localhost:8081': true,
|
||||
// 说明:同上,兜底错误的 https://127.0.0.1:8081
|
||||
'https://127.0.0.1:8081': true,
|
||||
'https://localhost:8081': true
|
||||
};
|
||||
function rewrite(u) {
|
||||
try {
|
||||
var abs = new URL(u, location.origin);
|
||||
var origin = abs.protocol + '//' + abs.host;
|
||||
// 说明:外网 https 访问时,如果 ONLYOFFICE 错误生成了 http://<host>/onlyoffice-server/...,
|
||||
// 浏览器会以 Mixed Content 阻止请求;这里直接升级为 https。
|
||||
if (location.protocol === 'https:' && abs.protocol === 'http:' && abs.host === location.host) {
|
||||
if (abs.pathname === '/onlyoffice-server' || abs.pathname.indexOf('/onlyoffice-server/') === 0) {
|
||||
abs.protocol = 'https:';
|
||||
return abs.toString();
|
||||
}
|
||||
}
|
||||
if (!internal[origin]) return u;
|
||||
return proxyPrefix + abs.pathname + abs.search + abs.hash;
|
||||
} catch (e) {
|
||||
@@ -154,13 +166,57 @@ const proxy = async (request: NextRequest, pathParts: string[]) => {
|
||||
// 说明:ONLYOFFICE 在被反向代理时,会根据 X-Forwarded-* 推导自身对外地址,
|
||||
// 用于生成静态资源/缓存文件的 URL。若缺失这些信息,可能会返回指向内部端口
|
||||
//(例如 http://127.0.0.1:8081/cache/...)的绝对 URL,导致浏览器跨域请求被 CORS 拦截。
|
||||
headers.set("x-forwarded-host", incomingUrl.host);
|
||||
headers.set("x-forwarded-proto", incomingUrl.protocol.replace(":", ""));
|
||||
if (incomingUrl.port) {
|
||||
headers.set("x-forwarded-port", incomingUrl.port);
|
||||
} else {
|
||||
headers.set("x-forwarded-port", incomingUrl.protocol === "https:" ? "443" : "80");
|
||||
}
|
||||
const runtimeCfg = getMnoteRuntimeConfig();
|
||||
const xfp = (request.headers.get("x-forwarded-proto") || "").split(",")[0].trim();
|
||||
const xfh = (request.headers.get("x-forwarded-host") || "").split(",")[0].trim();
|
||||
const xfpPort = (request.headers.get("x-forwarded-port") || "").split(",")[0].trim();
|
||||
|
||||
const originLike =
|
||||
request.headers.get("origin") ||
|
||||
request.headers.get("referer") ||
|
||||
"";
|
||||
const originUrl = (() => {
|
||||
try {
|
||||
if (!originLike) return null;
|
||||
return new URL(originLike);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const cloudflare = (() => {
|
||||
try {
|
||||
const raw = String(runtimeCfg.cloudflareAppOrigin || "").trim();
|
||||
if (!raw) return null;
|
||||
return new URL(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const inferredProto =
|
||||
xfp ||
|
||||
(originUrl ? originUrl.protocol.replace(":", "") : "") ||
|
||||
(cloudflare && cloudflare.host === incomingUrl.host ? cloudflare.protocol.replace(":", "") : "") ||
|
||||
incomingUrl.protocol.replace(":", "");
|
||||
const inferredHost =
|
||||
xfh ||
|
||||
(originUrl ? originUrl.host : "") ||
|
||||
incomingUrl.host;
|
||||
const inferredPort =
|
||||
xfpPort ||
|
||||
(originUrl ? originUrl.port : "") ||
|
||||
(cloudflare && cloudflare.host === incomingUrl.host ? cloudflare.port : "") ||
|
||||
incomingUrl.port ||
|
||||
(inferredProto === "https" ? "443" : "80");
|
||||
|
||||
const proto = inferredProto || "http";
|
||||
const host = inferredHost || incomingUrl.host;
|
||||
const port = inferredPort || (proto === "https" ? "443" : "80");
|
||||
|
||||
headers.set("x-forwarded-host", host);
|
||||
headers.set("x-forwarded-proto", proto);
|
||||
headers.set("x-forwarded-port", port);
|
||||
headers.set("x-forwarded-prefix", "/onlyoffice-server");
|
||||
// 说明:避免上游返回 gzip 后被 Node fetch 自动解压,但仍带着 content-encoding,
|
||||
// 导致浏览器二次解压报 ERR_CONTENT_DECODING_FAILED。
|
||||
|
||||
@@ -150,7 +150,14 @@ const setupOnlyOfficeInternalRequestRewrite = (baseUrl: string, onlyofficeBaseUr
|
||||
return `${window.location.origin.replace(/\/+$/, "")}${normalizedBase}`;
|
||||
})();
|
||||
|
||||
const internalOrigins = new Set<string>(["http://127.0.0.1:8081", "http://localhost:8081"]);
|
||||
const internalOrigins = new Set<string>([
|
||||
"http://127.0.0.1:8081",
|
||||
"http://localhost:8081",
|
||||
// 说明:部分环境下 ONLYOFFICE 会错误拼出 https://127.0.0.1:8081 这类 URL,
|
||||
// 浏览器会报 ERR_SSL_PROTOCOL_ERROR(因为 8081 实际是 http)。这里也一起兜底重写。
|
||||
"https://127.0.0.1:8081",
|
||||
"https://localhost:8081",
|
||||
]);
|
||||
try {
|
||||
if (onlyofficeBaseUrlDesktop) {
|
||||
const u = new URL(onlyofficeBaseUrlDesktop);
|
||||
@@ -166,6 +173,19 @@ const setupOnlyOfficeInternalRequestRewrite = (baseUrl: string, onlyofficeBaseUr
|
||||
const rewriteUrl = (input: string) => {
|
||||
try {
|
||||
const u = new (win as any).URL(input, (win as any).location?.origin || window.location.origin);
|
||||
// 说明:外网 https 访问时,若 ONLYOFFICE 错误生成 http://<host>/onlyoffice-server/... 会被 Mixed Content 阻止;
|
||||
// 这里提前升级为 https,避免浏览器直接拦截请求。
|
||||
try {
|
||||
const loc = (win as any).location;
|
||||
if (loc?.protocol === "https:" && u.protocol === "http:" && u.host === loc.host) {
|
||||
if (u.pathname === "/onlyoffice-server" || u.pathname.startsWith("/onlyoffice-server/")) {
|
||||
u.protocol = "https:";
|
||||
return u.toString();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const origin = `${u.protocol}//${u.host}`;
|
||||
if (!internalOrigins.has(origin)) return input;
|
||||
return `${proxyPrefix}${u.pathname}${u.search}${u.hash}`;
|
||||
|
||||
Reference in New Issue
Block a user