506 lines
17 KiB
C#
506 lines
17 KiB
C#
using C4IT.Logging;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.DirectoryServices.AccountManagement;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.AccessControl;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace LiamNtfs
|
|
{
|
|
public class cNtfsBase
|
|
{
|
|
private const int ErrorSessionCredentialConflict = 1219;
|
|
private const int ErrorNotConnected = 2250;
|
|
|
|
private cNtfsLogonInfo privLogonInfo = null;
|
|
private int scanningDepth;
|
|
public PrincipalContext adContext = null;
|
|
public Exception LastException { get; private set; } = null;
|
|
public string LastErrorMessage { get; private set; } = null;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void ResetError()
|
|
{
|
|
LastException = null;
|
|
LastErrorMessage = null;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
|
|
{
|
|
LastException = E;
|
|
LastErrorMessage = Action + ": " + E.Message;
|
|
cLogManager.LogEntry(Action, lev);
|
|
}
|
|
|
|
private async Task<bool> privLogonAsync(cNtfsLogonInfo LogonInfo)
|
|
{
|
|
try
|
|
{
|
|
//TODO: remove dummy delay?
|
|
await Task.Delay(0);
|
|
ResetError();
|
|
var netResource = new NetResource()
|
|
{
|
|
Scope = ResourceScope.GlobalNetwork,
|
|
ResourceType = ResourceType.Disk,
|
|
DisplayType = ResourceDisplaytype.Share,
|
|
RemoteName = LogonInfo.TargetNetworkName
|
|
};
|
|
|
|
var result = WNetAddConnection2(
|
|
netResource,
|
|
LogonInfo.UserSecret,
|
|
LogonInfo.User,
|
|
0);
|
|
|
|
if (result == ErrorSessionCredentialConflict)
|
|
{
|
|
var originalResult = result;
|
|
var cancelResult = WNetCancelConnection2(LogonInfo.TargetNetworkName, 0, true);
|
|
|
|
if (cancelResult == 0 || cancelResult == ErrorNotConnected)
|
|
{
|
|
result = WNetAddConnection2(
|
|
netResource,
|
|
LogonInfo.UserSecret,
|
|
LogonInfo.User,
|
|
0);
|
|
|
|
if (result == 0)
|
|
{
|
|
cLogManager.LogEntry(
|
|
$"NTFS login retry succeeded after SMB conflict on '{LogonInfo.TargetNetworkName}'. Initial add returned {originalResult} ({BuildWin32Message(originalResult)}), cancel returned {cancelResult} ({BuildWin32Message(cancelResult)}).",
|
|
LogLevels.Debug);
|
|
}
|
|
else
|
|
{
|
|
throw CreateNtfsLoginException(LogonInfo, originalResult, cancelResult, result);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw CreateNtfsLoginException(LogonInfo, originalResult, cancelResult, null);
|
|
}
|
|
}
|
|
|
|
if (result != 0)
|
|
{
|
|
throw CreateNtfsLoginException(LogonInfo, result, null, null);
|
|
}
|
|
var FSLogon = true;
|
|
|
|
adContext = new PrincipalContext(ContextType.Domain, LogonInfo.Domain, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password);
|
|
|
|
return FSLogon && adContext != null;
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
SetErrorException("exception error while Ntfs login", E, LogLevels.Debug);
|
|
cLogManager.LogException(E, LogLevels.Debug);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static Exception CreateNtfsLoginException(cNtfsLogonInfo logonInfo, int addResult, int? cancelResult, int? retryResult)
|
|
{
|
|
var stages = new List<string>
|
|
{
|
|
$"WNetAddConnection2={addResult} ({BuildWin32Message(addResult)})"
|
|
};
|
|
|
|
if (cancelResult.HasValue)
|
|
stages.Add($"WNetCancelConnection2('{logonInfo?.TargetNetworkName}')={cancelResult.Value} ({BuildWin32Message(cancelResult.Value)})");
|
|
|
|
if (retryResult.HasValue)
|
|
stages.Add($"RetryAdd={retryResult.Value} ({BuildWin32Message(retryResult.Value)})");
|
|
|
|
var details = string.Join(", ", stages);
|
|
var message = $"NTFS login to '{logonInfo?.TargetNetworkName}' for user '{logonInfo?.User}' failed. {details}.";
|
|
|
|
if (addResult == ErrorSessionCredentialConflict)
|
|
{
|
|
message += " This usually indicates an existing SMB/DFS session to the same server with a different credential context.";
|
|
|
|
if (cancelResult == ErrorNotConnected)
|
|
message += " The exact DFS/share path was not registered as a disconnectable connection, so the conflicting session is likely held under a different remote name or DFS target.";
|
|
}
|
|
|
|
var nativeResult = retryResult ?? cancelResult ?? addResult;
|
|
return new InvalidOperationException(message, new Win32Exception(nativeResult));
|
|
}
|
|
|
|
private static string BuildWin32Message(int errorCode)
|
|
{
|
|
return new Win32Exception(errorCode).Message;
|
|
}
|
|
|
|
private async Task<bool> privRelogon()
|
|
{
|
|
if (privLogonInfo == null)
|
|
return false;
|
|
var RetVal = await privLogonAsync(privLogonInfo);
|
|
return RetVal;
|
|
}
|
|
|
|
public async Task<bool> LogonAsync(cNtfsLogonInfo LogonInfo)
|
|
{
|
|
var RetVal = await privLogonAsync(LogonInfo);
|
|
if (RetVal == true)
|
|
privLogonInfo = LogonInfo;
|
|
return RetVal;
|
|
}
|
|
|
|
[DllImport("mpr.dll")]
|
|
private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags);
|
|
|
|
[DllImport("mpr.dll")]
|
|
private static extern int WNetCancelConnection2(string name, int flags,
|
|
bool force);
|
|
public async Task<cNtfsCollectionBase> RequestFoldersListAsync(string rootPath, int depth)
|
|
{
|
|
ResetError();
|
|
try
|
|
{
|
|
await Task.Delay(0);
|
|
if (depth == 0)
|
|
return null;
|
|
|
|
var Result = privRequestFoldersListAsync(rootPath, depth);
|
|
if (Result != null)
|
|
{
|
|
var RetVal = new cNtfsCollectionBase(Result.Count);
|
|
foreach (var Entry in Result)
|
|
{
|
|
var res = new cNtfsResultFolder((cNtfsResultFolder)Entry);
|
|
RetVal.Add(res);
|
|
}
|
|
return RetVal;
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogException(E);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
private List<cNtfsResultBase> privRequestFoldersListAsync(string rootPath, int depth)
|
|
{
|
|
if (String.IsNullOrEmpty(rootPath))
|
|
return new List<cNtfsResultBase>();
|
|
scanningDepth = depth;
|
|
return privRequestFoldersListAsync(new DirectoryInfo(rootPath), depth);
|
|
}
|
|
private List<cNtfsResultBase> privRequestFoldersListAsync(DirectoryInfo rootPath, int depth, cNtfsResultFolder parent = null)
|
|
{
|
|
ResetError();
|
|
var folders = new List<cNtfsResultBase>();
|
|
try
|
|
{
|
|
if (depth == 0)
|
|
return folders;
|
|
|
|
DirectoryInfo[] directories;
|
|
try
|
|
{
|
|
directories = rootPath.GetDirectories();
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogEntry($"Could not enumerate directories under '{rootPath.FullName}': {E.Message}", LogLevels.Warning);
|
|
cLogManager.LogException(E, LogLevels.Debug);
|
|
return folders;
|
|
}
|
|
|
|
foreach (var directory in directories)
|
|
{
|
|
cNtfsResultFolder folder;
|
|
try
|
|
{
|
|
folder = new cNtfsResultFolder()
|
|
{
|
|
ID = generateUniquId(directory.FullName),
|
|
Path = directory.FullName,
|
|
CreatedDate = directory.CreationTimeUtc.ToString("s"),
|
|
DisplayName = directory.Name,
|
|
Level = scanningDepth - depth + 1,
|
|
Parent = parent
|
|
};
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogEntry($"Could not read directory metadata for '{directory.FullName}': {E.Message}", LogLevels.Warning);
|
|
cLogManager.LogException(E, LogLevels.Debug);
|
|
continue;
|
|
}
|
|
|
|
folders.Add(folder);
|
|
if (depth <= 0)
|
|
continue;
|
|
|
|
try
|
|
{
|
|
var result = privRequestFoldersListAsync(directory, depth - 1, folder);
|
|
if (result != null && result.Count > 0)
|
|
folders.AddRange(result);
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogEntry($"Could not scan subtree '{directory.FullName}': {E.Message}", LogLevels.Warning);
|
|
cLogManager.LogException(E, LogLevels.Debug);
|
|
}
|
|
}
|
|
|
|
return folders;
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogException(E);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
|
|
public async Task<cNtfsCollectionBase> RequestFoldersListAsync(cNtfsResultBase folder, cNtfsResultBase parent)
|
|
{
|
|
ResetError();
|
|
try
|
|
{
|
|
await Task.Delay(0);
|
|
var Result = new List<cNtfsResultBase>();
|
|
|
|
if (Result != null)
|
|
{
|
|
var RetVal = new cNtfsCollectionBase(Result.Count);
|
|
foreach (var Entry in Result)
|
|
{
|
|
var res = new cNtfsResultFolder((cNtfsResultFolder)Entry);
|
|
res.Parent = parent;
|
|
RetVal.Add(res);
|
|
}
|
|
return RetVal;
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogException(E);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
internal static int getDepth(DirectoryInfo root, DirectoryInfo folder)
|
|
{
|
|
var rootDepth = root.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(System.IO.Path.DirectorySeparatorChar).Length;
|
|
var folderDepth = folder.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(System.IO.Path.DirectorySeparatorChar).Length;
|
|
return folderDepth - rootDepth;
|
|
|
|
}
|
|
internal static int getDepth(string root, string folder)
|
|
{
|
|
return getDepth(new DirectoryInfo(root), new DirectoryInfo(folder));
|
|
|
|
}
|
|
internal static string generateUniquId(string name)
|
|
{
|
|
try
|
|
{
|
|
var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
|
|
var utf8 = new System.Text.UTF8Encoding();
|
|
var hash = BitConverter.ToString(md5.ComputeHash(utf8.GetBytes(name)));
|
|
hash = hash.ToLower().Replace("-", "");
|
|
return hash;
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.DefaultLogger.LogException(E);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public class cNtfsLogonInfo
|
|
{
|
|
public string Domain;
|
|
public string User;
|
|
public string UserSecret;
|
|
public string TargetNetworkName;
|
|
public string TargetGroupPath;
|
|
}
|
|
|
|
public class cNtfsResultBase
|
|
{
|
|
public string ID { get; set; } = null;
|
|
public string DisplayName { get; set; } = null;
|
|
public string Path { get; set; } = null;
|
|
public int Level { get; set; } = -1;
|
|
public cNtfsResultBase()
|
|
{ }
|
|
public cNtfsResultBase(cNtfsResultBase Result)
|
|
{
|
|
if (Result == null)
|
|
return;
|
|
|
|
ID = Result.ID;
|
|
DisplayName = Result.DisplayName;
|
|
Path = Result.Path;
|
|
|
|
}
|
|
}
|
|
public class cNtfsResultFolder : cNtfsResultBase
|
|
{
|
|
public string Description { get; set; } = null;
|
|
public string Visibility { get; set; } = null;
|
|
public string OwnerADGroup { get; set; } = null;
|
|
public string WriteADGroup { get; set; } = null;
|
|
public string ReadADGroup { get; set; } = null;
|
|
public cNtfsResultBase Parent { get;set;} = null;
|
|
public string CreatedDate { get; set; } = DateTime.MinValue.ToString("s");
|
|
public cNtfsResultFolder Folders { get; private set; } = null;
|
|
public cNtfsResultFolder(cNtfsResultFolder Result) : base(Result)
|
|
{
|
|
Description = Result.Description;
|
|
Visibility = Result.Visibility;
|
|
CreatedDate = Result.CreatedDate;
|
|
Folders = Result.Folders;
|
|
Level = Result.Level;
|
|
Parent = Result.Parent;
|
|
OwnerADGroup = Result.OwnerADGroup;
|
|
WriteADGroup = Result.WriteADGroup;
|
|
ReadADGroup = Result.ReadADGroup;
|
|
}
|
|
|
|
public cNtfsResultFolder()
|
|
{
|
|
}
|
|
}
|
|
public class cNtfsResultShare : cNtfsResultBase
|
|
{
|
|
public cNtfsCollectionBase Folders { get; private set; } = null;
|
|
public cNtfsResultShare(cNtfsResultBase Result) : base(Result) { }
|
|
|
|
public cNtfsResultShare()
|
|
{
|
|
}
|
|
|
|
public async Task<bool> ResolveFolders(cNtfsBase ntfs)
|
|
{
|
|
Folders = await ntfs.RequestFoldersListAsync(this.Path, 0);
|
|
|
|
return Folders != null;
|
|
}
|
|
}
|
|
|
|
public class cNtfsCollectionBase : SortedList<string, cNtfsResultBase>
|
|
{
|
|
public cNtfsCollectionBase() { }
|
|
public cNtfsCollectionBase(int n) : base(n) { }
|
|
|
|
public void Add(cNtfsResultBase Folder)
|
|
{
|
|
if (!this.ContainsKey(Folder.ID))
|
|
this.Add(Folder.ID, Folder);
|
|
}
|
|
}
|
|
public class cFileshareLogonInfo
|
|
{
|
|
public string Domain;
|
|
public string User;
|
|
public string UserSecret;
|
|
public string TargetNetworkName;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public class NetResource
|
|
{
|
|
public ResourceScope Scope;
|
|
public ResourceType ResourceType;
|
|
public ResourceDisplaytype DisplayType;
|
|
public int Usage;
|
|
public string LocalName;
|
|
public string RemoteName;
|
|
public string Comment;
|
|
public string Provider;
|
|
}
|
|
|
|
public enum ResourceScope : int
|
|
{
|
|
Connected = 1,
|
|
GlobalNetwork,
|
|
Remembered,
|
|
Recent,
|
|
Context
|
|
};
|
|
|
|
public enum ResourceType : int
|
|
{
|
|
Any = 0,
|
|
Disk = 1,
|
|
Print = 2,
|
|
Reserved = 8,
|
|
}
|
|
|
|
public enum ResourceDisplaytype : int
|
|
{
|
|
Generic = 0x0,
|
|
Domain = 0x01,
|
|
Server = 0x02,
|
|
Share = 0x03,
|
|
File = 0x04,
|
|
Group = 0x05,
|
|
Network = 0x06,
|
|
Root = 0x07,
|
|
Shareadmin = 0x08,
|
|
Directory = 0x09,
|
|
Tree = 0x0a,
|
|
Ndscontainer = 0x0b
|
|
}
|
|
|
|
public enum NetError : uint
|
|
{
|
|
NERR_Success = 0,
|
|
NERR_BASE = 2100,
|
|
NERR_UnknownDevDir = (NERR_BASE + 16),
|
|
NERR_DuplicateShare = (NERR_BASE + 18),
|
|
NERR_BufTooSmall = (NERR_BASE + 23),
|
|
}
|
|
public enum SHARE_TYPE : uint
|
|
{
|
|
STYPE_DISKTREE = 0,
|
|
STYPE_PRINTQ = 1,
|
|
STYPE_DEVICE = 2,
|
|
STYPE_IPC = 3,
|
|
STYPE_SPECIAL = 0x80000000,
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
public struct SHARE_INFO_1
|
|
{
|
|
public string shi1_netname;
|
|
public uint shi1_type;
|
|
public string shi1_remark;
|
|
public SHARE_INFO_1(string sharename, uint sharetype, string remark)
|
|
{
|
|
this.shi1_netname = sharename;
|
|
this.shi1_type = sharetype;
|
|
this.shi1_remark = remark;
|
|
}
|
|
public override string ToString()
|
|
{
|
|
return shi1_netname;
|
|
}
|
|
}
|
|
}
|