chore: Coco app http request headers (#744)

Add the following HTTP headers when making HTTP requests:

- X-OS-NAME
- X-OS-VER
- X-OS-ARCH
- X-APP-NAME
- X-APP-VER
- X-APP-LANG
This commit is contained in:
SteveLauC
2025-07-17 11:31:19 +08:00
committed by GitHub
parent e3a3849fa4
commit 494e2f0d8a
11 changed files with 206 additions and 28 deletions

View File

@@ -38,6 +38,7 @@ Information about release notes of Coco Server is provided here.
- chore: assistant params & styles #753
- chore: make optional fields optional #758
- chore: search-chat components add formatUrl & think data & icons url #765
- chore: Coco app http request headers #744
## 0.6.0 (2025-06-29)

34
src-tauri/Cargo.lock generated
View File

@@ -876,6 +876,7 @@ dependencies = [
"serde_json",
"serde_plain",
"strsim 0.10.0",
"sysinfo",
"tauri",
"tauri-build",
"tauri-nspanel",
@@ -3666,6 +3667,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "num-bigfloat"
version = "1.7.2"
@@ -3984,6 +3994,16 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.1"
@@ -5857,6 +5877,20 @@ dependencies = [
"libc",
]
[[package]]
name = "sysinfo"
version = "0.35.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows 0.59.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"

View File

@@ -105,6 +105,7 @@ url = "2.5.2"
camino = "1.1.10"
tokio-stream = { version = "0.1.17", features = ["io-util"] }
cfg-if = "1.0.1"
sysinfo = "0.35.2"
[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }

View File

@@ -13,6 +13,7 @@ use std::collections::HashSet;
use std::path::Path;
use tauri::Manager;
use third_party::THIRD_PARTY_EXTENSIONS_SEARCH_SOURCE;
use crate::util::platform::Platform;
pub const LOCAL_QUERY_SOURCE_TYPE: &str = "local";
const PLUGIN_JSON_FILE_NAME: &str = "plugin.json";
@@ -22,17 +23,6 @@ fn default_true() -> bool {
true
}
#[derive(Debug, Deserialize, Serialize, Copy, Clone, Hash, PartialEq, Eq, Display)]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
enum Platform {
#[display("macOS")]
Macos,
#[display("Linux")]
Linux,
#[display("windows")]
Windows,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Extension {
/// Extension ID.

View File

@@ -4,7 +4,7 @@ use super::alter_extension_json_file;
use super::canonicalize_relative_icon_path;
use super::Extension;
use super::ExtensionType;
use super::Platform;
use crate::util::platform::Platform;
use super::LOCAL_QUERY_SOURCE_TYPE;
use super::PLUGIN_JSON_FILE_NAME;
use crate::common::document::open;
@@ -48,13 +48,6 @@ pub(crate) static THIRD_PARTY_EXTENSIONS_DIRECTORY: LazyLock<PathBuf> = LazyLock
app_data_dir
});
/// Helper function to determine the current platform.
fn current_platform() -> Platform {
let os_str = std::env::consts::OS;
serde_plain::from_str(os_str).unwrap_or_else(|_e| {
panic!("std::env::consts::OS is [{}], which is not a valid value for [enum Platform], valid values: ['macos', 'linux', 'windows']", os_str)
})
}
pub(crate) async fn list_third_party_extensions(
directory: &Path,
@@ -62,7 +55,7 @@ pub(crate) async fn list_third_party_extensions(
let mut found_invalid_extensions = false;
let mut extensions_dir_iter = read_dir(&directory).await.map_err(|e| e.to_string())?;
let current_platform = current_platform();
let current_platform = Platform::current();
let mut extensions = Vec::new();

View File

@@ -171,6 +171,7 @@ pub fn run() {
extension::built_in::file_search::config::set_file_system_config,
server::synthesize::synthesize,
util::file::get_file_icon,
util::app_lang::update_app_lang,
])
.setup(|app| {
#[cfg(target_os = "macos")]

View File

@@ -1,8 +1,11 @@
use crate::server::servers::{get_server_by_id, get_server_token};
use crate::util::app_lang::get_app_lang;
use crate::util::platform::Platform;
use http::{HeaderName, HeaderValue};
use once_cell::sync::Lazy;
use reqwest::{Client, Method, RequestBuilder};
use std::collections::HashMap;
use std::sync::LazyLock;
use std::time::Duration;
use tokio::sync::Mutex;
@@ -26,6 +29,26 @@ pub static HTTP_CLIENT: Lazy<Mutex<Client>> = Lazy::new(|| {
Mutex::new(new_reqwest_http_client(allow_self_signature))
});
/// These header values won't change during a process's lifetime.
static STATIC_HEADERS: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
HashMap::from([
(
"X-OS-NAME".into(),
Platform::current()
.to_os_name_http_header_str()
.into_owned(),
),
(
"X-OS-VER".into(),
sysinfo::System::os_version()
.expect("sysinfo::System::os_version() should be Some on major systems"),
),
("X-OS-ARCH".into(), sysinfo::System::cpu_arch()),
("X-APP-NAME".into(), "coco-app".into()),
("X-APP-VER".into(), env!("CARGO_PKG_VERSION").into()),
])
});
pub struct HttpClient;
impl HttpClient {
@@ -81,8 +104,32 @@ impl HttpClient {
// Build the request
let mut request_builder = client.request(method.clone(), url);
if let Some(h) = headers {
// Populate the headers defined by us
let mut req_headers = reqwest::header::HeaderMap::new();
for (key, value) in STATIC_HEADERS.iter() {
let key = HeaderName::from_bytes(key.as_bytes())
.expect("headers defined by us should be valid");
let value = HeaderValue::from_str(value.trim()).unwrap_or_else(|e| {
panic!(
"header value [{}] is invalid, error [{}], this should be unreachable",
value, e
);
});
req_headers.insert(key, value);
}
let app_lang = get_app_lang().await.to_string();
req_headers.insert(
"X-APP-LANG",
HeaderValue::from_str(&app_lang).unwrap_or_else(|e| {
panic!(
"header value [{}] is invalid, error [{}], this should be unreachable",
app_lang, e
);
}),
);
// Headers from the function parameter
if let Some(h) = headers {
for (key, value) in h.into_iter() {
match (
HeaderName::from_bytes(key.as_bytes()),

View File

@@ -0,0 +1,62 @@
//! Configuration entry App language is persisted in the frontend code, but we
//! need to access it on the backend.
//!
//! So we duplicate it here **in the MEMORY** and expose a setter method to the
//! frontend so that the value can be updated and stay update-to-date.
use function_name::named;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(non_camel_case_types)]
pub(crate) enum Lang {
en_US,
zh_CN,
}
impl std::fmt::Display for Lang {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Lang::en_US => write!(f, "en_US"),
Lang::zh_CN => write!(f, "zh_CN"),
}
}
}
impl std::str::FromStr for Lang {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"en" => Ok(Lang::en_US),
"zh" => Ok(Lang::zh_CN),
_ => Err(format!("Invalid language: {}", s)),
}
}
}
/// Cache the language config in memory.
static APP_LANG: RwLock<Option<Lang>> = RwLock::const_new(None);
/// Frontend code uses this interface to update the in-memory cached `APP_LANG` config.
#[named]
#[tauri::command]
pub(crate) async fn update_app_lang(lang: String) {
let app_lang = lang.parse::<Lang>().unwrap_or_else(|e| {
panic!(
"frontend code passes an invalid argument [{}] to interface [{}], parsing error [{}]",
lang,
function_name!(),
e
)
});
let mut write_guard = APP_LANG.write().await;
*write_guard = Some(app_lang);
}
/// Helper getter method to handle the `None` case.
pub(crate) async fn get_app_lang() -> Lang {
let opt_lang = *APP_LANG.read().await;
opt_lang.expect("frontend code did not invoke [update_app_lang()] to set the APP_LANG")
}

View File

@@ -1,4 +1,6 @@
pub(crate) mod file;
pub(crate) mod platform;
pub(crate) mod app_lang;
use std::{path::Path, process::Command};
use tauri::{AppHandle, Runtime};

View File

@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
use derive_more::Display;
use std::borrow::Cow;
#[derive(Debug, Deserialize, Serialize, Copy, Clone, Hash, PartialEq, Eq, Display)]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
pub(crate) enum Platform {
#[display("macOS")]
Macos,
#[display("Linux")]
Linux,
#[display("windows")]
Windows,
}
impl Platform {
/// Helper function to determine the current platform.
pub(crate) fn current() -> Platform {
let os_str = std::env::consts::OS;
serde_plain::from_str(os_str).unwrap_or_else(|_e| {
panic!("std::env::consts::OS is [{}], which is not a valid value for [enum Platform], valid values: ['macos', 'linux', 'windows']", os_str)
})
}
/// Return the `X-OS-NAME` HTTP request header.
pub(crate) fn to_os_name_http_header_str(&self) -> Cow<'static, str> {
match self {
Self::Macos => {
Cow::Borrowed("macos")
}
Self::Windows => {
Cow::Borrowed("windows")
}
// For Linux, we need the actual distro `ID`, not just a "linux".
Self::Linux => {
Cow::Owned(sysinfo::System::distribution_id())
}
}
}
}

View File

@@ -30,6 +30,7 @@ import {
change_shortcut,
unregister_shortcut,
} from "@/commands";
import platformAdapter from "@/utils/platformAdapter";
export function ThemeOption({
icon: Icon,
@@ -74,10 +75,13 @@ export default function GeneralSettings() {
const [launchAtLogin, setLaunchAtLogin] = useState(true);
const showTooltip = useAppStore((state) => state.showTooltip);
const setShowTooltip = useAppStore((state) => state.setShowTooltip);
const language = useAppStore((state) => state.language);
const setLanguage = useAppStore((state) => state.setLanguage);
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
useEffect(() => {
platformAdapter.invokeBackend("update_app_lang", {
lang: language,
});
}, [language]);
const fetchAutoStartStatus = async () => {
if (isTauri()) {
@@ -251,7 +255,9 @@ export default function GeneralSettings() {
<div className="flex items-center gap-2">
<select
value={currentLanguage}
onChange={(e) => setLanguage(e.target.value)}
onChange={(e) => {
setLanguage(e.currentTarget.value);
}}
className="px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="en">{t("settings.language.english")}</option>