license api

This commit is contained in:
weisd
2025-04-07 17:13:43 +08:00
committed by weisd
parent 3dbdd38889
commit 885944802a
11 changed files with 335 additions and 2 deletions
Generated
+123 -1
View File
@@ -200,6 +200,19 @@ dependencies = [
"url",
]
[[package]]
name = "appauth"
version = "0.0.1"
dependencies = [
"base64-simd",
"common",
"hex-simd",
"rand 0.8.5",
"rsa",
"serde",
"serde_json",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
@@ -1418,6 +1431,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.0"
@@ -2314,6 +2333,17 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.4.1"
@@ -2375,6 +2405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"const-oid 0.9.6",
"crypto-common 0.1.6",
"subtle",
]
@@ -2386,7 +2417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c478574b20020306f98d61c8ca3322d762e1ff08117422ac6106438605ea516"
dependencies = [
"block-buffer 0.11.0-rc.4",
"const-oid",
"const-oid 0.10.0",
"crypto-common 0.2.0-rc.2",
"subtle",
]
@@ -4510,6 +4541,9 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "lazycell"
@@ -5149,6 +5183,23 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -5849,6 +5900,15 @@ dependencies = [
"serde",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -6083,6 +6143,27 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -6919,6 +7000,26 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "rsa"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [
"const-oid 0.9.6",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-embed"
version = "8.6.0"
@@ -6997,6 +7098,7 @@ name = "rustfs"
version = "0.1.0"
dependencies = [
"api",
"appauth",
"async-trait",
"atoi",
"axum",
@@ -7638,6 +7740,16 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core 0.6.4",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@@ -7798,6 +7910,16 @@ version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sqlparser"
version = "0.54.0"
+1 -1
View File
@@ -13,7 +13,7 @@ members = [
"cli/rustfs-gui", # Graphical user interface client
"packages/obs", # Observability utilities
"s3select/api",
"s3select/query",
"s3select/query", "appauth",
]
resolver = "2"
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "appauth"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
base64-simd = "0.8.0"
common.workspace = true
hex-simd = "0.8.0"
rand.workspace = true
rsa = "0.9.8"
serde.workspace = true
serde_json.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
pub mod token;
+110
View File
@@ -0,0 +1,110 @@
use common::error::Result;
use rsa::Pkcs1v15Encrypt;
use rsa::{
pkcs8::{DecodePrivateKey, DecodePublicKey},
RsaPrivateKey, RsaPublicKey,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct Token {
pub name: String, // 应用ID
pub expired: u64, // 到期时间 (UNIX时间戳)
}
// 公钥生成Token
// [token] Token对象
// [key] 公钥字符串
// 返回base64处理的加密字符串
pub fn gencode(token: &Token, key: &str) -> Result<String> {
let data = serde_json::to_vec(token)?;
let public_key = RsaPublicKey::from_public_key_pem(key)?;
let encrypted_data = public_key.encrypt(&mut rand::thread_rng(), Pkcs1v15Encrypt, &data)?;
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data))
}
// 私钥解析Token
// [token] base64处理的加密字符串
// [key] 私钥字符串
// 返回Token对象
pub fn parse(token: &str, key: &str) -> Result<Token> {
let encrypted_data = base64_simd::URL_SAFE_NO_PAD.decode_to_vec(token.as_bytes())?;
let private_key = RsaPrivateKey::from_pkcs8_pem(key)?;
let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data)?;
let res: Token = serde_json::from_slice(&decrypted_data)?;
Ok(res)
}
pub fn parse_license(license: &str) -> Result<Token> {
parse(license, TEST_PRIVATE_KEY)
// match parse(license, TEST_PRIVATE_KEY) {
// Ok(token) => {
// if token.expired > SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() {
// Ok(token)
// } else {
// Err("Token expired".into())
// }
// }
// Err(e) => Err(e),
// }
}
static TEST_PRIVATE_KEY:&str ="-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj86SrJIuxSxR6\nBJ/dlJEUIj6NeBRnhLQlCDdovuz61+7kJXVcxaR66w4m8W7SLEUP+IlPtnn6vmiG\n7XMhGNHIr7r1JsEVVLhZmL3tKI66DEZl786ZhG81BWqUlmcooIPS8UEPZNqJXLuz\nVGhxNyVGbj/tV7QC2pSISnKaixc+nrhxvo7w56p5qrm9tik0PjTgfZsUePkoBsSN\npoRkAauS14MAzK6HGB75CzG3dZqXUNWSWVocoWtQbZUwFGXyzU01ammsHQDvc2xu\nK1RQpd1qYH5bOWZ0N0aPFwT0r59HztFXg9sbjsnuhO1A7OiUOkc6iGVuJ0wm/9nA\nwZIBqzgjAgMBAAECggEAPMpeSEbotPhNw2BrllE76ec4omPfzPJbiU+em+wPGoNu\nRJHPDnMKJbl6Kd5jZPKdOOrCnxfd6qcnQsBQa/kz7+GYxMV12l7ra+1Cnujm4v0i\nLTHZvPpp8ZLsjeOmpF3AAzsJEJgon74OqtOlVjVIUPEYKvzV9ijt4gsYq0zfdYv0\nhrTMzyrGM4/UvKLsFIBROAfCeWfA7sXLGH8JhrRAyDrtCPzGtyyAmzoHKHtHafcB\nuyPFw/IP8otAgpDk5iiQPNkH0WwzAQIm12oHuNUa66NwUK4WEjXTnDg8KeWLHHNv\nIfN8vdbZchMUpMIvvkr7is315d8f2cHCB5gEO+GWAQKBgQDR/0xNll+FYaiUKCPZ\nvkOCAd3l5mRhsqnjPQ/6Ul1lAyYWpoJSFMrGGn/WKTa/FVFJRTGbBjwP+Mx10bfb\ngUg2GILDTISUh54fp4zngvTi9w4MWGKXrb7I1jPkM3vbJfC/v2fraQ/r7qHPpO2L\nf6ZbGxasIlSvr37KeGoelwcAQQKBgQDH3hmOTS2Hl6D4EXdq5meHKrfeoicGN7m8\noQK7u8iwn1R9zK5nh6IXxBhKYNXNwdCQtBZVRvFjjZ56SZJb7lKqa1BcTsgJfZCy\nnI3Uu4UykrECAH8AVCVqBXUDJmeA2yE+gDAtYEjvhSDHpUfWxoGHr0B/Oqk2Lxc/\npRy1qV5fYwKBgBWSL/hYVf+RhIuTg/s9/BlCr9SJ0g3nGGRrRVTlWQqjRCpXeFOO\nJzYqSq9pFGKUggEQxoOyJEFPwVDo9gXqRcyov+Xn2kaXl7qQr3yoixc1YZALFDWY\nd1ySBEqQr0xXnV9U/gvEgwotPRnjSzNlLWV2ZuHPtPtG/7M0o1H5GZMBAoGAKr3N\nW0gX53o+my4pCnxRQW+aOIsWq1a5aqRIEFudFGBOUkS2Oz+fI1P1GdrRfhnnfzpz\n2DK+plp/vIkFOpGhrf4bBlJ2psjqa7fdANRFLMaAAfyXLDvScHTQTCcnVUAHQPVq\n2BlSH56pnugyj7SNuLV6pnql+wdhAmRN2m9o1h8CgYAbX2juSr4ioXwnYjOUdrIY\n4+ERvHcXdjoJmmPcAm4y5NbSqLXyU0FQmplNMt2A5LlniWVJ9KNdjAQUt60FZw/+\nr76LdxXaHNZghyx0BOs7mtq5unSQXamZ8KixasfhE9uz3ij1jXjG6hafWkS8/68I\nuWbaZqgvy7a9oPHYlKH7Jg==\n-----END PRIVATE KEY-----\n";
#[cfg(test)]
mod tests {
use super::*;
use rsa::{
pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding},
RsaPrivateKey,
};
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_gencode_and_parse() {
let mut rng = rand::thread_rng();
let bits = 2048;
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
let public_key = RsaPublicKey::from(&private_key);
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
let token = Token {
name: "test_app".to_string(),
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
};
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token");
assert_eq!(token.name, decoded.name);
assert_eq!(token.expired, decoded.expired);
}
#[test]
fn test_parse_invalid_token() {
let private_key_pem = RsaPrivateKey::new(&mut rand::thread_rng(), 2048)
.expect("Failed to generate private key")
.to_pkcs8_pem(LineEnding::LF)
.unwrap();
let invalid_token = "invalid_base64_token";
let result = parse(invalid_token, &private_key_pem);
assert!(result.is_err());
}
#[test]
fn test_gencode_with_invalid_key() {
let token = Token {
name: "test_app".to_string(),
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
};
let invalid_key = "invalid_public_key";
let result = gencode(&token, invalid_key);
assert!(result.is_err());
}
}
+1
View File
@@ -87,6 +87,7 @@ rust-embed = { workspace = true, features = ["interpolate-folder-path"] }
local-ip-address = { workspace = true }
chrono = { workspace = true }
rustfs-obs = { workspace = true }
appauth = { version = "0.0.1", path = "../appauth" }
[build-dependencies]
prost-build.workspace = true
+17
View File
@@ -2,6 +2,7 @@ use clap::Parser;
use const_str::concat;
use ecstore::global::DEFAULT_PORT;
use std::string::ToString;
use std::sync::OnceLock;
shadow_rs::shadow!(build);
@@ -100,4 +101,20 @@ pub struct Opt {
/// tls path for rustfs api and console.
#[arg(long, env = "RUSTFS_TLS_PATH")]
pub tls_path: Option<String>,
#[arg(long, env = "RUSTFS_LICENSE")]
pub license: Option<String>,
}
lazy_static::lazy_static! {
pub static ref OPT: OnceLock<Opt> = OnceLock::new();
}
pub fn init_config() {
let opt = Opt::parse();
OPT.set(opt).expect("Failed to set global config");
}
pub fn get_config() -> &'static Opt {
OPT.get().expect("Global config not initialized")
}
+24
View File
@@ -1,3 +1,4 @@
use crate::config;
use axum::{
body::Body,
extract::Host,
@@ -158,6 +159,28 @@ pub(crate) fn init_console_cfg(local_ip: Ipv4Addr, port: u16) {
// host.parse::<SocketAddr>().is_ok() || host.parse::<IpAddr>().is_ok()
// }
async fn license_handler() -> impl IntoResponse {
let license = config::get_config()
.license
.as_ref()
.map(|license| {
if license.is_empty() {
return None;
}
match appauth::token::parse_license(license) {
Ok(token) => Some(token),
Err(_) => None,
}
})
.unwrap_or_default();
Response::builder()
.header("content-type", "application/json")
.status(StatusCode::OK)
.body(Body::from(serde_json::to_string(&license).unwrap_or_default()))
.unwrap()
}
#[allow(clippy::const_is_empty)]
async fn config_handler(Host(host): Host) -> impl IntoResponse {
let host_with_port = if host.contains(':') { host } else { format!("{}:80", host) };
@@ -203,6 +226,7 @@ pub async fn start_static_file_server(
) {
// Create a route
let app = Router::new()
.route("/license", get(license_handler))
.route("/config.json", get(config_handler))
.nest_service("/", get(static_handler));
let local_addr: SocketAddr = addrs.parse().expect("Failed to parse socket address");
+29
View File
@@ -0,0 +1,29 @@
use crate::config;
use common::error::{Error, Result};
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tracing::error;
use tracing::info;
pub fn license_check() -> Result<()> {
let inval_license = config::get_config().license.as_ref().map(|license| {
if license.is_empty() {
error!("License is empty");
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
}
let token = appauth::token::parse_license(license)?;
if token.expired < SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() {
error!("License expired");
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
}
info!("License is valid ! expired at {}", token.expired);
Ok(())
});
if inval_license.is_none() || inval_license.is_some_and(|v| v.is_err()) {
return Err(Error::from_string("Incorrect license, please contact RustFS.".to_string()));
}
Ok(())
}
+4
View File
@@ -3,6 +3,7 @@ mod auth;
mod config;
mod console;
mod grpc;
pub mod license;
mod logging;
mod service;
mod storage;
@@ -11,6 +12,7 @@ mod utils;
use crate::auth::IAMAuth;
use crate::console::{init_console_cfg, CONSOLE_CONFIG};
use crate::utils::error;
// Ensure the correct path for parse_license is imported
use chrono::Datelike;
use clap::Parser;
use common::{
@@ -92,6 +94,8 @@ fn print_server_info() {
#[tokio::main]
async fn main() -> Result<()> {
config::init_config();
// Parse the obtained parameters
let opt = config::Opt::parse();
+6
View File
@@ -1,5 +1,6 @@
use super::ecfs::FS;
use crate::auth::{check_key_valid, get_condition_values, get_session_token};
use crate::license::license_check;
use ecstore::bucket::policy_sys::PolicySys;
use iam::error::Error as IamError;
use policy::auth;
@@ -180,6 +181,8 @@ impl S3Access for FS {
///
/// This method returns `Ok(())` by default.
async fn create_bucket(&self, req: &mut S3Request<CreateBucketInput>) -> S3Result<()> {
license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?;
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
@@ -241,6 +244,7 @@ impl S3Access for FS {
///
/// This method returns `Ok(())` by default.
async fn create_multipart_upload(&self, _req: &mut S3Request<CreateMultipartUploadInput>) -> S3Result<()> {
license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?;
Ok(())
}
@@ -995,6 +999,8 @@ impl S3Access for FS {
///
/// This method returns `Ok(())` by default.
async fn put_object(&self, req: &mut S3Request<PutObjectInput>) -> S3Result<()> {
license_check().map_err(|er| s3_error!(AccessDenied, "{:?}", er.to_string()))?;
let req_info = req.extensions.get_mut::<ReqInfo>().expect("ReqInfo not found");
req_info.bucket = Some(req.input.bucket.clone());
req_info.object = Some(req.input.key.clone());