Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Security audit

on:
push:
branches: [main]
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
pull_request:
branches: [main]
paths:
- '**/Cargo.toml'
- '**/Cargo.lock'
schedule:
- cron: '0 0 * * 6' # Run every Saturday at 00:00 UTC

jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- name: Install cargo-audit
run: cargo install cargo-audit --locked

- name: Run cargo audit
run: cargo audit
16 changes: 16 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ env:
CARGO_TERM_COLOR: always

jobs:
msrv:
name: Check MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: Swatinem/rust-cache@v2
with:
cache-directories: ~/.cargo/bin

- name: Install cargo-msrv
run: cargo install cargo-msrv --locked

- name: Verify MSRV with all features
run: cargo msrv verify -- cargo check --all-features

format:
name: Format
runs-on: ubuntu-latest
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to this project will be documented in this file.

## Unreleased

### Changed

- Updated `suppaftp` dependency from 7.0 to 12.0, addressing [RUSTSEC-2026-0271](https://rustsec.org/advisories/RUSTSEC-2026-0271.html) (FTP command injection via CRLF in control channel arguments). suppaftp 12 requires Rust 1.88; the MSRV is now declared as `rust-version = "1.88.0"`.
- Updated `rusty-s3` from 0.9 to 0.10 and `quick-xml` from 0.38 to 0.41, addressing [RUSTSEC-2026-0194](https://rustsec.org/advisories/RUSTSEC-2026-0194.html) and [RUSTSEC-2026-0195](https://rustsec.org/advisories/RUSTSEC-2026-0195.html) (quick-xml parsing denial-of-service). rusty-s3 0.10's `parse_response` takes `&str` instead of `&[u8]`; internal call sites updated accordingly.

### Added

- CI: security audit job (`cargo audit`, runs on Cargo.toml/Cargo.lock changes and weekly) and MSRV check job (`cargo msrv verify` with all features).
- Ignored integration test against `ftp.radb.net` (anonymous FTP read of `radb.db.gz`) for manual FTP-path verification.

## v0.26.0 -- 2026-08-31

### Added
Expand Down
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name = "oneio"
version = "0.26.0"
authors = ["Mingwei Zhang <mingwei@bgpkit.com>"]
edition = "2021"
rust-version = "1.88.0"
readme = "README.md"
license = "MIT"
repository = "https://github.com/bgpkit/oneio"
Expand All @@ -27,7 +28,7 @@ thiserror = "2.0"

# feature: remote
reqwest = { version = "0.12", default-features = false, features = ["blocking", "http2", "charset", "stream"], optional = true }
suppaftp = { version = "7.0", optional = true }
suppaftp = { version = "12.0", optional = true }

# feature: compressions
# Turn off flate2 default-features so we can explicitly choose backend via features
Expand All @@ -46,8 +47,8 @@ serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }

# feature: s3
rusty-s3 = { version = "0.9", optional = true }
quick-xml = { version = "0.38", optional = true }
rusty-s3 = { version = "0.10", optional = true }
quick-xml = { version = "0.41", optional = true }
percent-encoding = { version = "2.3", optional = true }
sha2 = { version = "0.10", optional = true }
hmac = { version = "0.12", optional = true }
Expand Down Expand Up @@ -113,7 +114,7 @@ rustls = [
"dep:rustls_sys",
"reqwest?/rustls-tls-native-roots",
"reqwest?/rustls-tls-webpki-roots",
"suppaftp?/rustls",
"suppaftp?/rustls-aws-lc-rs",
]

# Future: Async support
Expand Down
4 changes: 2 additions & 2 deletions src/s3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ fn upload_multipart(
.send()
})?)?;
let init_response =
rusty_s3::actions::CreateMultipartUpload::parse_response(response.text()?.as_bytes())
rusty_s3::actions::CreateMultipartUpload::parse_response(response.text()?.as_str())
.map_err(|e| OneIoError::Network(Box::new(e)))?;
let upload_id = init_response.upload_id().to_string();

Expand Down Expand Up @@ -905,7 +905,7 @@ pub fn s3_list(
let url = action.sign(config.ttl);
let response = ensure_s3_success(get_s3_client().get(url).send()?)?;

let parsed = rusty_s3::actions::ListObjectsV2::parse_response(response.text()?.as_bytes())
let parsed = rusty_s3::actions::ListObjectsV2::parse_response(response.text()?.as_str())
.map_err(|e| OneIoError::Network(Box::new(e)))?;

if dirs {
Expand Down
37 changes: 37 additions & 0 deletions tests/ftp_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! FTP integration tests against a real, public FTP server.
//!
//! These tests require network access and run against `ftp.radb.net` (RADB),
//! the same host that bgpkit-commons uses for IRR database downloads.
//! They are `#[ignore]`d by default; run manually with:
//!
//! ```bash
//! cargo test --test ftp_tests --features ftp -- --ignored --nocapture
//! ```

use std::io::Read;

/// Stream the first bytes of a real file over anonymous FTP and verify that
/// data actually flows through the suppaftp-based reader.
#[test]
#[ignore]
fn read_radb_file_header() {
// radb.db.gz is the full RADB IRR database dump (large). We only read the
// first 64 bytes to verify connect + login + RETR streaming without
// downloading the whole file.
let mut reader = oneio::get_reader("ftp://ftp.radb.net/radb/dbase/radb.db.gz")
.expect("failed to open FTP reader");
let mut buf = [0u8; 64];
let n = reader
.read(&mut buf)
.expect("failed to read from FTP stream");
assert!(n > 0, "expected at least some bytes from the FTP server");

let head = String::from_utf8_lossy(&buf[..n]);
// RADB dumps start with IRR text (comments or aut-num objects); after gzip
// decompression the first bytes must be printable ASCII, not binary.
assert!(
head.chars()
.all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace()),
"unexpected binary data at start of decompressed stream: {head:?}"
);
}