Added Support for NotifyChange and Cancel if the underlying file store supports it

This commit is contained in:
Tal Aloni 2017-07-25 18:51:41 +03:00
parent 7e3df3f694
commit 5b4207aedd
10 changed files with 331 additions and 17 deletions

View file

@ -5,6 +5,7 @@ namespace SMBLibrary
{
STATUS_SUCCESS = 0x00000000,
STATUS_PENDING = 0x00000103,
STATUS_NOTIFY_CLEANUP = 0x0000010B,
STATUS_NOTIFY_ENUM_DIR = 0x0000010C,
SEC_I_CONTINUE_NEEDED = 0x00090312,
STATUS_OBJECT_NAME_EXISTS = 0x40000000,

View file

@ -185,6 +185,8 @@
<Compile Include="RPC\Structures\ResultList.cs" />
<Compile Include="RPC\Structures\SyntaxID.cs" />
<Compile Include="Server\ConnectionManager.cs" />
<Compile Include="Server\ConnectionState\SMB1AsyncContext.cs" />
<Compile Include="Server\ConnectionState\SMB2AsyncContext.cs" />
<Compile Include="Server\ConnectionState\ConnectionState.cs" />
<Compile Include="Server\ConnectionState\OpenFileObject.cs" />
<Compile Include="Server\ConnectionState\OpenSearch.cs" />

View file

@ -0,0 +1,22 @@
/* Copyright (C) 2017 Tal Aloni <tal.aloni.il@gmail.com>. All rights reserved.
*
* You can redistribute this program and/or modify it under the terms of
* the GNU Lesser Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*/
using System;
using System.Collections.Generic;
namespace SMBLibrary.Server
{
internal class SMB1AsyncContext
{
public ushort UID; // User ID
public ushort TID; // Tree ID
public uint PID; // Process ID
public ushort MID; // Multiplex ID
public ushort FileID;
public SMB1ConnectionState Connection;
public object IORequest;
}
}

View file

@ -25,6 +25,8 @@ namespace SMBLibrary.Server
// Key is PID (PID MUST be unique within an SMB connection)
private Dictionary<uint, ProcessStateObject> m_processStateList = new Dictionary<uint, ProcessStateObject>();
private List<SMB1AsyncContext> m_pendingRequests = new List<SMB1AsyncContext>();
public SMB1ConnectionState(ConnectionState state) : base(state)
{
@ -212,5 +214,63 @@ namespace SMBLibrary.Server
{
m_processStateList.Remove(processID);
}
public SMB1AsyncContext CreateAsyncContext(ushort userID, ushort treeID, uint processID, ushort multiplexID, ushort fileID, SMB1ConnectionState connection)
{
SMB1AsyncContext context = new SMB1AsyncContext();
context.UID = userID;
context.TID = treeID;
context.MID = multiplexID;
context.PID = processID;
context.FileID = fileID;
context.Connection = connection;
lock (m_pendingRequests)
{
m_pendingRequests.Add(context);
}
return context;
}
public SMB1AsyncContext GetAsyncContext(ushort userID, ushort treeID, uint processID, ushort multiplexID)
{
lock (m_pendingRequests)
{
int index = IndexOfAsyncContext(userID, treeID, processID, multiplexID);
if (index >= 0)
{
return m_pendingRequests[index];
}
}
return null;
}
public void RemoveAsyncContext(SMB1AsyncContext context)
{
lock (m_pendingRequests)
{
int index = IndexOfAsyncContext(context.UID, context.TID, context.PID, context.MID);
if (index >= 0)
{
m_pendingRequests.RemoveAt(index);
}
}
}
private int IndexOfAsyncContext(ushort userID, ushort treeID, uint processID, ushort multiplexID)
{
for (int index = 0; index < m_pendingRequests.Count; index++)
{
SMB1AsyncContext context = m_pendingRequests[index];
if (context.UID == userID &&
context.TID == treeID &&
context.PID == processID &&
context.MID == multiplexID)
{
return index;
}
}
return -1;
}
}
}

View file

@ -0,0 +1,22 @@
/* Copyright (C) 2017 Tal Aloni <tal.aloni.il@gmail.com>. All rights reserved.
*
* You can redistribute this program and/or modify it under the terms of
* the GNU Lesser Public License as published by the Free Software Foundation,
* either version 3 of the License, or (at your option) any later version.
*/
using System;
using System.Collections.Generic;
using SMBLibrary.SMB2;
namespace SMBLibrary.Server
{
internal class SMB2AsyncContext
{
public ulong AsyncID;
public FileID FileID;
public SMB2ConnectionState Connection;
public ulong SessionID;
public uint TreeID;
public object IORequest;
}
}

