mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-14 14:52:08 +08:00
Revert "feat(cvd): add device-bound subscription protocol"
This reverts commit 2cb9c13ab6.
This commit is contained in:
@@ -58,20 +58,6 @@ pub struct PrfItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub home: Option<String>,
|
||||
|
||||
/// Retained for backward compatibility only. This field does NOT gate CVD; a missing or `false`
|
||||
/// value never disables it. Kept so older `profiles.yaml` entries deserialize without losing
|
||||
/// data.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cvd_enabled: Option<bool>,
|
||||
|
||||
/// Cached CVD device **public** key (base64url, 32 bytes) for this remote subscription. This is
|
||||
/// NOT a secret — it is advertised in the `X-CVD-Pub` header whenever CVD is active. It lives
|
||||
/// here so a refresh can build the request header without touching the OS keychain (which, per
|
||||
/// profile, would prompt on macOS). The matching private key stays in the keychain under
|
||||
/// `cvd/<uid>`. Absent for local profiles, pre-CVD remotes, and airports we never tried CVD on.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cvd_pub: Option<String>,
|
||||
|
||||
/// the file data
|
||||
#[serde(skip)]
|
||||
pub file_data: Option<String>,
|
||||
@@ -168,10 +154,7 @@ impl PrfOption {
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
/// Build a new profile item. For remote items the returned [`crate::cvd::DeviceKey`] (if any)
|
||||
/// must be persisted by the caller **after** the item is saved — see
|
||||
/// [`crate::config::profiles_append_item_with_filedata_safe`].
|
||||
pub async fn from(item: &Self, file_data: Option<String>) -> Result<(Self, Option<crate::cvd::DeviceKey>)> {
|
||||
pub async fn from(item: &Self, file_data: Option<String>) -> Result<Self> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
@@ -188,42 +171,14 @@ impl PrfItem {
|
||||
.ok_or_else(|| anyhow::anyhow!("url should not be null"))?;
|
||||
let name = item.name.as_ref();
|
||||
let desc = item.desc.as_ref();
|
||||
|
||||
// Mint the uid and the device key once, and reuse the key across the proxy-fallback
|
||||
// retry so both attempts present the SAME public key (one server slot). The key is
|
||||
// returned to the caller, which persists it to the keychain only after the profile is
|
||||
// saved — so a failed create never writes (and never orphans) a key.
|
||||
let uid = help::get_uid("R");
|
||||
let key = crate::cvd::DeviceKey::generate(&uid);
|
||||
|
||||
let built = match Self::from_url(
|
||||
url,
|
||||
name,
|
||||
desc,
|
||||
item.option.as_ref(),
|
||||
&uid,
|
||||
crate::cvd::CvdMode::New(&key),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(out) => out,
|
||||
Err(primary_err) => {
|
||||
// Retry once via the Clash self-proxy, reusing the same uid/key.
|
||||
let mut retry_opt = item.option.clone().unwrap_or_default();
|
||||
retry_opt.self_proxy = Some(true);
|
||||
retry_opt.with_proxy = Some(false);
|
||||
Self::from_url(url, name, desc, Some(&retry_opt), &uid, crate::cvd::CvdMode::New(&key))
|
||||
.await
|
||||
.map_err(|_| primary_err)?
|
||||
}
|
||||
};
|
||||
Ok((built, Some(key)))
|
||||
let option = item.option.as_ref();
|
||||
Self::from_url(url, name, desc, option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.clone().unwrap_or_else(|| "Local File".into());
|
||||
let desc = item.desc.clone().unwrap_or_else(|| "".into());
|
||||
let option = item.option.as_ref();
|
||||
Ok((Self::from_local(name, desc, file_data, option).await?, None))
|
||||
Self::from_local(name, desc, file_data, option).await
|
||||
}
|
||||
typ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
@@ -291,8 +246,6 @@ impl PrfItem {
|
||||
..PrfOption::default()
|
||||
}),
|
||||
home: None,
|
||||
cvd_enabled: None,
|
||||
cvd_pub: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(file_data.unwrap_or_else(|| tmpl::ITEM_LOCAL.into())),
|
||||
})
|
||||
@@ -300,17 +253,11 @@ impl PrfItem {
|
||||
|
||||
/// ## Remote type
|
||||
/// create a new item from url
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub async fn from_url(
|
||||
url: &str,
|
||||
name: Option<&String>,
|
||||
desc: Option<&String>,
|
||||
option: Option<&PrfOption>,
|
||||
// The stable uid for this profile (= its `<uid>.yaml` and keychain `cvd/<uid>`). The caller
|
||||
// always chooses it: a fresh id for a new profile, or the existing id when refreshing.
|
||||
uid: &str,
|
||||
// How to use CVD this fetch. The caller owns the device-key lifecycle; `from_url` only reads.
|
||||
cvd: crate::cvd::CvdMode<'_>,
|
||||
) -> Result<Self> {
|
||||
let with_proxy = option.is_some_and(|o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = option.is_some_and(|o| o.self_proxy.unwrap_or(false));
|
||||
@@ -336,18 +283,6 @@ impl PrfItem {
|
||||
|
||||
let url = fix_dirty_url(url)?;
|
||||
|
||||
let uid: String = uid.into();
|
||||
|
||||
// Build the CVD request header from the caller-supplied key (new) or cached public key
|
||||
// (refresh). `from_url` never writes the keychain: new keys are persisted by the caller only
|
||||
// after the profile is saved, and refreshes read the private key lazily (only to decrypt).
|
||||
let cvd_headers = cvd
|
||||
.header_public()
|
||||
.map(|pk| crate::cvd::request_headers(&pk))
|
||||
.unwrap_or_default();
|
||||
// Public key (non-secret) to cache on the built item so future refreshes need no keychain.
|
||||
let cvd_pub_cache = cvd.cached_public_b64();
|
||||
|
||||
// 使用网络管理器发送请求
|
||||
let resp = match NetworkManager::new()
|
||||
.get_with_interrupt(
|
||||
@@ -356,7 +291,6 @@ impl PrfItem {
|
||||
Some(timeout),
|
||||
user_agent.clone(),
|
||||
accept_invalid_certs,
|
||||
cvd_headers,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -368,49 +302,10 @@ impl PrfItem {
|
||||
};
|
||||
|
||||
let status_code = resp.status();
|
||||
|
||||
// CVD: classify the response before the generic status check (device-limit is a 403).
|
||||
// Only when CVD is active this fetch (we advertised a public key).
|
||||
let cvd_yaml: Option<std::string::String> = if cvd.is_active() {
|
||||
match crate::cvd::parse_response(status_code, resp.headers(), resp.text_with_charset()?) {
|
||||
crate::cvd::CvdResponse::Encrypted { aead, payload } => match &cvd {
|
||||
// New profile: decrypt with the caller's in-memory key (not yet in the keychain).
|
||||
crate::cvd::CvdMode::New(key) => Some(key.open(aead, &payload)?),
|
||||
// Refresh: read the private key lazily — only now, only for an actual ciphertext.
|
||||
crate::cvd::CvdMode::Existing { .. } => match crate::cvd::load_device_secret(uid.as_str())? {
|
||||
Some(secret) => Some(crate::cvd::open_with_secret(&secret, aead, &payload)?),
|
||||
// Cached public key but no private key → the device was restored, or an earlier
|
||||
// persist failed. This response was sealed to the lost key and is unrecoverable;
|
||||
// signal the caller (which re-registers a fresh key for the next refresh). We
|
||||
// stay pure here — no keychain or profile write inside from_url.
|
||||
None => return Err(anyhow::Error::new(crate::cvd::CvdKeyMissing)),
|
||||
},
|
||||
crate::cvd::CvdMode::Disabled => None, // unreachable: cvd.is_active() == false here
|
||||
},
|
||||
crate::cvd::CvdResponse::DeviceLimit => {
|
||||
bail!("{}", clash_verge_i18n::t!("errors.cvdDeviceLimit"))
|
||||
}
|
||||
crate::cvd::CvdResponse::Error(msg) => bail!(msg),
|
||||
crate::cvd::CvdResponse::Plaintext => None, // airport does not support CVD — use plaintext
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if cvd_yaml.is_none() && !status_code.is_success() {
|
||||
if !status_code.is_success() {
|
||||
bail!("failed to fetch remote profile with status {status_code}")
|
||||
}
|
||||
|
||||
// Keep a device key ONLY when CVD actually engaged (the server returned ciphertext). A
|
||||
// brand-new profile whose airport answered in plaintext drops the key entirely: caching its
|
||||
// public key and persisting its private key would be a pointless keychain write — and on
|
||||
// macOS, a password prompt — for a feature the server isn't using. Refreshes keep their
|
||||
// existing cached key regardless (cvd_yaml.is_some() iff we decrypted ciphertext).
|
||||
let cvd_pub_cache = match cvd {
|
||||
crate::cvd::CvdMode::New(_) if cvd_yaml.is_none() => None,
|
||||
_ => cvd_pub_cache,
|
||||
};
|
||||
|
||||
let header = resp.headers();
|
||||
|
||||
// parse the Subscription UserInfo
|
||||
@@ -479,14 +374,12 @@ impl PrfItem {
|
||||
None => None,
|
||||
};
|
||||
|
||||
let uid = help::get_uid("R").into();
|
||||
let file = format!("{uid}.yaml").into();
|
||||
let name = name
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
|
||||
let data = match cvd_yaml.as_deref() {
|
||||
Some(yaml) => yaml,
|
||||
None => resp.text_with_charset()?,
|
||||
};
|
||||
let data = resp.text_with_charset()?;
|
||||
|
||||
// process the charset "UTF-8 with BOM"
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
@@ -544,9 +437,6 @@ impl PrfItem {
|
||||
..PrfOption::default()
|
||||
}),
|
||||
home,
|
||||
cvd_enabled: None,
|
||||
// Cache the (non-secret) public key so future refreshes need no keychain access.
|
||||
cvd_pub: cvd_pub_cache.map(Into::into),
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(data.into()),
|
||||
})
|
||||
|
||||
@@ -214,23 +214,6 @@ impl IProfiles {
|
||||
bail!("failed to find the profile item \"uid:{uid}\"")
|
||||
}
|
||||
|
||||
/// Set (or clear with `None`) the cached CVD public key for a profile, then persist. Used by the
|
||||
/// refresh recovery to repoint a profile at a freshly re-registered key, or to drop the claim
|
||||
/// when the keychain is unavailable. Touches only `cvd_pub` metadata — never the keychain.
|
||||
pub async fn set_cvd_pub(&mut self, uid: &str, cvd_pub: Option<String>) -> Result<()> {
|
||||
let items = self
|
||||
.items
|
||||
.as_mut()
|
||||
.ok_or_else(|| anyhow::anyhow!("no profiles loaded"))?;
|
||||
for each in items.iter_mut() {
|
||||
if each.uid.as_deref() == Some(uid) {
|
||||
each.cvd_pub = cvd_pub;
|
||||
return self.save_file().await;
|
||||
}
|
||||
}
|
||||
bail!("failed to find the profile item \"uid:{uid}\"")
|
||||
}
|
||||
|
||||
/// be used to update the remote item
|
||||
/// only patch `updated` `extra` `file_data`
|
||||
pub async fn update_item(&mut self, uid: &String, item: &mut PrfItem) -> Result<()> {
|
||||
@@ -546,22 +529,8 @@ impl IProfiles {
|
||||
use crate::config::Config;
|
||||
|
||||
pub async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file_data: Option<String>) -> Result<()> {
|
||||
let (mut built, device_key) = PrfItem::from(item, file_data).await?;
|
||||
profiles_append_item_safe(&mut built).await?;
|
||||
// Write the CVD private key only now that the profile is durably saved (write-key-last), and only
|
||||
// if CVD actually engaged (item cached a pubkey). A plaintext airport leaves cvd_pub None, so we
|
||||
// skip the keychain write (and its macOS prompt) entirely.
|
||||
if let Some(key) = device_key
|
||||
&& built.cvd_pub.is_some()
|
||||
&& let Err(e) = key.persist()
|
||||
{
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"cvd: failed to persist device key after profile create: {e}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
let item = &mut PrfItem::from(item, file_data).await?;
|
||||
profiles_append_item_safe(item).await
|
||||
}
|
||||
|
||||
pub async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
|
||||
@@ -623,16 +592,3 @@ pub async fn profiles_draft_update_item_safe(index: &String, item: &mut PrfItem)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Set (or clear) a profile's cached CVD public key. `with_data_modify` clones-then-commits and
|
||||
/// holds no lock across the await, so this is safe to call from inside a refresh.
|
||||
pub async fn profiles_set_cvd_pub_safe(uid: &str, cvd_pub: Option<String>) -> Result<()> {
|
||||
let uid = uid.to_owned();
|
||||
Config::profiles()
|
||||
.await
|
||||
.with_data_modify(|mut profiles| async move {
|
||||
profiles.set_cvd_pub(&uid, cvd_pub).await?;
|
||||
Ok((profiles, ()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user