Compare commits
1 Commits
secrets-mc
...
secrets-mc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43d6164a15 |
@@ -170,6 +170,10 @@ oauth_accounts (
|
|||||||
|
|
||||||
`crates/secrets-mcp/CHANGELOG.md` 在构建时嵌入,服务端以 **Markdown** 渲染为 HTML(`pulldown-cmark`)。**首页**(`/`)页脚与 **Dashboard**(`/dashboard`,MCP 配置页)页脚均提供「变更记录」链接;发版时随 `secrets-mcp` 版本更新该文件即可。
|
`crates/secrets-mcp/CHANGELOG.md` 在构建时嵌入,服务端以 **Markdown** 渲染为 HTML(`pulldown-cmark`)。**首页**(`/`)页脚与 **Dashboard**(`/dashboard`,MCP 配置页)页脚均提供「变更记录」链接;发版时随 `secrets-mcp` 版本更新该文件即可。
|
||||||
|
|
||||||
|
### Google OAuth 出站 HTTP
|
||||||
|
|
||||||
|
换 token(`POST https://oauth2.googleapis.com/token`)与拉取 userinfo 使用工作区 **`reqwest`**。根目录 `Cargo.toml` 中为 `reqwest` 启用了 **`system-proxy`**(因 `default-features = false` 须显式打开),以便在 **macOS / Windows** 上读取**系统代理**,避免「浏览器能上 Google、服务端换 token 超时」这类代理不一致。若仅提供端口代理、系统代理未生效,可设 **`HTTPS_PROXY` / `NO_PROXY`**,见 `deploy/.env.example`。
|
||||||
|
|
||||||
### Web JSON API 与会话
|
### Web JSON API 与会话
|
||||||
|
|
||||||
除页面路由使用的 `require_valid_user`(未登录或 `key_version` 与库不一致时重定向 `/login`)外,JSON API(`/api/...`)使用等价校验:会话中的 `key_version` 须与 `users.key_version` 一致,否则返回 **401** JSON,避免仅校验 `user_id` 时与页面行为不一致。
|
除页面路由使用的 `require_valid_user`(未登录或 `key_version` 与库不一致时重定向 `/login`)外,JSON API(`/api/...`)使用等价校验:会话中的 `key_version` 须与 `users.key_version` 一致,否则返回 **401** JSON,避免仅校验 `user_id` 时与页面行为不一致。
|
||||||
|
|||||||
46
Cargo.lock
generated
46
Cargo.lock
generated
@@ -356,6 +356,16 @@ dependencies = [
|
|||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
@@ -1025,9 +1035,11 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2093,7 +2105,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secrets-mcp"
|
name = "secrets-mcp"
|
||||||
version = "0.5.24"
|
version = "0.5.26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"askama",
|
"askama",
|
||||||
@@ -2611,6 +2623,27 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"core-foundation",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -3378,6 +3411,17 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|||||||
@@ -36,4 +36,5 @@ tracing-subscriber = { version = "^0.3", features = ["env-filter"] }
|
|||||||
dotenvy = "^0.15"
|
dotenvy = "^0.15"
|
||||||
|
|
||||||
# HTTP
|
# HTTP
|
||||||
reqwest = { version = "^0.12", default-features = false, features = ["rustls-tls", "json"] }
|
# system-proxy:与浏览器一致,读取 macOS/Windows 系统代理(禁用 default 后须显式开启,否则 OAuth 出站不走 Clash 等)
|
||||||
|
reqwest = { version = "^0.12", default-features = false, features = ["rustls-tls", "json", "system-proxy"] }
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ cargo build --release -p secrets-mcp
|
|||||||
| `SECRETS_ENV` | 可选。设为 `prod` / `production` 时会拒绝弱 PostgreSQL TLS 模式(`prefer`、`disable`、`allow`、`require`)。 |
|
| `SECRETS_ENV` | 可选。设为 `prod` / `production` 时会拒绝弱 PostgreSQL TLS 模式(`prefer`、`disable`、`allow`、`require`)。 |
|
||||||
| `BASE_URL` | 对外访问基址;OAuth 回调为 `{BASE_URL}/auth/google/callback`。默认 `http://localhost:9315`。 |
|
| `BASE_URL` | 对外访问基址;OAuth 回调为 `{BASE_URL}/auth/google/callback`。默认 `http://localhost:9315`。 |
|
||||||
| `SECRETS_MCP_BIND` | 监听地址,默认 `127.0.0.1:9315`。容器内或直接对外暴露端口时请改为 `0.0.0.0:9315`;反代时常为 `127.0.0.1:9315`。 |
|
| `SECRETS_MCP_BIND` | 监听地址,默认 `127.0.0.1:9315`。容器内或直接对外暴露端口时请改为 `0.0.0.0:9315`;反代时常为 `127.0.0.1:9315`。 |
|
||||||
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | 可选;不配置则无 Google 登录入口。运行时从环境读取,勿写入 CI、勿打入二进制。 |
|
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | 可选;不配置则无 Google 登录入口。运行时从环境读取,勿写入 CI、勿打入二进制。换 token 须访问 `oauth2.googleapis.com`:工作区 **`reqwest` 已启用 `system-proxy`**,与浏览器一致可走 macOS/Windows **系统代理**(如 Clash 系统代理模式)。 |
|
||||||
|
| `HTTPS_PROXY` / `NO_PROXY` | 可选。仅当系统代理未被进程识别、又需走本地端口代理时设置;示例见 [`deploy/.env.example`](deploy/.env.example)。 |
|
||||||
| `RUST_LOG` | 可选;日志级别,如 `secrets_mcp=debug`。 |
|
| `RUST_LOG` | 可选;日志级别,如 `secrets_mcp=debug`。 |
|
||||||
| `SECRETS_DATABASE_POOL_SIZE` | 可选。连接池最大连接数,默认 `10`。 |
|
| `SECRETS_DATABASE_POOL_SIZE` | 可选。连接池最大连接数,默认 `10`。 |
|
||||||
| `SECRETS_DATABASE_ACQUIRE_TIMEOUT` | 可选。获取连接超时秒数,默认 `5`。 |
|
| `SECRETS_DATABASE_ACQUIRE_TIMEOUT` | 可选。获取连接超时秒数,默认 `5`。 |
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
本文档在构建时嵌入 Web 的 `/changelog` 页面,并由服务端渲染为 HTML。
|
本文档在构建时嵌入 Web 的 `/changelog` 页面,并由服务端渲染为 HTML。
|
||||||
|
|
||||||
|
## [0.5.26] - 2026-04-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Google OAuth**:工作区 `reqwest` 此前关闭默认特性且未启用 **`system-proxy`**,进程不读取 macOS/Windows 系统代理,易出现与浏览器不一致(本机可上 Google 但换 token 超时)。已显式启用 `system-proxy`。
|
||||||
|
|
||||||
|
## [0.5.25] - 2026-04-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Google OAuth:token / userinfo 请求单独 **45s** 超时(避免仅触达默认客户端 15s);失败时区分超时、连接错误,并在非 2xx 时记录/返回 Google 响应体片段(如 `invalid_grant`、`redirect_uri_mismatch`)。
|
||||||
|
|
||||||
## [0.5.24] - 2026-04-11
|
## [0.5.24] - 2026-04-11
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "secrets-mcp"
|
name = "secrets-mcp"
|
||||||
version = "0.5.24"
|
version = "0.5.26"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{OAuthConfig, OAuthUserInfo};
|
use super::{OAuthConfig, OAuthUserInfo};
|
||||||
|
|
||||||
|
/// OAuth token / userinfo calls can be slow on poor routes; keep above client default if needed.
|
||||||
|
const OAUTH_HTTP_TIMEOUT: Duration = Duration::from_secs(45);
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct TokenResponse {
|
struct TokenResponse {
|
||||||
access_token: String,
|
access_token: String,
|
||||||
@@ -20,14 +25,28 @@ struct UserInfo {
|
|||||||
picture: Option<String>,
|
picture: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_reqwest_send_err(e: reqwest::Error) -> anyhow::Error {
|
||||||
|
if e.is_timeout() {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"timeout reaching Google OAuth ({}s); ensure outbound HTTPS to oauth2.googleapis.com works (firewall/proxy/VPN if Google is unreachable)",
|
||||||
|
OAUTH_HTTP_TIMEOUT.as_secs()
|
||||||
|
)
|
||||||
|
} else if e.is_connect() {
|
||||||
|
anyhow::anyhow!("connection error to Google OAuth: {e}")
|
||||||
|
} else {
|
||||||
|
anyhow::Error::new(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Exchange authorization code for tokens and fetch user profile.
|
/// Exchange authorization code for tokens and fetch user profile.
|
||||||
pub async fn exchange_code(
|
pub async fn exchange_code(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
config: &OAuthConfig,
|
config: &OAuthConfig,
|
||||||
code: &str,
|
code: &str,
|
||||||
) -> Result<OAuthUserInfo> {
|
) -> Result<OAuthUserInfo> {
|
||||||
let token_resp: TokenResponse = client
|
let token_http = client
|
||||||
.post("https://oauth2.googleapis.com/token")
|
.post("https://oauth2.googleapis.com/token")
|
||||||
|
.timeout(OAUTH_HTTP_TIMEOUT)
|
||||||
.form(&[
|
.form(&[
|
||||||
("code", code),
|
("code", code),
|
||||||
("client_id", &config.client_id),
|
("client_id", &config.client_id),
|
||||||
@@ -37,24 +56,55 @@ pub async fn exchange_code(
|
|||||||
])
|
])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("failed to exchange Google code")?
|
.map_err(map_reqwest_send_err)
|
||||||
.error_for_status()
|
.context("Google token HTTP request failed")?;
|
||||||
.context("Google token endpoint error")?
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.context("failed to parse Google token response")?;
|
|
||||||
|
|
||||||
let user: UserInfo = client
|
let status = token_http.status();
|
||||||
|
let body_bytes = token_http
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.context("read Google token response body")?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let body_lossy = String::from_utf8_lossy(&body_bytes);
|
||||||
|
tracing::warn!(%status, body = %body_lossy, "Google token endpoint error");
|
||||||
|
anyhow::bail!(
|
||||||
|
"Google token error {}: {}",
|
||||||
|
status,
|
||||||
|
body_lossy.chars().take(512).collect::<String>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_resp: TokenResponse =
|
||||||
|
serde_json::from_slice(&body_bytes).context("failed to parse Google token JSON")?;
|
||||||
|
|
||||||
|
let user_http = client
|
||||||
.get("https://openidconnect.googleapis.com/v1/userinfo")
|
.get("https://openidconnect.googleapis.com/v1/userinfo")
|
||||||
|
.timeout(OAUTH_HTTP_TIMEOUT)
|
||||||
.bearer_auth(&token_resp.access_token)
|
.bearer_auth(&token_resp.access_token)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.context("failed to fetch Google userinfo")?
|
.map_err(map_reqwest_send_err)
|
||||||
.error_for_status()
|
.context("Google userinfo HTTP request failed")?;
|
||||||
.context("Google userinfo endpoint error")?
|
|
||||||
.json()
|
let status = user_http.status();
|
||||||
|
let body_bytes = user_http
|
||||||
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.context("failed to parse Google userinfo")?;
|
.context("read Google userinfo body")?;
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let body_lossy = String::from_utf8_lossy(&body_bytes);
|
||||||
|
tracing::warn!(%status, body = %body_lossy, "Google userinfo endpoint error");
|
||||||
|
anyhow::bail!(
|
||||||
|
"Google userinfo error {}: {}",
|
||||||
|
status,
|
||||||
|
body_lossy.chars().take(512).collect::<String>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: UserInfo =
|
||||||
|
serde_json::from_slice(&body_bytes).context("failed to parse Google userinfo JSON")?;
|
||||||
|
|
||||||
Ok(OAuthUserInfo {
|
Ok(OAuthUserInfo {
|
||||||
provider: "google".to_string(),
|
provider: "google".to_string(),
|
||||||
|
|||||||
@@ -20,9 +20,14 @@ BASE_URL=https://secrets.example.com
|
|||||||
|
|
||||||
# ─── Google OAuth ─────────────────────────────────────────────────────
|
# ─── Google OAuth ─────────────────────────────────────────────────────
|
||||||
# Google Cloud Console → APIs & Services → Credentials
|
# Google Cloud Console → APIs & Services → Credentials
|
||||||
# 授权回调 URI 须配置为:${BASE_URL}/auth/google/callback
|
# 授权回调 URI 须与 BASE_URL 完全一致:${BASE_URL}/auth/google/callback(含 http/https、主机名、端口)
|
||||||
|
# 运行 secrets-mcp 的机器须能访问 Google(oauth2.googleapis.com)。若本机用 Clash/Surge「系统代理」上网:
|
||||||
|
# 构建时已启用 reqwest 的 system-proxy,进程会跟随系统代理;仍失败时可设 HTTPS_PROXY(见下方)。
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
# 若仍无法换 token(仅提供端口代理、无系统代理):可取消注释并改为本机代理地址
|
||||||
|
# HTTPS_PROXY=http://127.0.0.1:7890
|
||||||
|
# NO_PROXY=localhost,127.0.0.1
|
||||||
|
|
||||||
# ─── 微信登录(暂未开放,预留)───────────────────────────────────────
|
# ─── 微信登录(暂未开放,预留)───────────────────────────────────────
|
||||||
# WECHAT_APP_CLIENT_ID=
|
# WECHAT_APP_CLIENT_ID=
|
||||||
|
|||||||
Reference in New Issue
Block a user