View file

@ -17,6 +17,9 @@ namespace SMBLibrary.Server
// Key is SessionID
private Dictionary<ulong, SMB2Session> m_sessions = new Dictionary<ulong, SMB2Session>();
private ulong m_nextSessionID = 1;
// Key is AsyncID
private Dictionary<ulong, SMB2AsyncContext> m_pendingRequests = new Dictionary<ulong, SMB2AsyncContext>();
private ulong m_nextAsyncID = 1;
public SMB2ConnectionState(ConnectionState state) : base(state)
{
@ -96,5 +99,61 @@ namespace SMBLibrary.Server
}
return result;
}
private ulong? AllocateAsyncID()
{
for (ulong offset = 0; offset < UInt64.MaxValue; offset++)
{
ulong asyncID = (ulong)(m_nextAsyncID + offset);
if (asyncID == 0 || asyncID == 0xFFFFFFFF)
{
continue;
}
if (!m_pendingRequests.ContainsKey(asyncID))
{
m_nextAsyncID = (ulong)(asyncID + 1);
return asyncID;
}
}
return null;
}
public SMB2AsyncContext CreateAsyncContext(FileID fileID, SMB2ConnectionState connection, ulong sessionID, uint treeID)
{
ulong? asyncID = AllocateAsyncID();
if (asyncID == null)
{
return null;
}
SMB2AsyncContext context = new SMB2AsyncContext();
context.AsyncID = asyncID.Value;
context.FileID = fileID;
context.Connection = connection;
context.SessionID = sessionID;
context.TreeID = treeID;
lock (m_pendingRequests)
{
m_pendingRequests.Add(asyncID.Value, context);
}
return context;
}
public SMB2AsyncContext GetAsyncContext(ulong asyncID)
{
SMB2AsyncContext context;
lock (m_pendingRequests)
{
m_pendingRequests.TryGetValue(asyncID, out context);
}
return context;
}
public void RemoveAsyncContext(SMB2AsyncContext context)
{
lock (m_pendingRequests)
{
m_pendingRequests.Remove(context.AsyncID);
}
}
}
}

View file

@ -15,6 +15,21 @@ namespace SMBLibrary.Server.SMB1
{
internal static void ProcessNTCancelRequest(SMB1Header header, NTCancelRequest request, ISMBShare share, SMB1ConnectionState state)
{
SMB1Session session = state.GetSession(header.UID);
SMB1AsyncContext context = state.GetAsyncContext(header.UID, header.TID, header.PID, header.MID);
if (context != null)
{
NTStatus status = share.FileStore.Cancel(context.IORequest);
OpenFileObject openFile = session.GetOpenFileObject(context.FileID);
if (openFile != null)
{
state.LogToServer(Severity.Information, "Cancel: Requested cancel on '{0}{1}', NTStatus: {2}. PID: {3}. MID: {4}.", share.Name, openFile.Path, status, context.PID, context.MID);
}
if (status == NTStatus.STATUS_SUCCESS || status == NTStatus.STATUS_CANCELLED)
{
state.RemoveAsyncContext(context);
}
}
}
}
}

View file

