From b0c1ccd0a0a02bf4ba2f34a1148f7480ab756482 Mon Sep 17 00:00:00 2001 From: glay Date: Fri, 22 Nov 2024 22:03:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=92=8C=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E8=AE=BE=E7=BD=AE=E5=8A=A0=E5=AF=86=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=95=B0=E6=8D=AE=E7=9A=84=E5=AF=86=E9=92=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.template | 7 +++-- app/api/auth.ts | 31 +++++-------------- app/api/bedrock.ts | 54 ++++++++++++++++++++------------- app/client/api.ts | 53 ++++++++++++++++---------------- app/client/platforms/bedrock.ts | 26 ++++++++-------- app/components/chat.module.scss | 15 ++------- app/components/settings.tsx | 34 ++++++++++----------- app/config/server.ts | 13 +++++--- app/locales/cn.ts | 12 +++----- app/locales/en.ts | 12 +++----- app/store/access.ts | 13 ++++++-- app/utils/aws.ts | 22 +++++--------- 12 files changed, 138 insertions(+), 154 deletions(-) diff --git a/.env.template b/.env.template index 524fc3da2..1f4773195 100644 --- a/.env.template +++ b/.env.template @@ -70,8 +70,9 @@ WHITE_WEBDAV_ENDPOINTS= ### bedrock (optional) AWS_REGION= -AWS_ACCESS_KEY= +AWS_ACCESS_KEY=AKIA AWS_SECRET_KEY= - -### Assign this with a secure, randomly generated key +### Assign this with a secure, randomly generated key; +### Generate a secure, random key that is at least 32 characters long. You can use a password generator or a command like this: +### openssl rand -base64 32 ENCRYPTION_KEY= \ No newline at end of file diff --git a/app/api/auth.ts b/app/api/auth.ts index e56f85820..044cd62f2 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -52,29 +52,6 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { msg: "you are not allowed to access with your own api key", }; } - // Special handling for Bedrock - if (modelProvider === ModelProvider.Bedrock) { - const region = serverConfig.awsRegion; - const accessKeyId = serverConfig.awsAccessKey; - const secretAccessKey = serverConfig.awsSecretKey; - - console.log("[Auth] Bedrock credentials:", { - region, - accessKeyId: accessKeyId ? "***" : undefined, - secretKey: secretAccessKey ? "***" : undefined, - }); - - // Check if AWS credentials are provided - if (!region || !accessKeyId || !secretAccessKey) { - return { - error: true, - msg: "Missing AWS credentials. Please configure Region, Access Key ID, and Secret Access Key in settings.", - }; - } - - return { error: false }; - } - // if user does not provide an api key, inject system api key if (!apiKey) { const serverConfig = getServerSideConfig(); @@ -120,6 +97,14 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { case ModelProvider.ChatGLM: systemApiKey = serverConfig.chatglmApiKey; break; + case ModelProvider.Bedrock: + systemApiKey = + serverConfig.awsRegion + + ":" + + serverConfig.awsAccessKey + + ":" + + serverConfig.awsSecretKey; + break; case ModelProvider.GPT: default: if (req.nextUrl.pathname.includes("azure/deployments")) { diff --git a/app/api/bedrock.ts b/app/api/bedrock.ts index d0da02ed0..e6b039ae8 100644 --- a/app/api/bedrock.ts +++ b/app/api/bedrock.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { sign, decrypt } from "../utils/aws"; +import { sign } from "../utils/aws"; +import { getServerSideConfig } from "../config/server"; const ALLOWED_PATH = new Set(["chat", "models"]); @@ -18,7 +19,7 @@ function parseEventData(chunk: Uint8Array): any { } return parsed.body || parsed; } catch (e) { - console.error("Error parsing event data:", e); + // console.error("Error parsing event data:", e); try { // Handle base64 encoded responses const base64Match = text.match(/:"([A-Za-z0-9+/=]+)"/); @@ -76,7 +77,7 @@ async function* transformBedrockStream( const parsed = parseEventData(value); if (!parsed) continue; - console.log("Parsed response:", JSON.stringify(parsed, null, 2)); + // console.log("Parsed response:", JSON.stringify(parsed, null, 2)); // Handle Titan models if (modelId.startsWith("amazon.titan")) { @@ -182,26 +183,38 @@ function validateRequest(body: any, modelId: string): void { async function requestBedrock(req: NextRequest) { const controller = new AbortController(); - const awsRegion = req.headers.get("X-Region") ?? ""; - const awsAccessKey = req.headers.get("X-Access-Key") ?? ""; - const awsSecretKey = req.headers.get("X-Secret-Key") ?? ""; - const awsSessionToken = req.headers.get("X-Session-Token"); - const modelId = req.headers.get("X-Model-Id") ?? ""; + + // Get AWS credentials from server config first + const config = getServerSideConfig(); + let awsRegion = config.awsRegion; + let awsAccessKey = config.awsAccessKey; + let awsSecretKey = config.awsSecretKey; + let modelId = ""; + + // If server-side credentials are not available, parse from Authorization header + if (!awsRegion || !awsAccessKey || !awsSecretKey) { + const authHeader = req.headers.get("Authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + throw new Error("Missing or invalid Authorization header"); + } + + const [_, credentials] = authHeader.split("Bearer "); + const [region, accessKey, secretKey, model] = credentials.split(","); + + if (!region || !accessKey || !secretKey || !model) { + throw new Error("Invalid Authorization header format"); + } + + awsRegion = region; + awsAccessKey = accessKey; + awsSecretKey = secretKey; + modelId = model; + } if (!awsRegion || !awsAccessKey || !awsSecretKey || !modelId) { throw new Error("Missing required AWS credentials or model ID"); } - const decryptedAccessKey = decrypt(awsAccessKey); - const decryptedSecretKey = decrypt(awsSecretKey); - const decryptedSessionToken = awsSessionToken - ? decrypt(awsSessionToken) - : undefined; - - if (!decryptedAccessKey || !decryptedSecretKey) { - throw new Error("Failed to decrypt AWS credentials"); - } - // Construct the base endpoint const baseEndpoint = `https://bedrock-runtime.${awsRegion}.amazonaws.com`; @@ -236,9 +249,8 @@ async function requestBedrock(req: NextRequest) { method: "POST", url: endpoint, region: awsRegion, - accessKeyId: decryptedAccessKey, - secretAccessKey: decryptedSecretKey, - sessionToken: decryptedSessionToken, + accessKeyId: awsAccessKey, + secretAccessKey: awsSecretKey, body: requestBody, service: "bedrock", }); diff --git a/app/client/api.ts b/app/client/api.ts index feb1c93a2..eb0e4270d 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -23,7 +23,6 @@ import { SparkApi } from "./platforms/iflytek"; import { XAIApi } from "./platforms/xai"; import { ChatGLMApi } from "./platforms/glm"; import { BedrockApi } from "./platforms/bedrock"; -import { encrypt } from "../utils/aws"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -258,8 +257,6 @@ export function getHeaders(ignoreHeaders: boolean = false) { const isEnabledAccessControl = accessStore.enabledAccessControl(); const apiKey = isGoogle ? accessStore.googleApiKey - : isBedrock - ? accessStore.awsAccessKey // Use AWS access key for Bedrock : isAzure ? accessStore.azureApiKey : isAnthropic @@ -278,6 +275,18 @@ export function getHeaders(ignoreHeaders: boolean = false) { ? accessStore.iflytekApiKey && accessStore.iflytekApiSecret ? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret : "" + : isBedrock + ? accessStore.awsRegion && + accessStore.awsAccessKey && + accessStore.awsSecretKey + ? accessStore.awsRegion + + "," + + accessStore.awsAccessKey + + "," + + accessStore.awsSecretKey + + "," + + modelConfig.model + : "" : accessStore.openaiApiKey; return { isBedrock, @@ -303,13 +312,10 @@ export function getHeaders(ignoreHeaders: boolean = false) { ? "x-api-key" : isGoogle ? "x-goog-api-key" - : isBedrock - ? "x-api-key" : "Authorization"; } const { - isBedrock, isGoogle, isAzure, isAnthropic, @@ -322,28 +328,23 @@ export function getHeaders(ignoreHeaders: boolean = false) { const authHeader = getAuthHeader(); - if (isBedrock) { - // Secure encryption of AWS credentials using the new encryption utility - headers["X-Region"] = encrypt(accessStore.awsRegion); - headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey); - headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey); + // if (isBedrock) { + // // Secure encryption of AWS credentials using the new encryption utility + // headers["X-Region"] = encrypt(accessStore.awsRegion); + // headers["X-Access-Key"] = encrypt(accessStore.awsAccessKey); + // headers["X-Secret-Key"] = encrypt(accessStore.awsSecretKey); + // } else { + const bearerToken = getBearerToken( + apiKey, + isAzure || isAnthropic || isGoogle, + ); - if (accessStore.awsSessionToken) { - headers["X-Session-Token"] = encrypt(accessStore.awsSessionToken); - } - } else { - const bearerToken = getBearerToken( - apiKey, - isAzure || isAnthropic || isGoogle, + if (bearerToken) { + headers[authHeader] = bearerToken; + } else if (isEnabledAccessControl && validString(accessStore.accessCode)) { + headers["Authorization"] = getBearerToken( + ACCESS_CODE_PREFIX + accessStore.accessCode, ); - - if (bearerToken) { - headers[authHeader] = bearerToken; - } else if (isEnabledAccessControl && validString(accessStore.accessCode)) { - headers["Authorization"] = getBearerToken( - ACCESS_CODE_PREFIX + accessStore.accessCode, - ); - } } return headers; diff --git a/app/client/platforms/bedrock.ts b/app/client/platforms/bedrock.ts index c13aa4410..a4a11c30b 100644 --- a/app/client/platforms/bedrock.ts +++ b/app/client/platforms/bedrock.ts @@ -1,5 +1,6 @@ import { ChatOptions, + getHeaders, LLMApi, SpeechOptions, RequestMessage, @@ -233,23 +234,22 @@ export class BedrockApi implements LLMApi { const accessStore = useAccessStore.getState(); if (!accessStore.isValidBedrock()) { throw new Error( - "Invalid AWS credentials. Please check your configuration.", + "Invalid AWS credentials. Please check your configuration and ensure ENCRYPTION_KEY is set.", ); } try { const apiEndpoint = "/api/bedrock/chat"; - const headers = { - "Content-Type": requestBody.contentType || "application/json", - Accept: requestBody.accept || "application/json", - "X-Region": accessStore.awsRegion, - "X-Access-Key": accessStore.awsAccessKey, - "X-Secret-Key": accessStore.awsSecretKey, - "X-Model-Id": modelConfig.model, - ...(accessStore.awsSessionToken && { - "X-Session-Token": accessStore.awsSessionToken, - }), - }; + // const headers = { + // "Content-Type": requestBody.contentType || "application/json", + // Accept: requestBody.accept || "application/json", + // "X-Region": accessStore.awsRegion, + // "X-Access-Key": accessStore.awsAccessKey, + // "X-Secret-Key": accessStore.awsSecretKey, + // "X-Model-Id": modelConfig.model, + // "X-Encryption-Key": accessStore.bedrockEncryptionKey, + // }; + const headers = getHeaders(); if (options.config.stream) { let index = -1; @@ -274,7 +274,6 @@ export class BedrockApi implements LLMApi { (text: string, runTools: ChatMessageTool[]) => { try { const chunkJson = JSON.parse(text); - // console.log("Received chunk:", JSON.stringify(chunkJson, null, 2)); if (chunkJson?.content_block?.type === "tool_use") { index += 1; currentToolArgs = ""; @@ -375,7 +374,6 @@ export class BedrockApi implements LLMApi { }); const resJson = await res.json(); - // console.log("Response:", JSON.stringify(resJson, null, 2)); const message = this.extractMessage(resJson, modelConfig.model); // console.log("Extracted message:", message); options.onFinish(message, res); diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss index 387d2f90e..55575c297 100644 --- a/app/components/chat.module.scss +++ b/app/components/chat.module.scss @@ -79,17 +79,6 @@ pointer-events: none; } - .icon { - display: flex; - align-items: center; - justify-content: center; - - svg { - width: 16px; - height: 16px; - } - } - &:hover { --delay: 0.5s; width: var(--full-width); @@ -410,8 +399,8 @@ button { padding: 7px; + } } -} /* Specific styles for iOS devices */ @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) { @@ -761,4 +750,4 @@ transform: translateX(0); } } -} +} \ No newline at end of file diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 7d48a0ec2..e06cc5442 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -980,12 +980,12 @@ export function Settings() { onChange={(e) => accessStore.update((access) => { const region = e.currentTarget.value; - if (!/^[a-z]{2}-[a-z]+-\d+$/.test(region)) { - showToast(Locale.Settings.Access.Bedrock.Region.Invalid); - return; - } + if (!/^[a-z]{2}-[a-z]+-\d+$/.test(region)) { + showToast(Locale.Settings.Access.Bedrock.Region.Invalid); + return; + } access.awsRegion = region; - }) + }) } /> @@ -999,7 +999,7 @@ export function Settings() { type="text" placeholder={Locale.Settings.Access.Bedrock.AccessKey.Placeholder} onChange={(e) => { - accessStore.update((access) => { + accessStore.update((access) => { const accessKey = e.currentTarget.value; if (accessKey && accessKey.length !== 20) { showToast(Locale.Settings.Access.Bedrock.AccessKey.Invalid); @@ -1022,11 +1022,11 @@ export function Settings() { placeholder={Locale.Settings.Access.Bedrock.SecretKey.Placeholder} onChange={(e) => { accessStore.update((access) => { - const secretKey = e.currentTarget.value; - if (secretKey && secretKey.length !== 40) { - showToast(Locale.Settings.Access.Bedrock.SecretKey.Invalid); - return; - } + const secretKey = e.currentTarget.value; + if (secretKey && secretKey.length !== 40) { + showToast(Locale.Settings.Access.Bedrock.SecretKey.Invalid); + return; + } access.awsSecretKey = secretKey; }); }} @@ -1034,17 +1034,17 @@ export function Settings() { /> { accessStore.update( - (access) => (access.awsSessionToken = e.currentTarget.value), + (access) => (access.bedrockEncryptionKey = e.currentTarget.value), ); }} maskWhenShow={true} diff --git a/app/config/server.ts b/app/config/server.ts index 0437ed534..e8fbf131a 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -13,9 +13,10 @@ declare global { OPENAI_ORG_ID?: string; // openai only // bedrock only - BEDROCK_REGION?: string; - BEDROCK_API_KEY?: string; - BEDROCK_API_SECRET?: string; + AWS_REGION?: string; + AWS_ACCESS_KEY?: string; + AWS_SECRET_KEY?: string; + ENCRYPTION_KEY?: string; VERCEL?: string; BUILD_MODE?: "standalone" | "export"; @@ -148,7 +149,10 @@ export const getServerSideConfig = () => { } const isStability = !!process.env.STABILITY_API_KEY; - const isBedrock = !!process.env.BEDROCK_API_KEY; + const isBedrock = + !!process.env.AWS_REGION && + !!process.env.AWS_ACCESS_KEY && + !!process.env.AWS_SECRET_KEY; const isAzure = !!process.env.AZURE_URL; const isGoogle = !!process.env.GOOGLE_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY; @@ -182,6 +186,7 @@ export const getServerSideConfig = () => { awsRegion: process.env.AWS_REGION, awsAccessKey: process.env.AWS_ACCESS_KEY, awsSecretKey: process.env.AWS_SECRET_KEY, + bedrockEncryptionKey: process.env.ENCRYPTION_KEY, isStability, stabilityUrl: process.env.STABILITY_URL, diff --git a/app/locales/cn.ts b/app/locales/cn.ts index d98a7eade..9a8f5cc19 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -361,14 +361,10 @@ const cn = { Placeholder: "****", Invalid: "无效的 AWS Secret Key 格式。必须为40个字符。", }, - SessionToken: { - Title: "AWS Session Token (Optional)", - SubTitle: "Your AWS session token if using temporary credentials", - Placeholder: "Optional session token", - }, - Endpoint: { - Title: "AWS Bedrock Endpoint", - SubTitle: "Custom endpoint for AWS Bedrock API. Default: ", + EncryptionKey: { + Title: "加密密钥", + SubTitle: "用于配置数据的加密密钥", + Placeholder: "输入加密密钥", }, }, Azure: { diff --git a/app/locales/en.ts b/app/locales/en.ts index e4bd0cedb..670f822c8 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -365,14 +365,10 @@ const en: LocaleType = { Placeholder: "****", Invalid: "Invalid AWS secret key format. Must be 40 characters long.", }, - SessionToken: { - Title: "AWS Session Token (Optional)", - SubTitle: "Your AWS session token if using temporary credentials", - Placeholder: "Optional session token", - }, - Endpoint: { - Title: "AWS Bedrock Endpoint", - SubTitle: "Custom endpoint for AWS Bedrock API. Default: ", + EncryptionKey: { + Title: "Encryption Key", + SubTitle: "Your encryption key for configuration data", + Placeholder: "Enter encryption key", }, }, Azure: { diff --git a/app/store/access.ts b/app/store/access.ts index 75a8123a2..5ec99b175 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -15,6 +15,7 @@ import { IFLYTEK_BASE_URL, XAI_BASE_URL, CHATGLM_BASE_URL, + BEDROCK_BASE_URL, } from "../constant"; import { getHeaders } from "../client/api"; import { getClientConfig } from "../config/client"; @@ -51,6 +52,8 @@ const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI; const DEFAULT_CHATGLM_URL = isApp ? CHATGLM_BASE_URL : ApiPath.ChatGLM; +const DEFAULT_BEDROCK_URL = isApp ? BEDROCK_BASE_URL : ApiPath.Bedrock; + const DEFAULT_ACCESS_STATE = { accessCode: "", useCustomConfig: false, @@ -117,10 +120,11 @@ const DEFAULT_ACCESS_STATE = { chatglmApiKey: "", // aws bedrock + bedrokUrl: DEFAULT_BEDROCK_URL, awsRegion: "", awsAccessKey: "", awsSecretKey: "", - awsSessionToken: "", + bedrockEncryptionKey: "", // server config needCode: true, @@ -200,7 +204,12 @@ export const useAccessStore = createPersistStore( }, isValidBedrock() { - return ensure(get(), ["awsRegion", "awsAccessKey", "awsSecretKey"]); + return ensure(get(), [ + "awsRegion", + "awsAccessKey", + "awsSecretKey", + "bedrockEncryptionKey", + ]); }, isAuthorized() { diff --git a/app/utils/aws.ts b/app/utils/aws.ts index dfa0a92fe..d88707328 100644 --- a/app/utils/aws.ts +++ b/app/utils/aws.ts @@ -3,14 +3,14 @@ import HmacSHA256 from "crypto-js/hmac-sha256"; import Hex from "crypto-js/enc-hex"; import Utf8 from "crypto-js/enc-utf8"; import { AES, enc } from "crypto-js"; +import { getServerSideConfig } from "../config/server"; -const SECRET_KEY = - process.env.ENCRYPTION_KEY || - "your-secret-key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; -if (!SECRET_KEY || SECRET_KEY.length < 32) { - throw new Error( - "ENCRYPTION_KEY environment variable must be set with at least 32 characters", - ); +const serverConfig = getServerSideConfig(); +// console.info(serverConfig); +const SECRET_KEY = serverConfig.bedrockEncryptionKey || ""; +// console.info("======SECRET_KEY:"+SECRET_KEY); +if (serverConfig.isBedrock && !SECRET_KEY) { + console.error("When use Bedrock modle,ENCRYPTION_KEY should been set!"); } export function encrypt(data: string): string { @@ -54,7 +54,6 @@ export interface SignParams { region: string; accessKeyId: string; secretAccessKey: string; - sessionToken?: string; body: string; service: string; } @@ -143,7 +142,6 @@ export async function sign({ region, accessKeyId, secretAccessKey, - sessionToken, body, service, }: SignParams): Promise> { @@ -169,11 +167,6 @@ export async function sign({ "x-amzn-bedrock-accept": "*/*", }; - // Add session token if present - if (sessionToken) { - headers["x-amz-security-token"] = sessionToken; - } - // Get sorted header keys (case-insensitive) const sortedHeaderKeys = Object.keys(headers).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()), @@ -230,7 +223,6 @@ export async function sign({ "X-Amz-Content-Sha256": headers["x-amz-content-sha256"], "X-Amz-Date": headers["x-amz-date"], "X-Amzn-Bedrock-Accept": headers["x-amzn-bedrock-accept"], - ...(sessionToken && { "X-Amz-Security-Token": sessionToken }), Authorization: authorization, }; }