866d0536c8
Phase 1: AAPL Create Context negotiation Phase 2: AFP_AfpInfo Stream structure (Finder info + creation time) Phase 2.5: SMB Named Stream Backend (NamedStreamPath) Phase 2.6: Backend Named Stream Support in handlers Phase 2.7: VFS Extended Attributes (get/set/remove/list_xattr) Phase 4: Time Machine share config (time_machine field) Phase 5: Server/Volume Capabilities Phase 6: macOS Unicode mapping (private range ↔ ASCII) Tests: 174 smb-server tests pass, 52 VFS tests pass
372 lines
13 KiB
Rust
372 lines
13 KiB
Rust
//! CREATE handler — open or create a file/directory and allocate a FileId.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::proto::header::Smb2Header;
|
|
use crate::proto::messages::{CreateRequest, CreateResponse};
|
|
use tracing::{debug, warn};
|
|
|
|
use crate::backend::{OpenIntent, OpenOptions};
|
|
use crate::builder::Access;
|
|
use crate::conn::state::{Connection, Open};
|
|
use crate::dispatch::HandlerResponse;
|
|
use crate::handlers::shared::lookup_session_tree;
|
|
use crate::ntstatus;
|
|
use crate::path::SmbPath;
|
|
use crate::server::ServerState;
|
|
use crate::utils::utf16le_to_units;
|
|
|
|
// MS-SMB2 §2.2.13 access mask flags
|
|
const FILE_READ_DATA: u32 = 0x0000_0001;
|
|
const FILE_WRITE_DATA: u32 = 0x0000_0002;
|
|
const FILE_APPEND_DATA: u32 = 0x0000_0004;
|
|
const FILE_READ_ATTRIBUTES: u32 = 0x0000_0080;
|
|
const FILE_WRITE_ATTRIBUTES: u32 = 0x0000_0100;
|
|
const DELETE: u32 = 0x0001_0000;
|
|
const GENERIC_READ: u32 = 0x8000_0000;
|
|
const GENERIC_WRITE: u32 = 0x4000_0000;
|
|
const GENERIC_ALL: u32 = 0x1000_0000;
|
|
const MAX_ALLOWED: u32 = 0x0200_0000;
|
|
|
|
// CreateOptions
|
|
const FILE_DIRECTORY_FILE: u32 = 0x0000_0001;
|
|
const FILE_NON_DIRECTORY_FILE: u32 = 0x0000_0040;
|
|
const FILE_DELETE_ON_CLOSE: u32 = 0x0000_1000;
|
|
|
|
// CreateDisposition
|
|
const FILE_SUPERSEDE: u32 = 0x0000_0000;
|
|
const FILE_OPEN: u32 = 0x0000_0001;
|
|
const FILE_CREATE: u32 = 0x0000_0002;
|
|
const FILE_OPEN_IF: u32 = 0x0000_0003;
|
|
const FILE_OVERWRITE: u32 = 0x0000_0004;
|
|
const FILE_OVERWRITE_IF: u32 = 0x0000_0005;
|
|
|
|
// CreateAction in response (MS-SMB2 §2.2.14)
|
|
const FILE_OPENED: u32 = 0x0000_0001;
|
|
const FILE_CREATED: u32 = 0x0000_0002;
|
|
|
|
pub async fn handle(
|
|
server: &Arc<ServerState>,
|
|
conn: &Arc<Connection>,
|
|
hdr: &Smb2Header,
|
|
body: &[u8],
|
|
) -> HandlerResponse {
|
|
let req = match CreateRequest::parse(body) {
|
|
Ok(r) => r,
|
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
|
};
|
|
|
|
let tree_arc = match lookup_session_tree(conn, hdr).await {
|
|
Ok(t) => t,
|
|
Err(s) => return HandlerResponse::err(s),
|
|
};
|
|
let tree = tree_arc.read().await;
|
|
let granted = tree.granted_access;
|
|
let backend = tree.share.backend.clone();
|
|
drop(tree);
|
|
|
|
// Decode path.
|
|
let units = match utf16le_to_units(&req.name) {
|
|
Some(u) => u,
|
|
None => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
|
};
|
|
|
|
// Check for named stream (colon separator)
|
|
let has_named_stream = units.iter().any(|&u| u == ':' as u16);
|
|
|
|
if has_named_stream {
|
|
use crate::named_stream::NamedStreamPath;
|
|
let stream_path = match NamedStreamPath::parse_from_utf16(&units) {
|
|
Ok(p) => p,
|
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
|
};
|
|
|
|
// Handle AFP_AfpInfo named stream
|
|
if stream_path.is_afp_info() {
|
|
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
|
|
// TODO: Implement actual AFP_AfpInfo handling via extended attributes
|
|
|
|
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 2.6 will implement)
|
|
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND);
|
|
}
|
|
|
|
// Handle AFP_Resource named stream
|
|
if stream_path.is_afp_resource() {
|
|
debug!(base_path = %stream_path.base_path(), stream = %stream_path.stream_name(), "AFP_Resource named stream open");
|
|
|
|
// For AFP_Resource, we return a virtual handle that reads/writes ._ file
|
|
// TODO: Implement actual AFP_Resource handling via AppleDouble files
|
|
|
|
// Return STATUS_OBJECT_NAME_NOT_FOUND for now (phase 3 will implement)
|
|
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_NOT_FOUND);
|
|
}
|
|
|
|
// Unknown named stream type
|
|
warn!(stream = %stream_path.stream_name(), "unknown named stream type");
|
|
return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID);
|
|
}
|
|
|
|
let path = match SmbPath::from_utf16(&units) {
|
|
Ok(p) => p,
|
|
Err(_) => return HandlerResponse::err(ntstatus::STATUS_OBJECT_NAME_INVALID),
|
|
};
|
|
|
|
// Translate disposition.
|
|
let intent = match req.create_disposition {
|
|
FILE_SUPERSEDE | FILE_OVERWRITE_IF => OpenIntent::OverwriteOrCreate,
|
|
FILE_OPEN => OpenIntent::Open,
|
|
FILE_CREATE => OpenIntent::Create,
|
|
FILE_OPEN_IF => OpenIntent::OpenOrCreate,
|
|
FILE_OVERWRITE => OpenIntent::Truncate,
|
|
_ => return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER),
|
|
};
|
|
|
|
// Translate desired access into read/write hints.
|
|
let want_read = req.desired_access
|
|
& (FILE_READ_DATA | FILE_READ_ATTRIBUTES | GENERIC_READ | GENERIC_ALL | MAX_ALLOWED)
|
|
!= 0;
|
|
let want_write = req.desired_access
|
|
& (FILE_WRITE_DATA
|
|
| FILE_APPEND_DATA
|
|
| FILE_WRITE_ATTRIBUTES
|
|
| DELETE
|
|
| GENERIC_WRITE
|
|
| GENERIC_ALL
|
|
| MAX_ALLOWED)
|
|
!= 0;
|
|
|
|
// Reject writes on a read-only tree.
|
|
if want_write && !granted.allows_write() {
|
|
warn!(path = %path, "write open on read-only tree");
|
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
|
}
|
|
// Disposition that creates: requires write permission.
|
|
if !granted.allows_write()
|
|
&& matches!(
|
|
intent,
|
|
OpenIntent::Create
|
|
| OpenIntent::OpenOrCreate
|
|
| OpenIntent::OverwriteOrCreate
|
|
| OpenIntent::Truncate
|
|
)
|
|
{
|
|
return HandlerResponse::err(ntstatus::STATUS_ACCESS_DENIED);
|
|
}
|
|
|
|
let directory = req.create_options & FILE_DIRECTORY_FILE != 0;
|
|
let non_directory = req.create_options & FILE_NON_DIRECTORY_FILE != 0;
|
|
if directory && non_directory {
|
|
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
|
}
|
|
let delete_on_close = req.create_options & FILE_DELETE_ON_CLOSE != 0;
|
|
|
|
let opts = OpenOptions {
|
|
read: want_read || !want_write,
|
|
write: want_write,
|
|
intent,
|
|
directory,
|
|
non_directory,
|
|
delete_on_close,
|
|
};
|
|
|
|
let handle = match backend.open(&path, opts).await {
|
|
Ok(h) => h,
|
|
Err(e) => {
|
|
debug!(error = %e, path = %path, "backend open failed");
|
|
return HandlerResponse::err(e.to_nt_status());
|
|
}
|
|
};
|
|
|
|
// Stat for the response.
|
|
let info = match handle.stat().await {
|
|
Ok(i) => i,
|
|
Err(e) => {
|
|
let _ = handle.close().await;
|
|
return HandlerResponse::err(e.to_nt_status());
|
|
}
|
|
};
|
|
|
|
// Allocate FileId, register Open.
|
|
let tree = tree_arc.write().await;
|
|
let file_id = tree.alloc_file_id();
|
|
|
|
// Phase 4: Oplock support - use OplockManager to determine granted level
|
|
let requested_oplock = req.requested_oplock_level;
|
|
let granted_oplock = if requested_oplock == 0 {
|
|
0 // No oplock requested
|
|
} else {
|
|
// Check with OplockManager
|
|
server.oplock_manager.can_grant(
|
|
&path,
|
|
requested_oplock,
|
|
req.share_access,
|
|
if want_write { granted } else { Access::Read },
|
|
).await.unwrap_or(0)
|
|
};
|
|
|
|
// Register with OplockManager if oplock granted
|
|
if granted_oplock > 0 {
|
|
use crate::oplock::OplockEntry;
|
|
server.oplock_manager.register(&path, OplockEntry {
|
|
file_id,
|
|
tree_id: tree.id,
|
|
session_id: hdr.session_id,
|
|
oplock_level: granted_oplock,
|
|
share_access: req.share_access,
|
|
granted_access: if want_write { granted } else { Access::Read },
|
|
connection_id: 0, // Will be tracked in Phase 3
|
|
}).await;
|
|
}
|
|
|
|
// Phase 3: Check for lease request in create contexts (SMB 3.x)
|
|
let (lease_key, lease_state) = if !req.create_contexts.is_empty() {
|
|
use crate::proto::messages::CreateContext;
|
|
let contexts = CreateContext::parse_chain(&req.create_contexts).unwrap_or_default();
|
|
|
|
// Find RqLs (REQUEST_LEASE) context
|
|
let lease_ctx = contexts.iter().find(|ctx| ctx.name == CreateContext::NAME_RQLS);
|
|
|
|
if let Some(ctx) = lease_ctx {
|
|
// Parse lease request (MS-SMB2 §2.2.13.2)
|
|
// Data format: LeaseKey (16 bytes) + LeaseState (4 bytes) + LeaseFlags (4 bytes)
|
|
if ctx.data.len() >= 24 {
|
|
let lease_key_bytes: [u8; 16] = ctx.data[0..16].try_into().unwrap_or([0; 16]);
|
|
let lease_state = u32::from_le_bytes([ctx.data[16], ctx.data[17], ctx.data[18], ctx.data[19]]);
|
|
|
|
// Check if lease can be granted
|
|
if server.lease_manager.can_grant(lease_state).await {
|
|
// Register lease
|
|
use crate::oplock::LeaseEntry;
|
|
server.lease_manager.register(LeaseEntry {
|
|
lease_key: lease_key_bytes,
|
|
lease_state,
|
|
lease_flags: 0,
|
|
file_id,
|
|
path: path.clone(),
|
|
session_id: hdr.session_id,
|
|
tree_id: tree.id,
|
|
}).await;
|
|
|
|
(Some(lease_key_bytes), Some(lease_state))
|
|
} else {
|
|
(None, None)
|
|
}
|
|
} else {
|
|
(None, None)
|
|
}
|
|
} else {
|
|
(None, None)
|
|
}
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
let open = Open::new(
|
|
file_id,
|
|
handle,
|
|
if want_write { granted } else { Access::Read },
|
|
path.clone(),
|
|
info.is_directory,
|
|
delete_on_close,
|
|
granted_oplock, // oplock_level
|
|
req.share_access, // share_access
|
|
);
|
|
|
|
// Phase 3: Set lease fields if granted
|
|
let open_arc = Arc::new(tokio::sync::RwLock::new(open));
|
|
if lease_key.is_some() {
|
|
let mut open_mut = open_arc.write().await;
|
|
open_mut.lease_key = lease_key;
|
|
open_mut.lease_state = lease_state;
|
|
open_mut.lease_flags = Some(0);
|
|
}
|
|
|
|
tree.opens.write().await.insert(file_id, open_arc.clone());
|
|
drop(tree);
|
|
|
|
// Phase AAPL: Check for AAPL context (Apple SMB Extensions)
|
|
let aapl_response_data = if !req.create_contexts.is_empty() {
|
|
use crate::proto::messages::CreateContext;
|
|
use crate::proto::messages::{
|
|
AaplCreateContextRequest, AaplCreateContextResponse,
|
|
SMB2_CRTCTX_AAPL_SERVER_QUERY, SMB2_CRTCTX_AAPL_SERVER_CAPS,
|
|
SMB2_CRTCTX_AAPL_VOLUME_CAPS, SMB2_CRTCTX_AAPL_MODEL_INFO,
|
|
SMB2_CRTCTX_AAPL_UNIX_BASED, SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
|
|
SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
|
|
};
|
|
|
|
let contexts = CreateContext::parse_chain(&req.create_contexts).unwrap_or_default();
|
|
let aapl_ctx = contexts.iter().find(|ctx| ctx.name == CreateContext::NAME_AAPL);
|
|
|
|
if let Some(ctx) = aapl_ctx {
|
|
if let Some(aapl_req) = AaplCreateContextRequest::from_bytes(&ctx.data) {
|
|
if aapl_req.command == SMB2_CRTCTX_AAPL_SERVER_QUERY {
|
|
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED | SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR;
|
|
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE;
|
|
let aapl_resp = AaplCreateContextResponse::new_server_query(
|
|
aapl_req.request_bitmap,
|
|
aapl_req.client_caps,
|
|
server_caps,
|
|
volume_caps,
|
|
"MarkBase SMB",
|
|
);
|
|
Some(aapl_resp.to_bytes())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let create_action = match intent {
|
|
OpenIntent::Create => FILE_CREATED,
|
|
OpenIntent::OpenOrCreate | OpenIntent::OverwriteOrCreate => FILE_OPENED,
|
|
OpenIntent::Open | OpenIntent::Truncate => FILE_OPENED,
|
|
};
|
|
|
|
// Build response with AAPL context if present
|
|
let (create_contexts_offset, create_contexts_length, create_contexts) = if let Some(data) = aapl_response_data {
|
|
use crate::proto::messages::CreateContext;
|
|
let aapl_ctx = CreateContext {
|
|
name: CreateContext::NAME_AAPL.to_vec(),
|
|
data,
|
|
};
|
|
let mut ctx_buf = Vec::new();
|
|
CreateContext::encode_chain(&[aapl_ctx], &mut ctx_buf).expect("encode AAPL context");
|
|
let offset = 80 + req.name.len() + 8; // After CreateResponse fixed + padding
|
|
(offset as u32, ctx_buf.len() as u32, ctx_buf)
|
|
} else {
|
|
(0, 0, vec![])
|
|
};
|
|
|
|
let resp = CreateResponse {
|
|
structure_size: 89,
|
|
oplock_level: granted_oplock, // Phase 4: will be dynamic
|
|
flags: 0,
|
|
create_action,
|
|
creation_time: info.creation_time,
|
|
last_access_time: info.last_access_time,
|
|
last_write_time: info.last_write_time,
|
|
change_time: info.change_time,
|
|
allocation_size: info.allocation_size,
|
|
end_of_file: info.end_of_file,
|
|
file_attributes: info.attributes(),
|
|
reserved2: 0,
|
|
file_id,
|
|
create_contexts_offset,
|
|
create_contexts_length,
|
|
create_contexts,
|
|
};
|
|
let mut buf = Vec::new();
|
|
resp.write_to(&mut buf).expect("encode");
|
|
HandlerResponse::ok(buf)
|
|
}
|