Implement SMB AFP_AfpInfo read/write via xattr (Phase 2.8 complete)
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -5486,6 +5486,7 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"xattr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
BIN
data/auth.sqlite
BIN
data/auth.sqlite
Binary file not shown.
1
vendor/smb-server/Cargo.toml
vendored
1
vendor/smb-server/Cargo.toml
vendored
@@ -25,6 +25,7 @@ aes = "0.8"
|
|||||||
cmac = "0.7"
|
cmac = "0.7"
|
||||||
rc4 = "0.2"
|
rc4 = "0.2"
|
||||||
ctr = "0.9" # AES-CTR for SMB3 encryption (simplified approach)
|
ctr = "0.9" # AES-CTR for SMB3 encryption (simplified approach)
|
||||||
|
xattr = "1.0" # Extended attributes support (AFP_AfpInfo)
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["localfs"]
|
default = ["localfs"]
|
||||||
|
|||||||
132
vendor/smb-server/src/backend.rs
vendored
132
vendor/smb-server/src/backend.rs
vendored
@@ -171,6 +171,28 @@ pub trait ShareBackend: Send + Sync + 'static {
|
|||||||
/// Static capabilities. The dispatcher consults these at TREE_CONNECT and
|
/// Static capabilities. The dispatcher consults these at TREE_CONNECT and
|
||||||
/// uses `is_read_only` to clamp authz.
|
/// uses `is_read_only` to clamp authz.
|
||||||
fn capabilities(&self) -> BackendCapabilities;
|
fn capabilities(&self) -> BackendCapabilities;
|
||||||
|
|
||||||
|
// ===== Extended Attributes (xattr) support =====
|
||||||
|
|
||||||
|
/// Get extended attribute value
|
||||||
|
async fn get_xattr(&self, _path: &SmbPath, _name: &str) -> SmbResult<Vec<u8>> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set extended attribute value
|
||||||
|
async fn set_xattr(&self, _path: &SmbPath, _name: &str, _value: &[u8]) -> SmbResult<()> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove extended attribute
|
||||||
|
async fn remove_xattr(&self, _path: &SmbPath, _name: &str) -> SmbResult<()> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List extended attribute names
|
||||||
|
async fn list_xattrs(&self, _path: &SmbPath) -> SmbResult<Vec<String>> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A live open file or directory handle.
|
/// A live open file or directory handle.
|
||||||
@@ -277,3 +299,113 @@ impl Handle for NullHandle {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// AFP_AfpInfo Handle (extended attribute virtual handle)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const AFP_INFO_XATTR_NAME: &str = "com.apple.aapl.AfpInfo";
|
||||||
|
const AFP_INFO_SIZE: usize = 32;
|
||||||
|
|
||||||
|
pub struct AfpInfoHandle {
|
||||||
|
base_path: SmbPath,
|
||||||
|
backend: std::sync::Arc<dyn ShareBackend>,
|
||||||
|
buffer: tokio::sync::RwLock<Vec<u8>>,
|
||||||
|
read_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AfpInfoHandle {
|
||||||
|
pub fn new(base_path: SmbPath, backend: std::sync::Arc<dyn ShareBackend>, read_only: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
base_path,
|
||||||
|
backend,
|
||||||
|
buffer: tokio::sync::RwLock::new(vec![0u8; AFP_INFO_SIZE]),
|
||||||
|
read_only,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_from_xattr(&self) -> SmbResult<()> {
|
||||||
|
let data = self.backend.get_xattr(&self.base_path, AFP_INFO_XATTR_NAME).await?;
|
||||||
|
let mut buffer = self.buffer.write().await;
|
||||||
|
buffer.clear();
|
||||||
|
buffer.extend_from_slice(&data);
|
||||||
|
if buffer.len() < AFP_INFO_SIZE {
|
||||||
|
buffer.resize(AFP_INFO_SIZE, 0);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_to_xattr(&self) -> SmbResult<()> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
let buffer = self.buffer.read().await;
|
||||||
|
let data = buffer.clone();
|
||||||
|
self.backend.set_xattr(&self.base_path, AFP_INFO_XATTR_NAME, &data[..]).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Handle for AfpInfoHandle {
|
||||||
|
async fn read(&self, offset: u64, len: u32) -> SmbResult<bytes::Bytes> {
|
||||||
|
self.load_from_xattr().await?;
|
||||||
|
let buffer = self.buffer.read().await;
|
||||||
|
let start = offset as usize;
|
||||||
|
let end = std::cmp::min(start + len as usize, buffer.len());
|
||||||
|
if start >= buffer.len() {
|
||||||
|
return Ok(bytes::Bytes::new());
|
||||||
|
}
|
||||||
|
Ok(bytes::Bytes::copy_from_slice(&buffer[start..end]))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write(&self, offset: u64, data: &[u8]) -> SmbResult<u32> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
let mut buffer = self.buffer.write().await;
|
||||||
|
let start = offset as usize;
|
||||||
|
if start + data.len() > AFP_INFO_SIZE {
|
||||||
|
return Err(SmbError::NotSupported);
|
||||||
|
}
|
||||||
|
buffer[start..start + data.len()].copy_from_slice(data);
|
||||||
|
Ok(data.len() as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn flush(&self) -> SmbResult<()> {
|
||||||
|
self.save_to_xattr().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stat(&self) -> SmbResult<FileInfo> {
|
||||||
|
Ok(FileInfo {
|
||||||
|
name: self.base_path.file_name().unwrap_or("").to_string(),
|
||||||
|
end_of_file: AFP_INFO_SIZE as u64,
|
||||||
|
allocation_size: AFP_INFO_SIZE as u64,
|
||||||
|
creation_time: 0,
|
||||||
|
last_access_time: 0,
|
||||||
|
last_write_time: 0,
|
||||||
|
change_time: 0,
|
||||||
|
is_directory: false,
|
||||||
|
file_index: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_times(&self, _times: FileTimes) -> SmbResult<()> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn truncate(&self, len: u64) -> SmbResult<()> {
|
||||||
|
if len != AFP_INFO_SIZE as u64 {
|
||||||
|
return Err(SmbError::NotSupported);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_dir(&self, _pattern: Option<&str>) -> SmbResult<Vec<DirEntry>> {
|
||||||
|
Err(SmbError::NotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(self: Box<Self>) -> SmbResult<()> {
|
||||||
|
self.save_to_xattr().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
65
vendor/smb-server/src/fs/local.rs
vendored
65
vendor/smb-server/src/fs/local.rs
vendored
@@ -428,6 +428,71 @@ impl ShareBackend for LocalFsBackend {
|
|||||||
case_sensitive: cfg!(any(target_os = "linux", target_os = "freebsd")),
|
case_sensitive: cfg!(any(target_os = "linux", target_os = "freebsd")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_xattr(&self, path: &SmbPath, name: &str) -> SmbResult<Vec<u8>> {
|
||||||
|
let rel = to_rel_path(path);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let xattr_name = name.to_string();
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
let full_path = root.join(&rel);
|
||||||
|
xattr::get(full_path.as_std_path(), &xattr_name)
|
||||||
|
.map_err(|e| SmbError::Io(e.into()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_xattr(&self, path: &SmbPath, name: &str, value: &[u8]) -> SmbResult<()> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = to_rel_path(path);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let xattr_name = name.to_string();
|
||||||
|
let value = value.to_vec();
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
let full_path = root.join(&rel);
|
||||||
|
xattr::set(full_path.as_std_path(), &xattr_name, &value)
|
||||||
|
.map_err(|e| SmbError::Io(e.into()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_xattr(&self, path: &SmbPath, name: &str) -> SmbResult<()> {
|
||||||
|
if self.read_only {
|
||||||
|
return Err(SmbError::AccessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = to_rel_path(path);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
let xattr_name = name.to_string();
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
let full_path = root.join(&rel);
|
||||||
|
xattr::remove(full_path.as_std_path(), &xattr_name)
|
||||||
|
.map_err(|e| SmbError::Io(e.into()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_xattrs(&self, path: &SmbPath) -> SmbResult<Vec<String>> {
|
||||||
|
let rel = to_rel_path(path);
|
||||||
|
let root = Arc::clone(&self.root);
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
let full_path = root.join(&rel);
|
||||||
|
xattr::list(full_path.as_std_path())
|
||||||
|
.map(|attrs| attrs.into_iter().map(|s| s.to_string()).collect())
|
||||||
|
.map_err(|e| SmbError::Io(e.into()))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(join_to_io)?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
56
vendor/smb-server/src/handlers/create.rs
vendored
56
vendor/smb-server/src/handlers/create.rs
vendored
@@ -1,6 +1,7 @@
|
|||||||
//! CREATE handler — open or create a file/directory and allocate a FileId.
|
//! CREATE handler — open or create a file/directory and allocate a FileId.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::proto::header::Smb2Header;
|
use crate::proto::header::Smb2Header;
|
||||||
use crate::proto::messages::{CreateRequest, CreateResponse};
|
use crate::proto::messages::{CreateRequest, CreateResponse};
|
||||||
@@ -85,11 +86,58 @@ pub async fn handle(
|
|||||||
if stream_path.is_afp_info() {
|
if stream_path.is_afp_info() {
|
||||||
debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_AfpInfo named stream open");
|
debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_AfpInfo named stream open");
|
||||||
|
|
||||||
// For AFP_AfpInfo, we return a virtual handle that reads/writes extended attributes
|
// Create AfpInfoHandle to read/write extended attributes
|
||||||
// TODO: Implement actual AFP_AfpInfo handling via extended attributes
|
let base_smb_path = stream_path.base_path().clone();
|
||||||
|
|
||||||
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 2.6 will implement)
|
// Check write permission
|
||||||
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND);
|
let want_write = req.desired_access & (FILE_WRITE_DATA | GENERIC_WRITE | GENERIC_ALL) != 0;
|
||||||
|
if want_write && !granted.allows_write() {
|
||||||
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate file_id
|
||||||
|
let mut tree = tree_arc.write().await;
|
||||||
|
let file_id = tree.alloc_file_id();
|
||||||
|
let read_only = tree.share.backend.capabilities().is_read_only;
|
||||||
|
|
||||||
|
let handle = crate::backend::AfpInfoHandle::new(base_smb_path.clone(), backend, want_write && read_only);
|
||||||
|
|
||||||
|
let open = Open::new(
|
||||||
|
file_id,
|
||||||
|
Box::new(handle),
|
||||||
|
if want_write { granted } else { Access::Read },
|
||||||
|
base_smb_path,
|
||||||
|
false, // is_directory
|
||||||
|
false, // delete_on_close
|
||||||
|
0, // oplock_level
|
||||||
|
0, // share_access
|
||||||
|
);
|
||||||
|
|
||||||
|
let open_arc = Arc::new(RwLock::new(open));
|
||||||
|
tree.opens.write().await.insert(file_id, open_arc.clone());
|
||||||
|
drop(tree);
|
||||||
|
|
||||||
|
let resp = CreateResponse {
|
||||||
|
structure_size: 89,
|
||||||
|
oplock_level: 0,
|
||||||
|
flags: 0,
|
||||||
|
create_action: FILE_OPENED,
|
||||||
|
creation_time: 0,
|
||||||
|
last_access_time: 0,
|
||||||
|
last_write_time: 0,
|
||||||
|
change_time: 0,
|
||||||
|
allocation_size: 32,
|
||||||
|
end_of_file: 32,
|
||||||
|
file_attributes: 0,
|
||||||
|
reserved2: 0,
|
||||||
|
file_id,
|
||||||
|
create_contexts_offset: 0,
|
||||||
|
create_contexts_length: 0,
|
||||||
|
create_contexts: vec![],
|
||||||
|
};
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
resp.write_to(&mut buf).expect("encode");
|
||||||
|
return HandlerResponse::ok(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle AFP_Resource named stream
|
// Handle AFP_Resource named stream
|
||||||
|
|||||||
Reference in New Issue
Block a user