@ -15,9 +15,70 @@ namespace SMBLibrary.Server.SMB1
{
internal static void ProcessNTTransactNotifyChangeRequest(SMB1Header header, uint maxParameterCount, NTTransactNotifyChangeRequest subcommand, ISMBShare share, SMB1ConnectionState state)
{
// [MS-CIFS] If the server does not support the NT_TRANSACT_NOTIFY_CHANGE subcommand, it can return an
// error response with STATUS_NOT_IMPLEMENTED [..] in response to an NT_TRANSACT_NOTIFY_CHANGE Request.
header.Status = NTStatus.STATUS_NOT_IMPLEMENTED;
SMB1Session session = state.GetSession(header.UID);
OpenFileObject openFile = session.GetOpenFileObject(subcommand.FID);
SMB1AsyncContext context = state.CreateAsyncContext(header.UID, header.TID, header.PID, header.MID, subcommand.FID, state);
// We wish to make sure that the 'Monitoring started' will appear before the 'Monitoring completed' in the log
lock (context)
{
header.Status = share.FileStore.NotifyChange(out context.IORequest, openFile.Handle, subcommand.CompletionFilter, subcommand.WatchTree, (int)maxParameterCount, OnNotifyChangeCompleted, context);
if (header.Status == NTStatus.STATUS_PENDING)
{
state.LogToServer(Severity.Verbose, "NotifyChange: Monitoring of '{0}{1}' started. PID: {2}. MID: {3}.", share.Name, openFile.Path, context.PID, context.MID);
}
else if (header.Status == NTStatus.STATUS_NOT_SUPPORTED)
{
// [MS-CIFS] If the server does not support the NT_TRANSACT_NOTIFY_CHANGE subcommand, it can return an
// error response with STATUS_NOT_IMPLEMENTED [..] in response to an NT_TRANSACT_NOTIFY_CHANGE Request.
header.Status = NTStatus.STATUS_NOT_IMPLEMENTED;
}
}
}
private static void OnNotifyChangeCompleted(NTStatus status, byte[] buffer, object context)
{
NTTransactNotifyChangeResponse notifyChangeResponse = new NTTransactNotifyChangeResponse();
SMB1AsyncContext asyncContext = (SMB1AsyncContext)context;
// Wait until the 'Monitoring started' will be written to the log
lock (asyncContext)
{
SMB1ConnectionState connection = asyncContext.Connection;
connection.RemoveAsyncContext(asyncContext);
SMB1Session session = connection.GetSession(asyncContext.UID);
if (session != null)
{
ISMBShare share = session.GetConnectedTree(asyncContext.TID);
OpenFileObject openFile = session.GetOpenFileObject(asyncContext.FileID);
if (share != null && openFile != null)
{
connection.LogToServer(Severity.Verbose, "NotifyChange: Monitoring of '{0}{1}' completed. NTStatus: {2}. PID: {3}. MID: {4}.", share.Name, openFile.Path, status, asyncContext.PID, asyncContext.MID);
}
}
SMB1Header header = new SMB1Header();
header.Command = CommandName.SMB_COM_NT_TRANSACT;
header.Status = status;
header.Flags = HeaderFlags.CaseInsensitive | HeaderFlags.CanonicalizedPaths | HeaderFlags.Reply;
// [MS-CIFS] SMB_FLAGS2_UNICODE SHOULD be set to 1 when the negotiated dialect is NT LANMAN.
// [MS-CIFS] The Windows NT Server implementation of NT_TRANSACT_NOTIFY_CHANGE always returns the names of changed files in Unicode format.
header.Flags2 = HeaderFlags2.Unicode | HeaderFlags2.NTStatusCode;
header.UID = asyncContext.UID;
header.TID = asyncContext.TID;
header.PID = asyncContext.PID;
header.MID = asyncContext.MID;
notifyChangeResponse.FileNotifyInformationBytes = buffer;
byte[] responseSetup = notifyChangeResponse.GetSetup();
byte[] responseParameters = notifyChangeResponse.GetParameters(false);
byte[] responseData = notifyChangeResponse.GetData();
List<SMB1Command> responseList = NTTransactHelper.GetNTTransactResponse(responseSetup, responseParameters, responseData, asyncContext.Connection.MaxBufferSize);
foreach (SMB1Command response in responseList)
{
SMB1Message reply = new SMB1Message();
reply.Header = header;
reply.Commands.Add(response);
SMBServer.EnqueueMessage(asyncContext.Connection, reply);
}
}
}
}
}

View file

@ -15,16 +15,44 @@ namespace SMBLibrary.Server.SMB2
{
internal static SMB2Command GetCancelResponse(CancelRequest request, SMB2ConnectionState state)
{
if (request.Header.IsAsync && request.Header.AsyncID == 0)
SMB2Session session = state.GetSession(request.Header.SessionID);
if (request.Header.IsAsync)
{
ErrorResponse response = new ErrorResponse(request.CommandName, NTStatus.STATUS_CANCELLED);
response.Header.IsAsync = true;
return response;
SMB2AsyncContext context = state.GetAsyncContext(request.Header.AsyncID);
if (context != null)
{
ISMBShare share = session.GetConnectedTree(context.TreeID);
OpenFileObject openFile = session.GetOpenFileObject(context.FileID);
NTStatus status = share.FileStore.Cancel(context.IORequest);
if (openFile != null)
{
state.LogToServer(Severity.Information, "Cancel: Requested cancel on '{0}{1}'. NTStatus: {2}, AsyncID: {3}.", share.Name, openFile.Path, status, context.AsyncID);
}
if (status == NTStatus.STATUS_SUCCESS || status == NTStatus.STATUS_CANCELLED)
{
state.RemoveAsyncContext(context);
// If the target request is successfully canceled, the target request MUST be failed by sending
// an ERROR response packet [..] with the status field of the SMB2 header set to STATUS_CANCELLED.
ErrorResponse response = new ErrorResponse(request.CommandName, NTStatus.STATUS_CANCELLED);
response.Header.IsAsync = true;
response.Header.AsyncID = context.AsyncID;
return response;
}
// [MS-SMB2] If the target request is not successfully canceled [..] no response is sent.
return null;
}
else
{
// [MS-SMB2] If a request is not found [..] no response is sent.
return null;
}
}
else
{
// [MS-SMB2] the SMB2 CANCEL Request MUST use an ASYNC header for canceling requests that have received an interim response.
// [MS-SMB2] If the target request is not successfully canceled [..] no response is sent.
return null;
}
// [MS-SMB2] If a request is not found [..] no response is sent.
// [MS-SMB2] If the target request is not successfully canceled [..] no response is sent.
return null;
}
}
}

