mirror of
https://github.com/rustfs/rustfs.git
synced 2026-01-16 17:20:33 +00:00
fix: decode form-urlencoded object names in webhook/mqtt Key field (#1210)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -112,8 +112,8 @@ jobs:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubicloud-standard-4
|
||||
- arch: aarch64
|
||||
runner: ubicloud-standard-4-arm
|
||||
# - arch: aarch64
|
||||
# runner: ubicloud-standard-4-arm
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -152,14 +152,14 @@ jobs:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubicloud-standard-4
|
||||
- arch: aarch64
|
||||
runner: ubicloud-standard-4-arm
|
||||
# - arch: aarch64
|
||||
# runner: ubicloud-standard-4-arm
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Clean up previous test run
|
||||
run: |matrix.arch }}-${{
|
||||
run: |
|
||||
rm -rf /tmp/rustfs
|
||||
rm -f /tmp/rustfs.log
|
||||
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
cargo build -p rustfs --bins --jobs 4
|
||||
|
||||
- name: Run end-to-end tests
|
||||
run: |matrix.arch }}-${{
|
||||
run: |
|
||||
s3s-e2e --version
|
||||
./scripts/e2e-run.sh ./target/debug/rustfs /tmp/rustfs
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ where
|
||||
compress: true,
|
||||
};
|
||||
|
||||
let data = serde_json::to_vec(&item).map_err(|e| StoreError::Serialization(e.to_string()))?;
|
||||
let data = serde_json::to_vec(&*item).map_err(|e| StoreError::Serialization(e.to_string()))?;
|
||||
self.write_file(&key, &data)?;
|
||||
|
||||
Ok(key)
|
||||
|
||||
@@ -159,3 +159,30 @@ impl std::fmt::Display for TargetType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a form-urlencoded object name to its original form.
|
||||
///
|
||||
/// This function properly handles form-urlencoded strings where spaces are
|
||||
/// represented as `+` symbols. It first replaces `+` with spaces, then
|
||||
/// performs standard percent-decoding.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `encoded` - The form-urlencoded string to decode
|
||||
///
|
||||
/// # Returns
|
||||
/// The decoded string, or an error if decoding fails
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use rustfs_targets::target::decode_object_name;
|
||||
///
|
||||
/// let encoded = "greeting+file+%282%29.csv";
|
||||
/// let decoded = decode_object_name(encoded).unwrap();
|
||||
/// assert_eq!(decoded, "greeting file (2).csv");
|
||||
/// ```
|
||||
pub fn decode_object_name(encoded: &str) -> Result<String, TargetError> {
|
||||
let replaced = encoded.replace("+", " ");
|
||||
urlencoding::decode(&replaced)
|
||||
.map(|s| s.into_owned())
|
||||
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ use std::{
|
||||
use tokio::sync::{Mutex, OnceCell, mpsc};
|
||||
use tracing::{debug, error, info, instrument, trace, warn};
|
||||
use url::Url;
|
||||
use urlencoding;
|
||||
|
||||
const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const EVENT_LOOP_POLL_TIMEOUT: Duration = Duration::from_secs(10); // For initial connection check in task
|
||||
@@ -258,8 +257,8 @@ where
|
||||
.as_ref()
|
||||
.ok_or_else(|| TargetError::Configuration("MQTT client not initialized".to_string()))?;
|
||||
|
||||
let object_name = urlencoding::decode(&event.object_name)
|
||||
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))?;
|
||||
// Decode form-urlencoded object name
|
||||
let object_name = crate::target::decode_object_name(&event.object_name)?;
|
||||
|
||||
let key = format!("{}/{}", event.bucket_name, object_name);
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ use std::{
|
||||
use tokio::net::lookup_host;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info, instrument};
|
||||
use urlencoding;
|
||||
|
||||
/// Arguments for configuring a Webhook target
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -221,8 +220,8 @@ where
|
||||
|
||||
async fn send(&self, event: &EntityTarget<E>) -> Result<(), TargetError> {
|
||||
info!("Webhook Sending event to webhook target: {}", self.id);
|
||||
let object_name = urlencoding::decode(&event.object_name)
|
||||
.map_err(|e| TargetError::Encoding(format!("Failed to decode object key: {e}")))?;
|
||||
// Decode form-urlencoded object name
|
||||
let object_name = crate::target::decode_object_name(&event.object_name)?;
|
||||
|
||||
let key = format!("{}/{}", event.bucket_name, object_name);
|
||||
|
||||
@@ -421,3 +420,51 @@ where
|
||||
self.args.enable
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::target::decode_object_name;
|
||||
use url::form_urlencoded;
|
||||
|
||||
#[test]
|
||||
fn test_decode_object_name_with_spaces() {
|
||||
// Test case from the issue: "greeting file (2).csv"
|
||||
let object_name = "greeting file (2).csv";
|
||||
|
||||
// Simulate what event.rs does: form-urlencoded encoding (spaces become +)
|
||||
let form_encoded = form_urlencoded::byte_serialize(object_name.as_bytes()).collect::<String>();
|
||||
assert_eq!(form_encoded, "greeting+file+%282%29.csv");
|
||||
|
||||
// Test the decode_object_name helper function
|
||||
let decoded = decode_object_name(&form_encoded).unwrap();
|
||||
assert_eq!(decoded, object_name);
|
||||
assert!(!decoded.contains('+'), "Decoded string should not contain + symbols");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_object_name_with_special_chars() {
|
||||
// Test with various special characters
|
||||
let test_cases = vec![
|
||||
("folder/greeting file (2).csv", "folder%2Fgreeting+file+%282%29.csv"),
|
||||
("test file.txt", "test+file.txt"),
|
||||
("my file (copy).pdf", "my+file+%28copy%29.pdf"),
|
||||
("file with spaces and (parentheses).doc", "file+with+spaces+and+%28parentheses%29.doc"),
|
||||
];
|
||||
|
||||
for (original, form_encoded) in test_cases {
|
||||
// Test the decode_object_name helper function
|
||||
let decoded = decode_object_name(form_encoded).unwrap();
|
||||
assert_eq!(decoded, original, "Failed to decode: {}", form_encoded);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_object_name_without_spaces() {
|
||||
// Test that files without spaces still work correctly
|
||||
let object_name = "simple-file.txt";
|
||||
let form_encoded = form_urlencoded::byte_serialize(object_name.as_bytes()).collect::<String>();
|
||||
|
||||
let decoded = decode_object_name(&form_encoded).unwrap();
|
||||
assert_eq!(decoded, object_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls charac
|
||||
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:thiserror", "dep:tokio"] # network features with DNS resolver
|
||||
io = ["dep:tokio"]
|
||||
path = []
|
||||
notify = ["dep:hyper", "dep:s3s", "dep:hashbrown", "dep:thiserror", "dep:serde", "dep:libc"] # file system notification features
|
||||
notify = ["dep:hyper", "dep:s3s", "dep:hashbrown", "dep:thiserror", "dep:serde", "dep:libc", "dep:url", "dep:regex"] # file system notification features
|
||||
compress = ["dep:flate2", "dep:brotli", "dep:snap", "dep:lz4", "dep:zstd"]
|
||||
string = ["dep:regex", "dep:rand"]
|
||||
crypto = ["dep:base64-simd", "dep:hex-simd", "dep:hmac", "dep:hyper", "dep:sha1"]
|
||||
|
||||
Reference in New Issue
Block a user