DirectorySearcher FindOne() delay on initial execution - c#

I'm seeing an initial delay of 2-5 seconds between the time that I execute DirectorySearcher FindOne() and the first network packet I see go out to the LDAP server. After the initial execution, subsequent executions complete instantly for about 45 seconds. After that period of fast executions, the next execution will be delayed and again all subsequent executions will complete instantly. It seems like there's some sort of caching going on but I haven't been able to find any resources confirming that or describing what is causing the initial delay.
We noticed this on a client Windows 2008 server and then reproduced on our own Windows 2008 and Windows 7 boxes.
Here's what my simple .NET 4.0 C# app looks like. The delay occurs between the "Started" and "Finished" messages.
Any idea why this delay occurs on the initial FindOne() execution? Any help is much appreciated!
using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices;
namespace LdapTest
{
class Program
{
static void Main(string[] args)
{
string[] fetchAttributes;
fetchAttributes = new string[] { "{string[0]}" };
using (DirectoryEntry searchRoot = new DirectoryEntry("LDAP://localserver/ou=lab,dc=ourdomain,dc=com", "cn=binduser,ou=Services,dc=ourdomain,dc=com", "Password", AuthenticationTypes.ReadonlyServer))
{
using (DirectorySearcher searcher = new DirectorySearcher(searchRoot, "(sAMAccountName=UserName)", fetchAttributes, SearchScope.Subtree))
{
Console.WriteLine("Started");
SearchResult result = searcher.FindOne();
Console.WriteLine("Finished");
}
}
}
}

According to the LDAP ADsPath MSDN article, you should specify the ServerBind flag if your binding LDAP path points to a server to avoid unnecessary network traffic. It also recommends giving the full DNS name of the server. In addition, the ReadonlyServer flag is meaningless when pointing to a server. So my first suggestion is to replace the ReadonlyServer flag with ServerBind (and preferably give the full DNS name), or remove the server part of the string (in your example, make it LDAP://ou=lab,dc=ourdomain,dc=com or LDAP://ourdomain.com/ou=lab,dc=ourdomain,dc=com).
The other thing to look at is that you're providing the username by distinguished name. If you look at the core API that DirectoryEntry uses, IADsOpenDSObject::OpenDSObject, it requires that the lpReserved flag [the AuthenticationTypes parameter in DirectoryEntry] is zero [None] or includes the ADS_USE_SSL [SecureSocketsLayer] flag when passing a distinguished name for the username. Note that the SecureSocketsLayer flag requires that Active Directory requires that a certificate server is installed before you can use this flag. You might want to pass the username in a different format.
Finally, this MDSN page says that without any authentication flags, the username and password is sent cleartext. You should add the Secure flag.

Related

Run C# script through scheduled task

I've developed a small script in c# that is querying SQL Server and add computer objects to some Active Directory groups based on certain criteria. The script is working fine when I run it using the account which has the necessary rights to add/remove objects from Active Directory Group.
When I try to schedule the job, so it runs automatically from server using the "SYSTEM" account it does not work, I get "Access denied" I've updated the bind account to use the credentials from an account that works but I still have the same issue.
> Error Message:
> *2020-01-13 18:32:30,984 [1] ERROR TestAD.clsActiveDirectory - Error occured when trying to add computerobject abcdefg-a10 to group. Error
> message: Access is denied.*
The only way to make it work is using the actual account as account to run the scheduled task, however, problem is that our company policy does not allow us to store passwords, so I need to have the account logged-on to run this script.
code snippet
de.Username = "testing#test.com";
de.Password = "xxxxxxxxx";
de.AuthenticationType = AuthenticationTypes.Secure;
de.AuthenticationType = AuthenticationTypes.Sealing;
de.AuthenticationType = AuthenticationTypes.Delegation;
de.Properties.Count.ToString();
ds.SearchRoot = de;
ds.Filter = "(&(objectClass=computer)(name=" + _myComputerName.ToString() + `"))";`
ds.PropertiesToLoad.Add("memberof");
ds.PropertiesToLoad.Add("distinguishedname");
ds.SizeLimit = 10;
ds.PageSize = 0;
ds.SearchScope = System.DirectoryServices.SearchScope.Subtree;
I've tried adding some "AuthenticationTypes" to see if that made difference but still same
any help would be appreciated
Thx.
Have you tried using SQL Server Agents? My company uses them as opposed to Scheduled Tasks. They may be less elegant, but it may be a good alternative to your situation.
Create a SQL Server Agent that calls the executable with or without parameters.
If you cannot call the executable from the hosting OS, you can call an SSIS package on the network to call the executable for you.
Please let me know if you need more details.
I found the issue, and in the end pretty straight forward.
The Active Directory flow is following
- Bind to active directory with my special account and search for the computer object and
validate if it needs to be added to Active Directory Group
- if it needs to be added do 2nd bind to the Active Directory group and add computer
object. ==> This piece failed when using scheduled task or run under "SYSTEM" context
Reason for failure: When I bind 2nd time I did not specify any credentials so it was
using default credentials (SYSTEM) if I run the script my account which has enough
rights to add computer objects to groups.
I updated code for 2nd bind to include binding credentials and now it's working as
expected.
I hope this will be helpful for somebody else who has to troubleshoot similar issues.
old code
try
{
DirectoryEntry ent = new DirectoryEntry(bindString);
ent.Properties["member"].Add(newMember);
ent.CommitChanges();
}
New code
try
{
DirectoryEntry ent = new DirectoryEntry(bindString);
ent.AuthenticationType = AuthenticationTypes.Secure;
ent.AuthenticationType = AuthenticationTypes.Sealing;
ent.AuthenticationType = AuthenticationTypes.Delegation;
ent.Username = "test123#test.com";
ent.Password = "test123";
ent.Properties["member"].Add(newMember);
ent.CommitChanges();
}

Get users who did not login to exchange server for the past 120 days using powershell script in C#

I have an web application that needs to connect to Exchange server and get list of users who did not login to the mailbox in the last 120 days.
This needs to be done using powershell script in C#.
The code uses powershell script to find the users who have not logged in to exchange server in 120 days. When trying to use the script mentioned in the code it returns count as 0 but there are many users who have not logged in the past 120 days.
Below the current code.
using System;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Security;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Collections.ObjectModel;
using System.DirectoryServices;
namespace Exchange
{
public partial class NoLoginLast120Days : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string serverAddress = "my server address", runasPassword = "serverpassword", runasUsername = "server username";
// Prepare the credentials that will be used when connecting
// to the server. More info on the user to use on the notes
// below this code snippet.
SecureString ssRunasPassword = new SecureString();
foreach (char x in runasPassword)
ssRunasPassword.AppendChar(x);
PSCredential credentials = new PSCredential(runasUsername, ssRunasPassword);
// Prepare the connection
var connInfo = new WSManConnectionInfo(
new Uri("https://" + serverAddress + "/PowerShell"),
"http://schemas.microsoft.com/powershell/Microsoft.Exchange",
credentials);
connInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
connInfo.SkipCACheck = true;
connInfo.SkipCNCheck = true;
// Create the runspace where the command will be executed
var runspace = RunspaceFactory.CreateRunspace(connInfo);
runspace.Open();
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
Command getStatistics = new Command("Get-MailboxStatistics");
getStatistics.Parameters.Add("Filter", "$_.Lastlogontime -lt (get-date).AddDays(-1000)");
ps.Commands.Add(getStatistics);
Collection<PSObject> commandResults = ps.Invoke();
runspace.Close();
runspace.Dispose();
}
}
}
Update:
I tried getStatistics.Parameters.Add("Filter", "Lastlogontime $null"); for getting results which had lastlogontime as null, it was successful.
I was able to process the commandResults.
I even tried the below for getting users with lastlogontime as not null, so I can filter the desired results using C# but it did not work out.
getStatistics.Parameters.Add("Filter", "Lastlogontime -ne $null");
Also the users count is huge that I get System.OutOfMemoryException if I try without any filters.
If there is a way to get users who have not logged in for the past 120 days using powershell script in C# it will be really helpful as I am stuck with this for a long time.
Update
Below code works in powershell. It will be really helpful to know how to use this in C#.
Get-MailboxStatistics -Server <servername> | where {$_.LastLogonTime -lt (get-date).AddDays(-120)} | ft displayName, lastlogontime
Without knowing your environment its not easy to answer. But I think the issue is a miss understanding from the timestamps you are using. So before you can use it, you need to understand the difference between Last-Logon-Timestamp and Last-Logon AD attribute:
Last-Logon-Timestamp Attribute
This is the time that the user last logged into the domain. Whenever a user logs on, the value of this attribute is read from the DC. If the value is older [ current_time - msDS-LogonTimeSyncInterval ], the value is updated. The initial update after the raise of the domain functional level is calculated as 14 days minus random percentage of 5 days. So with the default settings in place the lastLogontimeStamp will be 9-14 days behind the current date (as mentioned by Microsoft here).
Last-Logon Attribute
The last time the user logged on. This value is stored as a large integer that represents the number of 100 nanosecond intervals since January 1, 1601 (UTC). A value of zero means that the last logon time is unknown. This attribute is not replicated and is maintained separately on each domain controller in the domain. To get an accurate value for the user's last logon in the domain, the Last-Logon attribute for the user must be retrieved from every domain controller in the domain. Keep noted that your "solution" might pick different domain controller here and therefore will get different results!
This should explain your issue and why you see some "strange" results as using the Last-Logon-Timestamp is more a random value ...

