Merge branch 'main' into serverrside

This commit is contained in:
DeanYao
2024-03-28 11:20:52 +08:00
committed by GitHub
139 changed files with 4744 additions and 1077 deletions

View File

@@ -1,7 +1,7 @@
import { NextRequest } from "next/server";
import { getServerSideConfig } from "../config/server";
import md5 from "spark-md5";
import { ACCESS_CODE_PREFIX } from "../constant";
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip");
@@ -16,15 +16,15 @@ function getIP(req: NextRequest) {
function parseApiKey(bearToken: string) {
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
return {
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isOpenAiKey ? token : "",
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isApiKey ? token : "",
};
}
export function auth(req: NextRequest) {
export function auth(req: NextRequest, modelProvider: ModelProvider) {
const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token
@@ -49,22 +49,23 @@ export function auth(req: NextRequest) {
if (serverConfig.hideUserApiKey && !!apiKey) {
return {
error: true,
msg: "you are not allowed to access openai with your own api key",
msg: "you are not allowed to access with your own api key",
};
}
// if user does not provide an api key, inject system api key
if (!apiKey) {
const serverApiKey = serverConfig.isAzure
? serverConfig.azureApiKey
: serverConfig.apiKey;
const serverConfig = getServerSideConfig();
if (serverApiKey) {
const systemApiKey =
modelProvider === ModelProvider.GeminiPro
? serverConfig.googleApiKey
: serverConfig.isAzure
? serverConfig.azureApiKey
: serverConfig.apiKey;
if (systemApiKey) {
console.log("[Auth] use system api key");
req.headers.set(
"Authorization",
`${serverConfig.isAzure ? "" : "Bearer "}${serverApiKey}`,
);
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
} else {
console.log("[Auth] admin did not provide an api key");
}

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import { DEFAULT_MODELS, OPENAI_BASE_URL } from "../constant";
import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant";
import { collectModelTable } from "../utils/model";
import { makeAzurePath } from "../azure";
@@ -9,8 +9,21 @@ const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) {
const controller = new AbortController();
const authValue = req.headers.get("Authorization") ?? "";
const authHeaderName = serverConfig.isAzure ? "api-key" : "Authorization";
var authValue,
authHeaderName = "";
if (serverConfig.isAzure) {
authValue =
req.headers
.get("Authorization")
?.trim()
.replaceAll("Bearer ", "")
.trim() ?? "";
authHeaderName = "api-key";
} else {
authValue = req.headers.get("Authorization") ?? "";
authHeaderName = "Authorization";
}
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
@@ -116,12 +129,20 @@ export async function requestOpenai(req: NextRequest) {
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
// Also, this is to prevent the header from being sent to the client
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
newHeaders.delete("OpenAI-Organization");
}
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,

View File

@@ -1,43 +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() ?? "",
);
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 = "nodejs";

View File

@@ -0,0 +1,116 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
import { getServerSideConfig } from "@/app/config/server";
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Google Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const controller = new AbortController();
const serverConfig = getServerSideConfig();
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const authResult = auth(req, ModelProvider.GeminiPro);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
const bearToken = req.headers.get("Authorization") ?? "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const key = token ? token : serverConfig.googleApiKey;
if (!key) {
return NextResponse.json(
{
error: true,
message: `missing GOOGLE_API_KEY in server env vars`,
},
{
status: 401,
},
);
}
const fetchUrl = `${baseUrl}/${path}?key=${key}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
},
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"bom1",
"cle1",
"cpt1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

View File

@@ -1,6 +1,6 @@
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
import { getServerSideConfig } from "@/app/config/server";
import { OpenaiPath } from "@/app/constant";
import { ModelProvider, OpenaiPath } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
@@ -45,7 +45,7 @@ async function handle(
);
}
const authResult = auth(req);
const authResult = auth(req, ModelProvider.GPT);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
@@ -75,4 +75,22 @@ export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = ['arn1', 'bom1', 'cdg1', 'cle1', 'cpt1', 'dub1', 'fra1', 'gru1', 'hnd1', 'iad1', 'icn1', 'kix1', 'lhr1', 'pdx1', 'sfo1', 'sin1', 'syd1'];
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

View File

@@ -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";

View File

@@ -0,0 +1,124 @@
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");
// Validate the endpoint to prevent potential SSRF attacks
if (!endpoint || !endpoint.startsWith("/")) {
return NextResponse.json(
{
error: true,
msg: "Invalid endpoint",
},
{
status: 400,
},
);
}
const endpointPath = params.path.join("/");
const targetPath = `${endpoint}/${endpointPath}`;
// 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 " + targetPath,
},
{
status: 403,
},
);
}
// for MKCOL request, only allow request ${folder}
if (
req.method === "MKCOL" &&
!targetPath.endsWith(folder)
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
status: 403,
},
);
}
// for GET request, only allow request ending with fileName
if (
req.method === "GET" &&
!targetPath.endsWith(fileName)
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
status: 403,
},
);
}
// for PUT request, only allow request ending with fileName
if (
req.method === "PUT" &&
!targetPath.endsWith(fileName)
) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
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,
redirect: 'manual',
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";