mod client;
mod config;
mod file;
mod namespace;
mod types;
pub use client::S3Client;
pub use config::S3Config;
pub use file::S3File;
pub use types::{
S3ClientOptions, S3ListEntry, S3ListOptions, S3ListResult, S3PresignOptions, S3StatResult,
S3WriteOptions,
};
use rong::*;
rong::js_api! {
fn register_s3_namespace(ctx) {
namespace RongNamespace = ctx.host_namespace();
class S3Client = S3Client;
}
}
pub(crate) fn register_s3_classes(ctx: &JSContext) -> JSResult<()> {
ctx.register_hidden_class::<S3File>()?;
ctx.register_hidden_class::<S3Client>()?;
Ok(())
}
pub fn init(ctx: &JSContext) -> JSResult<()> {
register_s3_classes(ctx)?;
register_s3_namespace(ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use rong_test::*;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone)]
struct TestNamespaceState(Rc<RefCell<String>>);
struct TestS3Server {
endpoint: String,
access_key_id: String,
secret_access_key: String,
}
async fn spawn_s3_server() -> TestS3Server {
let tmp = tempfile::tempdir().expect("tempdir");
let fs = s3s_fs::FileSystem::new(tmp.path()).expect("s3s-fs");
std::fs::create_dir_all(tmp.path().join("test-bucket")).expect("create bucket dir");
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
let access_key_id = format!("test-access-{}-{nonce}", std::process::id());
let secret_access_key = format!("test-secret-{}-{nonce}", std::process::id());
let mut auth = s3s::auth::SimpleAuth::new();
auth.register(access_key_id.clone(), secret_access_key.clone().into());
let mut builder = s3s::service::S3ServiceBuilder::new(fs);
builder.set_auth(auth);
let service = builder.build();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("local_addr");
let _tmp = Box::leak(Box::new(tmp));
tokio::spawn(async move {
let http_server =
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new());
loop {
let Ok((stream, _)) = listener.accept().await else {
continue;
};
let service = service.clone();
let builder = http_server.clone();
tokio::spawn(async move {
let _ = builder
.serve_connection(hyper_util::rt::TokioIo::new(stream), service)
.await;
});
}
});
TestS3Server {
endpoint: format!("http://127.0.0.1:{}", addr.port()),
access_key_id,
secret_access_key,
}
}
fn setup_s3_env(ctx: &JSContext, server: &TestS3Server) -> JSResult<()> {
ctx.global()
.set("TEST_S3_ENDPOINT", server.endpoint.as_str())?;
ctx.global()
.set("TEST_S3_ACCESS_KEY", server.access_key_id.as_str())?;
ctx.global()
.set("TEST_S3_SECRET_KEY", server.secret_access_key.as_str())?;
ctx.global().set("TEST_S3_BUCKET", "test-bucket")?;
rong_console::init(ctx)?;
rong_assert::init(ctx)?;
init(ctx)?;
Ok(())
}
fn test_s3_config(server: &TestS3Server) -> S3Config {
S3Config {
access_key_id: server.access_key_id.clone(),
secret_access_key: server.secret_access_key.clone(),
bucket: "test-bucket".to_string(),
endpoint: Some(server.endpoint.clone()),
..Default::default()
}
}
#[test]
fn test_s3() {
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("workspace root");
std::env::set_current_dir(&workspace_root).expect("set cwd");
async_run!(|ctx: JSContext| async move {
unsafe {
std::env::set_var("NO_PROXY", "127.0.0.1,localhost");
}
let server = spawn_s3_server().await;
setup_s3_env(&ctx, &server)?;
let passed = UnitJSRunner::load_script(&ctx, "s3.js")
.await?
.run()
.await?;
assert!(passed);
Ok(())
});
}
#[test]
fn test_s3_namespace() {
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("workspace root");
std::env::set_current_dir(&workspace_root).expect("set cwd");
async_run!(|ctx: JSContext| async move {
unsafe {
std::env::set_var("NO_PROXY", "127.0.0.1,localhost");
}
let server = spawn_s3_server().await;
setup_s3_env(&ctx, &server)?;
let client = S3Client::new(test_s3_config(&server)).with_namespace("app1/")?;
ctx.host_namespace()
.set("s3", client.into_js_object(&ctx)?)?;
let passed = UnitJSRunner::load_script(&ctx, "s3_namespace.js")
.await?
.run()
.await?;
assert!(passed);
Ok(())
});
}
#[test]
fn test_s3_dynamic_namespace() {
async_run!(|ctx: JSContext| async move {
unsafe {
std::env::set_var("NO_PROXY", "127.0.0.1,localhost");
}
let server = spawn_s3_server().await;
setup_s3_env(&ctx, &server)?;
let namespace_state = TestNamespaceState(Rc::new(RefCell::new("scope-a".to_owned())));
ctx.set_state(namespace_state.clone());
let client = S3Client::new(test_s3_config(&server)).with_namespace_resolver(|ctx| {
let state = ctx.get_state::<TestNamespaceState>().ok_or_else(|| {
HostError::new("E_INVALID_STATE", "missing host namespace context")
})?;
Ok(format!("{}/", state.0.borrow().as_str()))
});
ctx.host_namespace()
.set("dynamicS3", client.into_js_object(&ctx)?)?;
let set_test_namespace = JSFunc::new(&ctx, move |namespace: String| {
*namespace_state.0.borrow_mut() = namespace;
})?;
ctx.host_namespace()
.set("__setTestS3Namespace", set_test_namespace)?;
let empty_namespace = S3Client::new(test_s3_config(&server))
.with_namespace_resolver(|_| Ok(String::new()));
ctx.host_namespace()
.set("emptyNamespaceS3", empty_namespace.into_js_object(&ctx)?)?;
let resolver_error = S3Client::new(S3Config::default()).with_namespace_resolver(|_| {
Err(HostError::new("E_TEST_NAMESPACE", "namespace unavailable")
.with_name("TypeError")
.into())
});
ctx.host_namespace()
.set("resolverErrorS3", resolver_error.into_js_object(&ctx)?)?;
let passed = UnitJSRunner::load_script(&ctx, "s3_dynamic_namespace.js")
.await?
.run()
.await?;
assert!(passed);
Ok(())
});
}
#[test]
fn test_s3_injected_api() {
async_run!(|ctx: JSContext| async move {
assert!(
S3Client::new(S3Config::default())
.with_namespace("")
.is_err()
);
let client = S3Client::new(S3Config::default());
ctx.host_namespace()
.set("s3", client.into_js_object(&ctx)?)?;
assert!(ctx.eval::<bool>(Source::from_bytes(
r#"typeof Rong.s3.write === "function"
&& typeof Rong.s3.file === "function"
&& typeof Rong.S3Client === "undefined"
&& typeof S3File === "undefined"
&& (() => {
try {
Rong.s3.file("key", { endpoint: "http://untrusted.invalid" });
return false;
} catch (error) {
return error.name === "TypeError"
&& error.message.includes("host-injected S3Client");
}
})()"#,
))?);
Ok(())
});
}
}