From 5e83b0ce5a95cb8fbe3803f5d3ba96d4f89dd538 Mon Sep 17 00:00:00 2001 From: H0llyW00dzZ Date: Thu, 14 Mar 2024 01:45:55 +0700 Subject: [PATCH] [Cherry Pick] Head Repo (#306) * chore: specify yarn 1 in package.json * fix: adjust upstash api * fix: add webdav request filter * fix: remove corsFetch * fix: change matching pattern * chore: update cors default path * fix: fix upstash sync issue * fix: fix webdav sync issue * feat: bump version * Fix Github Gist - [+] refactor(gist.ts): replace corsFetch with native fetch function - [+] refactor(gist.ts): rename error variable to e in catch block * Fix Gosync - [+] chore(gosync.ts): remove unused import 'corsFetch' - [+] feat(gosync.ts): add explanatory note for createGoSyncClient function --------- Co-authored-by: SukkaW Co-authored-by: fred-bf --- app/api/cors/[...path]/route.ts | 70 ------------- app/api/upstash/[action]/[...key]/route.ts | 73 ++++++++++++++ app/api/webdav/[...path]/route.ts | 112 +++++++++++++++++++++ app/constant.ts | 2 +- app/utils/cloud/gist.ts | 13 ++- app/utils/cloud/gosync.ts | 2 +- app/utils/cloud/upstash.ts | 37 +++---- app/utils/cloud/webdav.ts | 71 +++++-------- app/utils/cors.ts | 34 ------- package.json | 5 +- src-tauri/tauri.conf.json | 2 +- 11 files changed, 243 insertions(+), 178 deletions(-) delete mode 100644 app/api/cors/[...path]/route.ts create mode 100644 app/api/upstash/[action]/[...key]/route.ts create mode 100644 app/api/webdav/[...path]/route.ts diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts deleted file mode 100644 index 586afe5b09f..00000000000 --- a/app/api/cors/[...path]/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -async function handle( - req: NextRequest, - { params }: { params: { path: string[] } }, -) { - if (req.method === "OPTIONS") { - return NextResponse.json({ body: "OK" }, { status: 200 }); - } - - const [protocol, ...subpath] = params.path; - const targetUrl = `${protocol}://${subpath.join("/")}`; - - const method = req.headers.get("method") ?? undefined; - const shouldNotHaveBody = ["get", "head"].includes( - method?.toLowerCase() ?? "", - ); - - function isRealDevicez(userAgent: string | null): boolean { - // Author : @H0llyW00dzZ - // Note : This just an experiment for a prevent suspicious bot - // Modify this function to define your logic for determining if the user-agent belongs to a real device - // For example, you can check if the user-agent contains certain keywords or patterns that indicate a real device - if (userAgent) { - return userAgent.includes("AppleWebKit") && !userAgent.includes("Headless"); - } - return false; - } - - - const userAgent = req.headers.get("User-Agent"); - const isRealDevice = isRealDevicez(userAgent); - - if (!isRealDevice) { - return NextResponse.json( - { - error: true, - msg: "Access Forbidden", - }, - { - status: 403, - }, - ); - } - - const fetchOptions: RequestInit = { - headers: { - authorization: req.headers.get("authorization") ?? "", - }, - body: shouldNotHaveBody ? null : req.body, - method, - // @ts-ignore - duplex: "half", - }; - - const fetchResult = await fetch(targetUrl, fetchOptions); - - console.log("[Cloud Sync]", targetUrl, { - status: fetchResult.status, - statusText: fetchResult.statusText, - }); - - return fetchResult; -} - -export const POST = handle; -export const GET = handle; -export const OPTIONS = handle; - -export const runtime = "edge"; diff --git a/app/api/upstash/[action]/[...key]/route.ts b/app/api/upstash/[action]/[...key]/route.ts new file mode 100644 index 00000000000..fcfef471862 --- /dev/null +++ b/app/api/upstash/[action]/[...key]/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from "next/server"; + +async function handle( + req: NextRequest, + { params }: { params: { action: string; key: string[] } }, +) { + const requestUrl = new URL(req.url); + const endpoint = requestUrl.searchParams.get("endpoint"); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + const [...key] = params.key; + // only allow to request to *.upstash.io + if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.key.join("/"), + }, + { + status: 403, + }, + ); + } + + // only allow upstash get and set method + if (params.action !== "get" && params.action !== "set") { + console.log("[Upstash Route] forbidden action ", params.action); + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.action, + }, + { + status: 403, + }, + ); + } + + const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`; + + const method = req.method; + const shouldNotHaveBody = ["get", "head"].includes( + method?.toLowerCase() ?? "", + ); + + const fetchOptions: RequestInit = { + headers: { + authorization: req.headers.get("authorization") ?? "", + }, + body: shouldNotHaveBody ? null : req.body, + method, + // @ts-ignore + duplex: "half", + }; + + console.log("[Upstash Proxy]", targetUrl, fetchOptions); + const fetchResult = await fetch(targetUrl, fetchOptions); + + console.log("[Any Proxy]", targetUrl, { + status: fetchResult.status, + statusText: fetchResult.statusText, + }); + + return fetchResult; +} + +export const POST = handle; +export const GET = handle; +export const OPTIONS = handle; + +export const runtime = "edge"; diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts new file mode 100644 index 00000000000..c60ca18bb39 --- /dev/null +++ b/app/api/webdav/[...path]/route.ts @@ -0,0 +1,112 @@ +import { NextRequest, NextResponse } from "next/server"; +import { STORAGE_KEY } from "../../../constant"; +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + const folder = STORAGE_KEY; + const fileName = `${folder}/backup.json`; + + const requestUrl = new URL(req.url); + let endpoint = requestUrl.searchParams.get("endpoint"); + if (!endpoint?.endsWith("/")) { + endpoint += "/"; + } + const endpointPath = params.path.join("/"); + + // only allow MKCOL, GET, PUT + if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + // for MKCOL request, only allow request ${folder} + if ( + req.method == "MKCOL" && + !new URL(endpointPath).pathname.endsWith(folder) + ) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + // for GET request, only allow request ending with fileName + if ( + req.method == "GET" && + !new URL(endpointPath).pathname.endsWith(fileName) + ) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + // for PUT request, only allow request ending with fileName + if ( + req.method == "PUT" && + !new URL(endpointPath).pathname.endsWith(fileName) + ) { + return NextResponse.json( + { + error: true, + msg: "you are not allowed to request " + params.path.join("/"), + }, + { + status: 403, + }, + ); + } + + const targetUrl = `${endpoint + endpointPath}`; + + const method = req.method; + const shouldNotHaveBody = ["get", "head"].includes( + method?.toLowerCase() ?? "", + ); + + const fetchOptions: RequestInit = { + headers: { + authorization: req.headers.get("authorization") ?? "", + }, + body: shouldNotHaveBody ? null : req.body, + method, + // @ts-ignore + duplex: "half", + }; + + const fetchResult = await fetch(targetUrl, fetchOptions); + + console.log("[Any Proxy]", targetUrl, { + status: fetchResult.status, + statusText: fetchResult.statusText, + }); + + return fetchResult; +} + +export const POST = handle; +export const GET = handle; +export const OPTIONS = handle; + +export const runtime = "edge"; diff --git a/app/constant.ts b/app/constant.ts index 1d53c664935..b004c35412c 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -26,7 +26,7 @@ export enum Path { } export enum ApiPath { - Cors = "/api/cors", + Cors = "", OpenAI = "/api/openai", } diff --git a/app/utils/cloud/gist.ts b/app/utils/cloud/gist.ts index d37aaa248da..db3ae6f4717 100644 --- a/app/utils/cloud/gist.ts +++ b/app/utils/cloud/gist.ts @@ -1,7 +1,6 @@ import { STORAGE_KEY, REPO_URL } from "@/app/constant"; import { chunks } from "../format"; import { SyncStore } from "@/app/store/sync"; -import { corsFetch } from "../cors"; export type GistConfig = SyncStore["githubGist"] & { gistId: string }; export type GistClient = ReturnType; @@ -37,7 +36,7 @@ export function createGistClient(store: SyncStore) { }; } - return corsFetch("https://api.github.com/gists", { + return await fetch("https://api.github.com/gists", { method: "POST", headers: this.headers(), body: JSON.stringify({ @@ -68,7 +67,7 @@ export function createGistClient(store: SyncStore) { }, async check(): Promise { - const res = await corsFetch(this.path(gistId), { + const res = await fetch(this.path(gistId), { method: "GET", headers: this.headers(), }); @@ -85,7 +84,7 @@ export function createGistClient(store: SyncStore) { }, async get() { - const res = await corsFetch(this.path(gistId), { + const res = await fetch(this.path(gistId), { method: "GET", headers: this.headers(), }); @@ -110,7 +109,7 @@ export function createGistClient(store: SyncStore) { const newContent = JSON.stringify(data, null, 2); const description = `[Sync] [200 OK] [GithubGist] Last Sync: ${currentDate} Site: ${REPO_URL}`; - return corsFetch(this.path(gistId), { + return fetch(this.path(gistId), { method: existingContent ? "PATCH" : "POST", headers: this.headers(), body: JSON.stringify({ @@ -131,11 +130,11 @@ export function createGistClient(store: SyncStore) { ); return newContent; }) - .catch((error) => { + .catch((e) => { console.error( "[Gist] Set A Data oF File Name", `${fileBackup}`, - error, + Error, ); return ""; }); diff --git a/app/utils/cloud/gosync.ts b/app/utils/cloud/gosync.ts index 8b5a1b1672e..9273b8b7d22 100644 --- a/app/utils/cloud/gosync.ts +++ b/app/utils/cloud/gosync.ts @@ -1,6 +1,5 @@ import { STORAGE_KEY } from "@/app/constant"; import { SyncStore } from "@/app/store/sync"; -import { corsFetch } from "../cors"; import { chunks } from "../format"; export type GoSync = SyncStore["gosync"]; @@ -8,4 +7,5 @@ export type GoSyncClient = ReturnType; export function createGoSyncClient(store: SyncStore) { /** TODO */ + // Note: This my own sync writen in go, not yet ready } diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts index 658028e299a..bf6147bd467 100644 --- a/app/utils/cloud/upstash.ts +++ b/app/utils/cloud/upstash.ts @@ -1,6 +1,5 @@ import { STORAGE_KEY } from "@/app/constant"; import { SyncStore } from "@/app/store/sync"; -import { corsFetch } from "../cors"; import { chunks } from "../format"; export type UpstashConfig = SyncStore["upstash"]; @@ -18,11 +17,9 @@ export function createUpstashClient(store: SyncStore) { return { async check() { try { - const res = await corsFetch(this.path(`get/${storeKey}`), { + const res = await fetch(this.path(`get/${storeKey}`, proxyUrl), { method: "GET", headers: this.headers(), - proxyUrl, - mode: "cors", }); console.log("[Upstash] check", res.status, res.statusText); return [200].includes(res.status); @@ -33,11 +30,9 @@ export function createUpstashClient(store: SyncStore) { }, async redisGet(key: string) { - const res = await corsFetch(this.path(`get/${key}`), { + const res = await fetch(this.path(`get/${key}`, proxyUrl), { method: "GET", headers: this.headers(), - proxyUrl, - mode: "cors", }); console.log("[Upstash] get key = ", key, res.status, res.statusText); @@ -47,12 +42,10 @@ export function createUpstashClient(store: SyncStore) { }, async redisSet(key: string, value: string) { - const res = await corsFetch(this.path(`set/${key}`), { + const res = await fetch(this.path(`set/${key}`, proxyUrl), { method: "POST", headers: this.headers(), body: value, - proxyUrl, - mode: "cors", }); console.log("[Upstash] set key = ", key, res.status, res.statusText); @@ -87,18 +80,28 @@ export function createUpstashClient(store: SyncStore) { Authorization: `Bearer ${config.apiKey}`, }; }, - path(path: string) { - let url = config.endpoint; - - if (!url.endsWith("/")) { - url += "/"; + path(path: string, proxyUrl: string = "") { + if (!path.endsWith("/")) { + path += "/"; } - if (path.startsWith("/")) { path = path.slice(1); } - return url + path; + if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) { + proxyUrl += "/"; + } + + let url; + if (proxyUrl.length > 0 || proxyUrl === "/") { + let u = new URL(proxyUrl + "/api/upstash/" + path); + // add query params + u.searchParams.append("endpoint", config.endpoint); + url = u.toString(); + } else { + url = "/api/upstash/" + path + "?endpoint=" + config.endpoint; + } + return url; }, }; } diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts index 34f72548b53..bc569de0ec4 100644 --- a/app/utils/cloud/webdav.ts +++ b/app/utils/cloud/webdav.ts @@ -1,13 +1,12 @@ import { STORAGE_KEY } from "@/app/constant"; import { SyncStore } from "@/app/store/sync"; -import { corsFetch } from "../cors"; export type WebDAVConfig = SyncStore["webdav"]; export type WebDavClient = ReturnType; export function createWebDavClient(store: SyncStore) { const folder = STORAGE_KEY; - const fileName = `${folder}/${store.webdav.filename}`; + const fileName = `${folder}/backup.json`; const config = store.webdav; const proxyUrl = store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; @@ -15,19 +14,12 @@ export function createWebDavClient(store: SyncStore) { return { async check() { try { - const res = await corsFetch(this.path(fileName), { - method: "PROPFIND", + const res = await fetch(this.path(folder, proxyUrl), { + method: "MKCOL", headers: this.headers(), - proxyUrl, - mode: "cors", }); - console.log( - "[WebDav] Check Data From File Name", - `${fileName}`, - res.status, - res.statusText, - ); - return [200, 207, 404].includes(res.status); + console.log("[WebDav] check", res.status, res.statusText); + return [201, 200, 404, 301, 302, 307, 308].includes(res.status); } catch (e) { console.error("[WebDav] failed to check", e); } @@ -36,45 +28,24 @@ export function createWebDavClient(store: SyncStore) { }, async get(key: string) { - const res = await corsFetch(this.path(fileName), { + const res = await fetch(this.path(fileName, proxyUrl), { method: "GET", headers: this.headers(), - proxyUrl, - mode: "cors", }); - console.log("[WebDav] Get File Name =", key, res.status, res.statusText); + console.log("[WebDav] get key = ", key, res.status, res.statusText); return await res.text(); }, async set(key: string, value: string) { - const exists = await this.check(); - - if (!exists) { - await corsFetch(this.path(fileName), { - method: "PUT", - headers: this.headers(), - body: "", - proxyUrl, - mode: "cors", - }); - } - - const res = await corsFetch(this.path(fileName), { + const res = await fetch(this.path(fileName, proxyUrl), { method: "PUT", headers: this.headers(), body: value, - proxyUrl, - mode: "cors", }); - console.log( - "[WebDav] Set A new data from File Name =", - key, - res.status, - res.statusText, - ); + console.log("[WebDav] set key = ", key, res.status, res.statusText); }, headers() { @@ -84,18 +55,28 @@ export function createWebDavClient(store: SyncStore) { authorization: `Basic ${auth}`, }; }, - path(path: string) { - let url = config.endpoint; - - if (!url.endsWith("/")) { - url += "/"; + path(path: string, proxyUrl: string = "") { + if (!path.endsWith("/")) { + path += "/"; } - if (path.startsWith("/")) { path = path.slice(1); } - return url + path; + if (proxyUrl.length > 0 && !proxyUrl.endsWith("/")) { + proxyUrl += "/"; + } + + let url; + if (proxyUrl.length > 0 || proxyUrl === "/") { + let u = new URL(proxyUrl + "/api/webdav/" + path); + // add query params + u.searchParams.append("endpoint", config.endpoint); + url = u.toString(); + } else { + url = "/api/upstash/" + path + "?endpoint=" + config.endpoint; + } + return url; }, }; } diff --git a/app/utils/cors.ts b/app/utils/cors.ts index 20b3e516017..93956a7b5c7 100644 --- a/app/utils/cors.ts +++ b/app/utils/cors.ts @@ -14,37 +14,3 @@ export function corsPath(path: string) { return `${baseUrl}${path}`; } - -export function corsFetch( - url: string, - options: RequestInit & { - proxyUrl?: string; - }, -) { - if (!url.startsWith("http")) { - throw Error("[CORS Fetch] url must starts with http/https"); - } - - let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors); - if (!proxyUrl.endsWith("/")) { - proxyUrl += "/"; - } - - url = url.replace("://", "/"); - - const corsOptions = { - ...options, - method: "POST", - headers: options.method - ? { - ...options.headers, - method: options.method, - } - : options.headers, - }; - - const corsUrl = proxyUrl + url; - console.info("[CORS] target = ", corsUrl); - - return fetch(corsUrl, corsOptions); -} diff --git a/package.json b/package.json index 0bd74ff314d..0b96ebb13f4 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,6 @@ }, "resolutions": { "lint-staged/yaml": "^2.2.2" - } -} \ No newline at end of file + }, + "packageManager": "yarn@1.22.19" +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 53852278b68..19e94a54bd6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "NextChat", - "version": "2.11.2" + "version": "2.11.3" }, "tauri": { "allowlist": {