mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-24 10:34:08 +03:00
feat(native): split package (#8853)
This commit is contained in:
parent
9642566086
commit
1c2b23b160
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -17,10 +17,21 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rand",
|
||||
"rayon",
|
||||
"sha3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "affine_native"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"affine_common",
|
||||
"affine_schema",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -49,6 +60,7 @@ version = "0.0.0"
|
||||
name = "affine_server_native"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"affine_common",
|
||||
"chrono",
|
||||
"file-format",
|
||||
"mimalloc",
|
||||
|
@ -1,8 +1,14 @@
|
||||
[workspace]
|
||||
members = ["./packages/backend/native", "./packages/frontend/native", "./packages/frontend/native/schema"]
|
||||
members = [
|
||||
"./packages/backend/native",
|
||||
"./packages/common/native",
|
||||
"./packages/frontend/native",
|
||||
"./packages/frontend/native/schema"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
affine_common = { path = "./packages/common/native" }
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15"
|
||||
|
@ -7,6 +7,7 @@ version = "1.0.0"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
affine_common = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
file-format = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
|
@ -1 +0,0 @@
|
||||
../../../frontend/native/src/hashcash.rs
|
69
packages/backend/native/src/hashcash.rs
Normal file
69
packages/backend/native/src/hashcash.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = JsBoolean;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
stamp.check(self.bits, &self.resource)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn verify_challenge_response(
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
) -> AsyncTask<AsyncVerifyChallengeResponse> {
|
||||
AsyncTask::new(AsyncVerifyChallengeResponse {
|
||||
response,
|
||||
bits,
|
||||
resource,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AsyncMintChallengeResponse {
|
||||
bits: Option<u32>,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = JsString;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn mint_challenge_response(
|
||||
resource: String,
|
||||
bits: Option<u32>,
|
||||
) -> AsyncTask<AsyncMintChallengeResponse> {
|
||||
AsyncTask::new(AsyncMintChallengeResponse { bits, resource })
|
||||
}
|
12
packages/common/native/Cargo.toml
Normal file
12
packages/common/native/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rayon = { workspace = true }
|
204
packages/common/native/src/hashcash.rs
Normal file
204
packages/common/native/src/hashcash.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, Distribution},
|
||||
thread_rng,
|
||||
};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
const SALT_LENGTH: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Stamp {
|
||||
version: String,
|
||||
claim: u32,
|
||||
ts: String,
|
||||
resource: String,
|
||||
ext: String,
|
||||
rand: String,
|
||||
counter: String,
|
||||
}
|
||||
|
||||
impl Stamp {
|
||||
fn check_expiration(&self) -> bool {
|
||||
NaiveDateTime::parse_from_str(&self.ts, "%Y%m%d%H%M%S")
|
||||
.ok()
|
||||
.map(|ts| DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc))
|
||||
.and_then(|utc| {
|
||||
utc
|
||||
.checked_add_signed(Duration::minutes(5))
|
||||
.map(|utc| Utc::now() <= utc)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn check<S: AsRef<str>>(&self, bits: u32, resource: S) -> bool {
|
||||
if self.version == "1"
|
||||
&& bits <= self.claim
|
||||
&& self.check_expiration()
|
||||
&& self.resource == resource.as_ref()
|
||||
{
|
||||
let hex_digits = ((self.claim as f32) / 4.).floor() as usize;
|
||||
|
||||
// check challenge
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(self.format().as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize());
|
||||
result[..hex_digits] == String::from_utf8(vec![b'0'; hex_digits]).unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}",
|
||||
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
|
||||
)
|
||||
}
|
||||
|
||||
/// Mint a new hashcash stamp.
|
||||
pub fn mint(resource: String, bits: Option<u32>) -> Self {
|
||||
let version = "1";
|
||||
let now = Utc::now();
|
||||
let ts = now.format("%Y%m%d%H%M%S");
|
||||
let bits = bits.unwrap_or(20);
|
||||
let rand = String::from_iter(
|
||||
Alphanumeric
|
||||
.sample_iter(thread_rng())
|
||||
.take(SALT_LENGTH)
|
||||
.map(char::from),
|
||||
);
|
||||
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, &resource, "", rand);
|
||||
|
||||
Stamp {
|
||||
version: version.to_string(),
|
||||
claim: bits,
|
||||
ts: ts.to_string(),
|
||||
resource,
|
||||
ext: "".to_string(),
|
||||
rand,
|
||||
counter: {
|
||||
let mut hasher = Sha3_256::new();
|
||||
let mut counter = 0;
|
||||
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
|
||||
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
|
||||
loop {
|
||||
hasher.update(format!("{}:{:x}", challenge, counter).as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize_reset());
|
||||
if result[..hex_digits] == zeros {
|
||||
break format!("{:x}", counter);
|
||||
};
|
||||
counter += 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Stamp {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let stamp_vec = value.split(':').collect::<Vec<&str>>();
|
||||
if stamp_vec.len() != 7
|
||||
|| stamp_vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(i, s)| i != 4 && s.is_empty())
|
||||
{
|
||||
return Err(format!(
|
||||
"Malformed stamp, expected 6 parts, got {}",
|
||||
stamp_vec.len()
|
||||
));
|
||||
}
|
||||
Ok(Stamp {
|
||||
version: stamp_vec[0].to_string(),
|
||||
claim: stamp_vec[1]
|
||||
.parse()
|
||||
.map_err(|_| "Malformed stamp".to_string())?,
|
||||
ts: stamp_vec[2].to_string(),
|
||||
resource: stamp_vec[3].to_string(),
|
||||
ext: stamp_vec[4].to_string(),
|
||||
rand: stamp_vec[5].to_string(),
|
||||
counter: stamp_vec[6].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Stamp;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use rayon::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_mint() {
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should pass"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(19)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should fail with lower bits"
|
||||
);
|
||||
}
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test2"),
|
||||
"should fail with different resource"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_expiration() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert!(response.check_expiration());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert_eq!(
|
||||
response.format(),
|
||||
format!(
|
||||
"1:20:{}:test::{}:{}",
|
||||
response.ts, response.rand, response.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz() {
|
||||
(0..1000).into_par_iter().for_each(|_| {
|
||||
let bit = rand::random::<u32>() % 20 + 1;
|
||||
let resource = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let response = Stamp::mint(resource.clone(), Some(bit)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(bit, resource),
|
||||
"should pass"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
1
packages/common/native/src/lib.rs
Normal file
1
packages/common/native/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod hashcash;
|
@ -7,6 +7,7 @@ version = "0.0.0"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
affine_common = { workspace = true }
|
||||
affine_schema = { path = "./schema" }
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
@ -1,133 +1,8 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, Distribution},
|
||||
thread_rng,
|
||||
};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
const SALT_LENGTH: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Stamp {
|
||||
version: String,
|
||||
claim: u32,
|
||||
ts: String,
|
||||
resource: String,
|
||||
ext: String,
|
||||
rand: String,
|
||||
counter: String,
|
||||
}
|
||||
|
||||
impl Stamp {
|
||||
fn check_expiration(&self) -> bool {
|
||||
NaiveDateTime::parse_from_str(&self.ts, "%Y%m%d%H%M%S")
|
||||
.ok()
|
||||
.map(|ts| DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc))
|
||||
.and_then(|utc| {
|
||||
utc
|
||||
.checked_add_signed(Duration::minutes(5))
|
||||
.map(|utc| Utc::now() <= utc)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn check<S: AsRef<str>>(&self, bits: u32, resource: S) -> bool {
|
||||
if self.version == "1"
|
||||
&& bits <= self.claim
|
||||
&& self.check_expiration()
|
||||
&& self.resource == resource.as_ref()
|
||||
{
|
||||
let hex_digits = ((self.claim as f32) / 4.).floor() as usize;
|
||||
|
||||
// check challenge
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(self.format().as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize());
|
||||
result[..hex_digits] == String::from_utf8(vec![b'0'; hex_digits]).unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn format(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}",
|
||||
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
|
||||
)
|
||||
}
|
||||
|
||||
/// Mint a new hashcash stamp.
|
||||
pub fn mint(resource: String, bits: Option<u32>) -> Self {
|
||||
let version = "1";
|
||||
let now = Utc::now();
|
||||
let ts = now.format("%Y%m%d%H%M%S");
|
||||
let bits = bits.unwrap_or(20);
|
||||
let rand = String::from_iter(
|
||||
Alphanumeric
|
||||
.sample_iter(thread_rng())
|
||||
.take(SALT_LENGTH)
|
||||
.map(char::from),
|
||||
);
|
||||
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, &resource, "", rand);
|
||||
|
||||
Stamp {
|
||||
version: version.to_string(),
|
||||
claim: bits,
|
||||
ts: ts.to_string(),
|
||||
resource,
|
||||
ext: "".to_string(),
|
||||
rand,
|
||||
counter: {
|
||||
let mut hasher = Sha3_256::new();
|
||||
let mut counter = 0;
|
||||
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
|
||||
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
|
||||
loop {
|
||||
hasher.update(format!("{}:{:x}", challenge, counter).as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize_reset());
|
||||
if result[..hex_digits] == zeros {
|
||||
break format!("{:x}", counter);
|
||||
};
|
||||
counter += 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Stamp {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let stamp_vec = value.split(':').collect::<Vec<&str>>();
|
||||
if stamp_vec.len() != 7
|
||||
|| stamp_vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(i, s)| i != 4 && s.is_empty())
|
||||
{
|
||||
return Err(format!(
|
||||
"Malformed stamp, expected 6 parts, got {}",
|
||||
stamp_vec.len()
|
||||
));
|
||||
}
|
||||
Ok(Stamp {
|
||||
version: stamp_vec[0].to_string(),
|
||||
claim: stamp_vec[1]
|
||||
.parse()
|
||||
.map_err(|_| "Malformed stamp".to_string())?,
|
||||
ts: stamp_vec[2].to_string(),
|
||||
resource: stamp_vec[3].to_string(),
|
||||
ext: stamp_vec[4].to_string(),
|
||||
rand: stamp_vec[5].to_string(),
|
||||
counter: stamp_vec[6].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
response: String,
|
||||
@ -192,79 +67,3 @@ pub fn mint_challenge_response(
|
||||
) -> AsyncTask<AsyncMintChallengeResponse> {
|
||||
AsyncTask::new(AsyncMintChallengeResponse { bits, resource })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Stamp;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use rayon::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_mint() {
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should pass"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(19)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should fail with lower bits"
|
||||
);
|
||||
}
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test2"),
|
||||
"should fail with different resource"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_expiration() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert!(response.check_expiration());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert_eq!(
|
||||
response.format(),
|
||||
format!(
|
||||
"1:20:{}:test::{}:{}",
|
||||
response.ts, response.rand, response.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz() {
|
||||
(0..1000).into_par_iter().for_each(|_| {
|
||||
let bit = rand::random::<u32>() % 20 + 1;
|
||||
let resource = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let response = Stamp::mint(resource.clone(), Some(bit)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(bit, resource),
|
||||
"should pass"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user