Using SSH.NET to connect to Remote Hosts

I'm trying to automate configuring remote hosts, we have hundreds of these devices, we normally do it through USB programming, but if I could get a script to connect to these devices and do it programmatically, it would free up time.
These devices run some type of linux os, i'm not sure exactly, but they do have SSH enabled and confirm server host keys when you first connect to them via utility like PuTTY.
For now, i'm just trying to initiate an SSH session with the device. I've done quite a bit of research, and have come up with this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Renci.SshNet;
using Renci.SshNet.Common;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
//Connection information
string user = "admin";
string pass = "********";
string host = "IP Address";
//Set up the SSH connection
using (var client = new SshClient(host, user, pass))
{
//Accept Host key
client.HostKeyReceived += delegate (object sender, HostKeyEventArgs e)
{
e.CanTrust = true;
};
//Start the connection
client.Connect();
var output = client.RunCommand("show device details");
client.Disconnect();
Console.WriteLine(output.ToString());
Console.ReadLine();
}
}
}
}
The problem is this doesn't seem to execute the command listed. The console window comes up, and I can access the same device by WebGUI and see the log file, it shows a connection being made, but when I break the execution and see the variable values the output variable shows null.
If I let the execution sit, with the console window open (just shows a blinking cursor in the upper left), the connection times out after 10 minutes and connection is lost, which I also see happen in the device log.
Why would does this not seem to execute the runcommand and store the results in the output variable?
When you execute the RunCommand() method on an object of type Renci.SshNet.SshClient, it does not return the result as a variable.
Instead, it returns an object of the Renci.SshNet.SshCommand type.
The issue is that, it looks like you can't fit this resultant SshCommand object into a var.
This Renci.SshNet.SshCommand, returned when you execute RunCommand(), will contain several properties and methods.
The properties are:
CommandText
CommandTimeout
ExitStatus
OutputStream
ExtendedOutputStream
Result
Error
They're all useful, but as everything else seems to be working, the only relevant one you want is "Result".
The "Result" property will contain a String, which will be the host stream result of the command you provided to RunCommand().
As you mention the device's logfile has logged a successful connection being made, it looks like the connection is successful. So you'd just have to make the proper tweak to grab the Result, as described above, and you should be good to go.
Addendum:
The following line in the original post's code:
var output = client.RunCommand("show device details");
Should be replaced with this code:
var output = client.RunCommand("show device details").Result;
This will assign the Result property (which is a String) to the output var, which will give the desired outcome.

