fix: use droplet-rs crate

This commit is contained in:
DecDuck
2025-12-01 11:23:20 +11:00
parent e219ea13fb
commit 2a746f22ac
10 changed files with 341 additions and 835 deletions
Generated
+321 -355
View File
File diff suppressed because it is too large Load Diff
+9 -16
View File
@@ -1,7 +1,7 @@
[package]
edition = "2021"
name = "droplet"
version = "0.7.0"
version = "0.3.5"
license = "AGPL-3.0-only"
description = "Droplet is a `napi.rs` Rust/Node.js package full of high-performance and low-level utils for Drop"
[lib]
@@ -9,30 +9,23 @@ crate-type = ["cdylib"]
[dependencies]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "3.0.0-beta.11", default-features = false, features = ["napi6", "async", "web_stream", "error_anyhow"] }
napi = { version = "3.0.0-beta.11", default-features = false, features = [
"napi6",
"async",
"web_stream",
"error_anyhow",
] }
napi-derive = "3.0.0-beta.11"
hex = "0.4.3"
md5 = "0.7.0"
time-macros = "0.2.22"
time = "0.3.41"
webpki = "0.22.4"
ring = "0.17.14"
tokio = { version = "1.45.1", features = ["fs", "io-util"] }
tokio-util = { version = "0.7.15", features = ["codec"] }
dyn-clone = "1.0.20"
rhai = "1.22.2"
# mlua = { version = "0.11.2", features = ["luajit"] }
boa_engine = "0.20.0"
serde_json = "1.0.143"
anyhow = "1.0.99"
[dependencies.x509-parser]
version = "0.17.0"
features = ["verify"]
[dependencies.rcgen]
version = "0.13.2"
features = ["crypto", "pem", "x509-parser"]
anyhow = "*"
droplet-rs = { git = "https://github.com/Drop-OSS/droplet-rs.git" }
[dependencies.serde]
version = "1.0.210"
Vendored
+1 -2
View File
@@ -1,7 +1,6 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Persistent object so we can cache things between commands
/** * Persistent object so we can cache things between commands
*/
export declare class DropletHandler {
constructor()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@drop-oss/droplet",
"version": "3.5.0",
"version": "3.5.1",
"main": "index.js",
"types": "index.d.ts",
"napi": {
+2 -1
View File
@@ -1,5 +1,6 @@
use std::{collections::HashMap, sync::Arc, thread};
use droplet_rs::versions::types::VersionBackend;
use napi::{
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
Result,
@@ -7,7 +8,7 @@ use napi::{
use serde_json::json;
use uuid::Uuid;
use crate::version::{types::VersionBackend, utils::DropletHandler};
use crate::version::DropletHandler;
const CHUNK_SIZE: usize = 1024 * 1024 * 64;
+6 -106
View File
@@ -1,129 +1,29 @@
use anyhow::anyhow;
use rcgen::{
CertificateParams, DistinguishedName, IsCa, KeyPair, KeyUsagePurpose, PublicKeyData,
SubjectPublicKeyInfo,
};
use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, VerificationAlgorithm};
use time::{Duration, OffsetDateTime};
use x509_parser::parse_x509_certificate;
use x509_parser::pem::Pem;
#[napi]
pub fn generate_root_ca() -> anyhow::Result<Vec<String>> {
let mut params = CertificateParams::default();
let mut name = DistinguishedName::new();
name.push(rcgen::DnType::CommonName, "Drop Root Server");
name.push(rcgen::DnType::OrganizationName, "Drop");
params.distinguished_name = name;
params.not_before = OffsetDateTime::now_utc();
params.not_after = OffsetDateTime::now_utc()
.checked_add(Duration::days(365 * 1000))
.ok_or(anyhow!("failed to calculate end date"))?;
params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
];
let key_pair = KeyPair::generate()?;
let certificate = CertificateParams::self_signed(params, &key_pair)?;
// Returns certificate, then private key
Ok(vec![certificate.pem(), key_pair.serialize_pem()])
Ok(droplet_rs::ssl::generate_root_ca()?)
}
#[napi]
pub fn generate_client_certificate(
client_id: String,
_client_name: String,
client_name: String,
root_ca: String,
root_ca_private: String,
) -> anyhow::Result<Vec<String>> {
let root_key_pair = KeyPair::from_pem(&root_ca_private)?;
let certificate_params = CertificateParams::from_ca_cert_pem(&root_ca)?;
let root_ca = CertificateParams::self_signed(certificate_params, &root_key_pair)?;
let mut params = CertificateParams::default();
let mut name = DistinguishedName::new();
name.push(rcgen::DnType::CommonName, client_id);
name.push(rcgen::DnType::OrganizationName, "Drop");
params.distinguished_name = name;
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::DataEncipherment,
];
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P384_SHA384)?;
let certificate = CertificateParams::signed_by(params, &key_pair, &root_ca, &root_key_pair)?;
// Returns certificate, then private key
Ok(vec![certificate.pem(), key_pair.serialize_pem()])
Ok(droplet_rs::ssl::generate_client_certificate(client_id, client_name, root_ca, root_ca_private)?)
}
#[napi]
pub fn verify_client_certificate(client_cert: String, root_ca: String) -> anyhow::Result<bool> {
let root_ca = Pem::iter_from_buffer(root_ca.as_bytes())
.next()
.ok_or(anyhow!("no certificates in root ca"))??;
let root_ca = root_ca.parse_x509()?;
let client_cert = Pem::iter_from_buffer(client_cert.as_bytes())
.next()
.ok_or(anyhow!("No client certs in chain."))??;
let client_cert = client_cert.parse_x509()?;
let valid = root_ca
.verify_signature(Some(client_cert.public_key()))
.is_ok();
Ok(valid)
Ok(droplet_rs::ssl::verify_client_certificate(client_cert, root_ca)?)
}
#[napi]
pub fn sign_nonce(private_key: String, nonce: String) -> anyhow::Result<String> {
let rng = SystemRandom::new();
let key_pair = KeyPair::from_pem(&private_key)?;
let key_pair = EcdsaKeyPair::from_pkcs8(
&ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING,
&key_pair.serialize_der(),
&rng,
)
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
let signature = key_pair
.sign(&rng, nonce.as_bytes())
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
let hex_signature = hex::encode(signature);
Ok(hex_signature)
Ok(droplet_rs::ssl::sign_nonce(private_key, nonce)?)
}
#[napi]
pub fn verify_nonce(public_cert: String, nonce: String, signature: String) -> anyhow::Result<bool> {
let (_, pem) = x509_parser::pem::parse_x509_pem(public_cert.as_bytes())?;
let (_, spki) = parse_x509_certificate(&pem.contents)?;
let public_key = SubjectPublicKeyInfo::from_der(spki.public_key().raw)?;
let raw_signature = hex::decode(signature)?;
let valid = ring::signature::ECDSA_P384_SHA384_FIXED
.verify(
public_key.der_bytes().into(),
nonce.as_bytes().into(),
raw_signature[..].into(),
)
.is_ok();
Ok(valid)
Ok(droplet_rs::ssl::verify_nonce(public_cert, nonce, signature)?)
}
+1 -51
View File
@@ -6,60 +6,10 @@ use std::{
};
use anyhow::anyhow;
use droplet_rs::versions::{create_backend_constructor, types::{ReadToAsyncRead, VersionBackend, VersionFile}};
use napi::{bindgen_prelude::*, sys::napi_value__, tokio_stream::StreamExt};
use tokio_util::codec::{BytesCodec, FramedRead};
use crate::version::{
backends::{
PathVersionBackend, ZipVersionBackend, SEVEN_ZIP_INSTALLED, SUPPORTED_FILE_EXTENSIONS,
},
types::{ReadToAsyncRead, VersionBackend, VersionFile},
};
/**
* Append new backends here
*/
pub fn create_backend_constructor<'a>(
path: &Path,
) -> Option<Box<dyn FnOnce() -> Result<Box<dyn VersionBackend + Send + 'a>>>> {
if !path.exists() {
return None;
}
let is_directory = path.is_dir();
if is_directory {
let base_dir = path.to_path_buf();
return Some(Box::new(move || {
Ok(Box::new(PathVersionBackend { base_dir }))
}));
};
if *SEVEN_ZIP_INSTALLED {
/*
Slow 7zip integrity test
let mut test = Command::new("7z");
test.args(vec!["t", path.to_str().expect("invalid utf path")]);
let status = test.status().ok()?;
if status.code().unwrap_or(1) == 0 {
let buf = path.to_path_buf();
return Some(Box::new(move || Ok(Box::new(ZipVersionBackend::new(buf)?))));
}
*/
// Fast filename-based test
if let Some(extension) = path.extension().and_then(|v| v.to_str()) {
let supported = SUPPORTED_FILE_EXTENSIONS
.iter()
.find(|v| ***v == *extension)
.is_some();
if supported {
let buf = path.to_path_buf();
return Some(Box::new(move || Ok(Box::new(ZipVersionBackend::new(buf)?))));
}
}
}
None
}
/**
* Persistent object so we can cache things between commands
-245
View File
@@ -1,245 +0,0 @@
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
cell::LazyCell,
fs::{self, metadata, File},
io::{self, BufRead, BufReader, Read, Seek, SeekFrom, Sink},
path::{Path, PathBuf},
process::{Child, ChildStdout, Command, Stdio},
sync::{Arc, LazyLock},
};
use anyhow::anyhow;
use crate::version::types::{MinimumFileObject, VersionBackend, VersionFile};
pub fn _list_files(vec: &mut Vec<PathBuf>, path: &Path) -> napi::Result<()> {
if metadata(path)?.is_dir() {
let paths = fs::read_dir(path)?;
for path_result in paths {
let full_path = path_result?.path();
if metadata(&full_path)?.is_dir() {
_list_files(vec, &full_path)?;
} else {
vec.push(full_path);
}
}
};
Ok(())
}
#[derive(Clone)]
pub struct PathVersionBackend {
pub base_dir: PathBuf,
}
impl VersionBackend for PathVersionBackend {
fn list_files(&mut self) -> anyhow::Result<Vec<VersionFile>> {
let mut vec = Vec::new();
_list_files(&mut vec, &self.base_dir)?;
let mut results = Vec::new();
for pathbuf in vec.iter() {
let relative = pathbuf.strip_prefix(self.base_dir.clone())?;
results.push(
self.peek_file(
relative
.to_str()
.ok_or(napi::Error::from_reason("Could not parse path"))?
.to_owned(),
)?,
);
}
Ok(results)
}
fn reader(
&mut self,
file: &VersionFile,
start: u64,
end: u64,
) -> anyhow::Result<Box<dyn MinimumFileObject + 'static>> {
let mut file = File::open(self.base_dir.join(file.relative_filename.clone()))?;
if start != 0 {
file.seek(SeekFrom::Start(start))?;
}
if end != 0 {
return Ok(Box::new(file.take(end - start)));
}
Ok(Box::new(file))
}
fn peek_file(&mut self, sub_path: String) -> anyhow::Result<VersionFile> {
let pathbuf = self.base_dir.join(sub_path.clone());
if !pathbuf.exists() {
return Err(anyhow!("Path doesn't exist."));
};
let file = File::open(pathbuf.clone())?;
let metadata = file.try_clone()?.metadata()?;
let permission_object = metadata.permissions();
let permissions = {
let perm: u32;
#[cfg(target_family = "unix")]
{
perm = permission_object.mode();
}
#[cfg(not(target_family = "unix"))]
{
perm = 0
}
perm
};
Ok(VersionFile {
relative_filename: sub_path,
permission: permissions,
size: metadata.len(),
})
}
fn require_whole_files(&self) -> bool {
false
}
}
pub static SEVEN_ZIP_INSTALLED: LazyLock<bool> =
LazyLock::new(|| Command::new("7z").output().is_ok());
// https://7-zip.opensource.jp/chm/general/formats.htm
// Intentionally repeated some because it's a trivial cost and it's easier to directly copy from the docs above
pub const SUPPORTED_FILE_EXTENSIONS: [&str; 89] = [
"7z", "bz2", "bzip2", "tbz2", "tbz", "gz", "gzip", "tgz", "tar", "wim", "swm", "esd", "xz",
"txz", "zip", "zipx", "jar", "xpi", "odt", "ods", "docx", "xlsx", "epub", "apm", "ar", "a",
"deb", "lib", "arj", "cab", "chm", "chw", "chi", "chq", "msi", "msp", "doc", "xls", "ppt",
"cpio", "cramfs", "dmg", "ext", "ext2", "ext3", "ext4", "img", "fat", "img", "hfs", "hfsx",
"hxs", "hxr", "hxq", "hxw", "lit", "ihex", "iso", "img", "lzh", "lha", "lzma", "mbr", "mslz",
"mub", "nsis", "ntfs", "img", "mbr", "rar", "r00", "rpm", "ppmd", "qcow", "qcow2", "qcow2c",
"squashfs", "udf", "iso", "img", "scap", "uefif", "vdi", "vhd", "vmdk", "xar", "pkg", "z", "taz",
];
#[derive(Clone)]
pub struct ZipVersionBackend {
path: String,
}
impl ZipVersionBackend {
pub fn new(path: PathBuf) -> anyhow::Result<Self> {
Ok(Self {
path: path.to_str().expect("invalid utf path").to_owned(),
})
}
}
pub struct ZipFileWrapper {
command: Child,
reader: BufReader<ChildStdout>,
}
impl ZipFileWrapper {
pub fn new(mut command: Child) -> Self {
let stdout = command
.stdout
.take()
.expect("failed to access stdout of 7z");
let reader = BufReader::new(stdout);
ZipFileWrapper { command, reader }
}
}
/**
* This read implemention is a result of debugging hell
* It should probably be replaced with a .take() call.
*/
impl Read for ZipFileWrapper {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.reader.read(buf)
}
}
impl Drop for ZipFileWrapper {
fn drop(&mut self) {
self.command.wait().expect("failed to wait for 7z exit");
}
}
impl VersionBackend for ZipVersionBackend {
fn list_files(&mut self) -> anyhow::Result<Vec<VersionFile>> {
let mut list_command = Command::new("7z");
list_command.args(vec!["l", "-ba", &self.path]);
let result = list_command.output()?;
if !result.status.success() {
return Err(anyhow!(
"failed to list files: code {:?}",
result.status.code()
));
}
let raw_result = String::from_utf8(result.stdout)?;
let files = raw_result
.split("\n")
.filter(|v| v.len() > 0)
.map(|v| v.split(" ").filter(|v| v.len() > 0));
let mut results = Vec::new();
for file in files {
let values = file.collect::<Vec<&str>>();
let mut iter = values.iter();
let (date, time, attrs, size, compress, name) = (
iter.next().expect("failed to read date"),
iter.next().expect("failed to read time"),
iter.next().expect("failed to read attrs"),
iter.next().expect("failed to read size"),
iter.next().expect("failed to read compress"),
iter.collect::<Vec<&&str>>(),
);
if attrs.starts_with("D") {
continue;
}
results.push(VersionFile {
relative_filename: name
.into_iter()
.map(|v| *v)
.fold(String::new(), |a, b| a + b + " ")
.trim_end()
.to_owned(),
permission: 0o744, // owner r/w/x, everyone else, read
size: size.parse().unwrap(),
});
}
Ok(results)
}
fn reader(
&mut self,
file: &VersionFile,
start: u64,
end: u64,
) -> anyhow::Result<Box<dyn MinimumFileObject + '_>> {
let mut read_command = Command::new("7z");
read_command.args(vec!["e", "-so", &self.path, &file.relative_filename]);
let output = read_command
.stdout(Stdio::piped())
.spawn()
.expect("failed to spawn 7z");
Ok(Box::new(ZipFileWrapper::new(output)))
}
fn peek_file(&mut self, sub_path: String) -> anyhow::Result<VersionFile> {
let files = self.list_files()?;
let file = files
.iter()
.find(|v| v.relative_filename == sub_path)
.expect("file not found");
Ok(file.clone())
}
fn require_whole_files(&self) -> bool {
true
}
}
-3
View File
@@ -1,3 +0,0 @@
pub mod utils;
pub mod types;
pub mod backends;
-55
View File
@@ -1,55 +0,0 @@
use std::{fmt::Debug, io::Read};
use dyn_clone::DynClone;
use tokio::io::{self, AsyncRead};
#[derive(Debug, Clone)]
pub struct VersionFile {
pub relative_filename: String,
pub permission: u32,
pub size: u64,
}
pub trait MinimumFileObject: Read + Send {}
impl<T: Read + Send> MinimumFileObject for T {}
// Intentionally not a generic, because of types in read_file
pub struct ReadToAsyncRead<'a> {
pub inner: Box<dyn Read + Send + 'a>,
}
const ASYNC_READ_BUFFER_SIZE: usize = 8128;
impl<'a> AsyncRead for ReadToAsyncRead<'a> {
fn poll_read(
mut self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<io::Result<()>> {
let mut read_buf = [0u8; ASYNC_READ_BUFFER_SIZE];
let read_size = ASYNC_READ_BUFFER_SIZE.min(buf.remaining());
match self.inner.read(&mut read_buf[0..read_size]) {
Ok(read) => {
buf.put_slice(&read_buf[0..read]);
std::task::Poll::Ready(Ok(()))
}
Err(err) => {
std::task::Poll::Ready(Err(err))
},
}
}
}
pub trait VersionBackend: DynClone {
fn require_whole_files(&self) -> bool;
fn list_files(&mut self) -> anyhow::Result<Vec<VersionFile>>;
fn peek_file(&mut self, sub_path: String) -> anyhow::Result<VersionFile>;
fn reader(
&mut self,
file: &VersionFile,
start: u64,
end: u64,
) -> anyhow::Result<Box<dyn MinimumFileObject + '_>>;
}
dyn_clone::clone_trait_object!(VersionBackend);