use crate::core::error::{AppError, Result};
use serde_json::{Value, json};
use std::io::Write;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Ports {
pub http: u16,
pub events: u16,
pub control: u16,
}
impl Ports {
pub fn validate(self) -> Result<()> {
let ports = [self.http, self.events, self.control];
if ports.iter().any(|port| *port < 1024)
|| self.http == self.events
|| self.http == self.control
|| self.events == self.control
{
return Err(AppError::new(
"RUNTIME_CONFIG_INVALID",
"Managed endpoints require three distinct non-privileged ports.",
4,
));
}
Ok(())
}
}
pub struct Bundle {
version: &'static str,
ports: Ports,
api: String,
control: String,
jwt: String,
files: Vec<ConfigFile>,
}
pub struct ConfigFile {
name: &'static str,
bytes: Vec<u8>,
}
impl ConfigFile {
pub fn name(&self) -> &'static str {
self.name
}
pub fn write_to(&self, mut destination: impl Write) -> Result<()> {
destination.write_all(&self.bytes).map_err(|_| {
AppError::new(
"RUNTIME_CONFIG_IO",
"Managed configuration could not be written privately.",
6,
)
})
}
}
impl Bundle {
pub fn generate(version: &str, ports: Ports) -> Result<Self> {
let release = crate::napcat::install::catalog::lookup(version)?;
ports.validate()?;
let mut random = [0_u8; 96];
getrandom::fill(&mut random).map_err(|_| {
AppError::new(
"RANDOM_UNAVAILABLE",
"Managed credentials could not be generated.",
6,
)
})?;
let secrets = random.as_chunks::<32>().0;
let [api, control, jwt] = std::array::from_fn(|index| {
secrets[index]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
});
if api == control || api == jwt || control == jwt {
return Err(AppError::new(
"RANDOM_UNAVAILABLE",
"Managed credential domains were not independent.",
6,
));
}
Self::restore(release.version, ports, api, control, jwt)
}
pub(crate) fn restore(
version: &str,
ports: Ports,
api: String,
control: String,
jwt: String,
) -> Result<Self> {
let release = crate::napcat::install::catalog::lookup(version)?;
ports.validate()?;
if [&api, &control, &jwt].iter().any(|token| {
token.len() != 64
|| !token
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}) || api == control
|| api == jwt
|| control == jwt
{
return Err(AppError::new(
"RUNTIME_CREDENTIAL_INVALID",
"Managed credential material is invalid or inconsistent.",
6,
));
}
let files = vec![
file(
"napcat.json",
json!({"fileLog":false,"consoleLog":false,"fileLogLevel":"error","consoleLogLevel":"error"}),
)?,
file(
"webui.json",
json!({"host":"127.0.0.1","port":ports.control,"token":control,"autoLoginAccount":"","disableWebUI":false,"loginRate":10,"accessControlMode":"whitelist","ipWhitelist":["127.0.0.1"],"ipBlacklist":[],"enableXForwardedFor":false,"enable2FA":false,"totpSecret":""}),
)?,
file(
"onebot11.json",
json!({"network":{
"httpServers":[{"name":"qq-cli-http","enable":true,"host":"127.0.0.1","port":ports.http,"token":api,"enableCors":false,"enableWebsocket":false,"messagePostFormat":"array","debug":false}],
"websocketServers":[{"name":"qq-cli-events","enable":true,"host":"127.0.0.1","port":ports.events,"token":api,"messagePostFormat":"array","reportSelfMessage":true,"enableForcePushEvent":true,"heartInterval":30000,"debug":false}],
"httpSseServers":[],"httpClients":[],"websocketClients":[],"plugins":[]},
"enableLocalFile2Url":true,"parseMultMsg":false,"imageDownloadProxy":"","musicSignUrl":"",
"timeout":{"baseTimeout":10000,"uploadSpeedKBps":256,"downloadSpeedKBps":256,"maxTimeout":300000}
}),
)?,
];
Ok(Self {
version: release.version,
ports,
api,
control,
jwt,
files,
})
}
pub fn version(&self) -> &'static str {
self.version
}
pub fn ports(&self) -> Ports {
self.ports
}
pub fn files(&self) -> impl Iterator<Item = &ConfigFile> {
self.files.iter()
}
pub fn api_token(&self) -> &str {
&self.api
}
pub fn control_token(&self) -> &str {
&self.control
}
pub(crate) fn jwt_secret(&self) -> &str {
&self.jwt
}
pub fn visit_environment(
&self,
mut visitor: impl FnMut(&'static str, &str) -> Result<()>,
) -> Result<()> {
visitor("NAPCAT_WEBUI_JWT_SECRET_KEY", &self.jwt)
}
}
fn file(name: &'static str, value: Value) -> Result<ConfigFile> {
let bytes = serde_json::to_vec(&value).map_err(|_| AppError::protocol())?;
Ok(ConfigFile { name, bytes })
}