iisreset over a list of servers programmatically

I want to perform iisreset programmatically from C# code over a list of servers with account having privilege to do that.
It's easy to do that for local machine for example that's a sample code:
// using ...
using System.Diagnostics;
public class YourForm : Form
{
// ...
private void yourButton_Click(object sender, EventArgs e)
{
Process.Start(#"C:\WINDOWS\system32\iisreset.exe", "/noforce");
}
// ...
}
Also:
using System.ServiceProcess;
using (ServiceController controller = new ServiceController())
{
controller.MachineName = “My local or remote computer name”;
controller.ServiceName = “IIS Service Name”; // i.e “w3svc”
if (controller.Status != ServiceControllerStatus.Running)
{
// Start the service
controller.Start();
Log.Debug(“IIS has been started successfully, now checking again for webservice availability”);
}
else
{
// Stop the service
controller.Stop();
// Start the service
controller.Start();
Log.Debug(“IIS has been restarted successfully”);
}
}
but how to perform this for more than one server.
Your first code snippet should work perfectly taking in considerations that there is no need to provide the full path of iisreset command.
Actually, you don't need that full path while calling IISRESET from CMD or Run tool. So, it is the same call.
Regarding user privilege, there are 2 approaches
You can pass desired user as an argument to Process.Start
Process.Start("iisreset", "server1", "admin", "admin password", "domain");
You can just call Process.Start as you did in your code, then make sure to run your application with the suitable user
I tried below and it worked perfectly
static void Main(string[] args)
{
string[] servers = LoadServersFromFile();
foreach (string server in servers)
{
Process.Start("iisreset", server.Trim());
}
}
private static string[] LoadServersFromFile()
{
//just listed all servers comma separated in a text file, change this to any other approach fits for your case
TextReader reader = new StreamReader("Servers.txt");
return reader.ReadToEnd().Split(',');
}
You probably need an impersonator to execute the above code.
I think the username and password used in the impersonator should have admin rights for that server (which you do).
You probably also need to remotely access the machine and then execute your code.
The post here, here and here might be of help to you.
Will update this post if something more useful comes to my mind.
EDIT:
You can try out the following steps:
Create a windows service with code for restarting the IIS
Deploy this service on all the servers for which you need to reset the IIS
Keep this service turned off
Remotely access this service (code to access services remotely is given in one of the posts above)
Start and stop the service. This will execute the code for resetting the IIS. Code for this is given here
Hope this helps.

Rotativa ActionAsPdf() Very Slow

Using Rotativa 1.6.4 from NuGet and have noticed the following issue using the code below.
ActionAsPdf hangs randomly for indeterminate amount of time.
Code below that is hanging:
var pdfResult = new ActionAsPdf("Report", new {id = Request.Params["id"]})
{
Cookies = cookieCollection,
FormsAuthenticationCookieName = FormsAuthentication.FormsCookieName,
CustomSwitches = "--load-error-handling ignore"
};
Background info that may help:
The customSwitches is in use to ignore a documented issue calling wkhtmltopdf.exe using the ActionAsPdf, but it does not suppress errors in the code only in the wkhtmltopdf call.
Observations, usage and testing:
It works but when running the application (whether or not stepping through code), it can be anywhere from 10 seconds up to about 4 minutes between hitting the pdfResult = new ActionAsPdf and finally entering into the "Report" action being called. Can't discern anything actually happening in the output window of Visual Studio, no errors are being thrown that I have found. Just random slow transition into the Reports() action.
I can run the Reports() action directly via URL and it never slows like this and is quite fast for PDF generation. I am running it using the ActionAsPdf to obtain the binary to save to file system and send via email, which is the prescribed method of doing so for this library.
The behavior exists on both a local Windows 10 dev box and a remote Server 2008R2 Test box. .Net 4.5.1 on both boxes, default IIS on each.
Questions I have:
Any idea on what might cause this slow down and how to remedy it?
I ended up using UrlAsPdf() instead of ActionAsPdf() and it works. Seems there may be some issues with the ActionAsPdf() and I have filed a bug with Rotative project on GitHub. The ActionAsPdf() is still marked as beta, so hopefully it get's fixed in future versions or by the community.
In my case, I had to do few more tweaks along with using UrlAsPdf(). I have narrowed down the issue to the cookie collection that I was adding. So I tried just adding the cookie that I needed, and the issue was resolved. Following is the sample code that I have used.
var report = new UrlAsPdf(url);
Dictionary<string, string> cookieCollection = new Dictionary<string, string>();
foreach (var key in Request.Cookies.AllKeys)
{
if (Crypto.Hash("_user").Equals(key))
{
cookieCollection.Add(key, Request.Cookies.Get(key).Value);
break;
}
}
report.Cookies = cookieCollection;
report.FormsAuthenticationCookieName = FormsAuthentication.FormsCookieName;

Categories