View file

@ -15,12 +15,56 @@ namespace SMBLibrary.Server.SMB2
{
internal static SMB2Command GetChangeNotifyInterimResponse(ChangeNotifyRequest request, ISMBShare share, SMB2ConnectionState state)
{
// [MS-SMB2] If the underlying object store does not support change notifications, the server MUST fail this request with STATUS_NOT_SUPPORTED
ErrorResponse response = new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED);
// Windows 7 / 8 / 10 will infinitely retry sending ChangeNotify requests if the response does not have SMB2_FLAGS_ASYNC_COMMAND set.
// Note: NoRemoteChangeNotify can be set in the registry to prevent the client from sending ChangeNotify requests altogether.
response.Header.IsAsync = true;
return response;
SMB2Session session = state.GetSession(request.Header.SessionID);
OpenFileObject openFile = session.GetOpenFileObject(request.FileId);
bool watchTree = (request.Flags & ChangeNotifyFlags.WatchTree) > 0;
SMB2AsyncContext context = state.CreateAsyncContext(request.FileId, state, request.Header.SessionID, request.Header.TreeID);
// We have to make sure that we don't send an interim response after the final response.
lock (context)
{
NTStatus status = share.FileStore.NotifyChange(out context.IORequest, openFile.Handle, request.CompletionFilter, watchTree, (int)request.OutputBufferLength, OnNotifyChangeCompleted, context);
if (status == NTStatus.STATUS_PENDING)
{
state.LogToServer(Severity.Verbose, "NotifyChange: Monitoring of '{0}{1}' started. AsyncID: {2}.", share.Name, openFile.Path, context.AsyncID);
}
// [MS-SMB2] If the underlying object store does not support change notifications, the server MUST fail this request with STATUS_NOT_SUPPORTED
ErrorResponse response = new ErrorResponse(request.CommandName, status);
// Windows 7 / 8 / 10 will infinitely retry sending ChangeNotify requests if the response does not have SMB2_FLAGS_ASYNC_COMMAND set.
// Note: NoRemoteChangeNotify can be set in the registry to prevent the client from sending ChangeNotify requests altogether.
response.Header.IsAsync = true;
response.Header.AsyncID = context.AsyncID;
return response;
}
}
private static void OnNotifyChangeCompleted(NTStatus status, byte[] buffer, object context)
{
SMB2AsyncContext asyncContext = (SMB2AsyncContext)context;
// Wait until the interim response has been sent
lock (asyncContext)
{
SMB2ConnectionState connection = asyncContext.Connection;
connection.RemoveAsyncContext(asyncContext);
SMB2Session session = connection.GetSession(asyncContext.SessionID);
if (session != null)
{
ISMBShare share = session.GetConnectedTree(asyncContext.TreeID);
OpenFileObject openFile = session.GetOpenFileObject(asyncContext.FileID);
if (share != null && openFile != null)
{
connection.LogToServer(Severity.Verbose, "NotifyChange: Monitoring of '{0}{1}' completed. NTStatus: {2}. AsyncID: {3}", share.Name, openFile.Path, status, asyncContext.AsyncID);
}
}
ChangeNotifyResponse response = new ChangeNotifyResponse();
response.Header.Status = status;
response.Header.IsAsync = true;
response.Header.AsyncID = asyncContext.AsyncID;
response.Header.SessionID = asyncContext.SessionID;
response.OutputBuffer = buffer;
SMBServer.EnqueueResponse(connection, response);
}
}
}
}