What port is that system on anyways? C#, SNMP and Dell Switch Ports

If any of you have ever tried hunting down a port when the network goes out on a punch down only to realize your documentation hasn’t been kept up on will appreciate this one.  After digging through many unanswered threads, the very few well written blogs on the subject I managed to find a decent method for correlating a switch port to a host name.

It’s not something you would use all the time, but when you need it, you need it.

I have to give thanks to the creators of SnmpSharpNet and to the article that helped me find the correct OID for Dell Switch Ports by Geoff Garside as they were essential in creating this project.

While this article pertains primarily to Dell Switches, since that’s what I had to work with, it could easily be setup to work with HP by changing the OID and possibly others by tweaking the SNMP portion. The OID for Dell switches, at least 3500 and 5500 Series based on experience and other articles is 1.3.6.1.2.1.17.7.1.2.2.1.2.1 though there is a good chance it will work with any of them.

I decided on a WPF application this time around since you can create a list of custom objects and have it displayed on a DataGrid in a single line of code, very cool stuff.

The CODE:

using SnmpSharpNet;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace PortMapper
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        List<KeyValuePair<string, string>> portList = new List<KeyValuePair<string, string>>();
        List<Portmap> portMaps = new List<Portmap>();

        public class Portmap
        {
            public string Hostname { get; set; }
            public string Port { get; set; }
            public string IP { get; set; }
            public string MAC { get; set; }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            IPAddress ip = IPAddress.Parse("192.168.0.2");
            IPAddress ip2 = IPAddress.Parse("192.168.0.3");
            SnmpWalk(ip, "community", "1.3.6.1.2.1.17.7.1.2.2.1.2.1", "1");
            SnmpWalk(ip2, "community","1.3.6.1.2.1.17.7.1.2.2.1.2.1", "2");
            DhcpQuery("netsh", "dhcp server \\\\servername scope 192.168.0.0 show clients", "192");
            List<Portmap> gridResults = new List<Portmap>();
            //Example of filtering uplink ports from other switches
            foreach(Portmap portMap in portMaps)
            {
                if (portMap.Port != "1/2/48" && portMap.Port != "2/1/48")
                {
                    gridResults.Add(portMap);
                }
            }
            PortMapGrid.ItemsSource = gridResults;
        }
        
        //Use NETSH to retrieve a list of DHCP MAC and IP addresses
        private void DhcpQuery(string cmd, string args, string subnet)
        {
            ProcessStartInfo procStartInfo = new ProcessStartInfo();
            procStartInfo.RedirectStandardOutput = true;
            procStartInfo.UseShellExecute = false;
            procStartInfo.FileName = cmd;
            procStartInfo.Arguments = args;
            procStartInfo.CreateNoWindow = true;
            string output;
            using (Process proc = Process.Start(procStartInfo))
            {
                output = proc.StandardOutput.ReadToEnd();
                proc.WaitForExit();
            }

            //Find valid leases in command output
            string[] lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
            List<string> leases = new List<string>();
            foreach (string line in lines)
            {
                if (line.StartsWith(subnet))
                {
                   leases.Add(line);
                }
            }

            //Create threads
            Thread[] threadArray = new Thread[leases.Count];
            int threadcount = 0;

            //Loop each Dhcp Lease
            foreach (string line in leases)
            {
                string[] pieces = line.Split('-');
                string ipAddress = pieces[0].Trim();
                string mac = "";
                string hostname = "";
                foreach (string piece in pieces)
                {
                    if (piece.Trim().Length == 2)
                    {
                        mac += piece;
                    }
                }

                ThreadStart start = delegate
                {
                    hostname = GetHost(ipAddress);
                    foreach (KeyValuePair<string, string> port in portList)
                    {
                        if (port.Key.ToUpper().Trim() == mac.ToUpper().Trim())
                        {
                            Portmap portMap = new Portmap();
                            portMap.IP = ipAddress;
                            portMap.MAC = mac.ToUpper();
                            portMap.Port = port.Value;
                            portMap.Hostname = hostname;
                            portMaps.Add(portMap);
                        }
                    }
                };
                threadArray[threadcount] = new Thread(start);
                threadArray[threadcount].Start();
                threadcount = threadcount + 1;
            }

            //Join all threads in the array to wait for results
            for (int i = 0; i < threadcount; i++)
            {
                threadArray[i].Join();
            }
        }

        //SNMPWALK the ports on a switch or stack of switches. Ports will be labeled SwitchNum/Stack Number/Port Numbers.
        private void SnmpWalk(IPAddress ip, string snmpCommunity, string oid, string switchNum)
        {
            UdpTarget target = new UdpTarget(ip);

            // SNMP community name
            OctetString community = new OctetString(snmpCommunity);
            // Define agent parameters class
            AgentParameters param = new AgentParameters(community);
            // Set SNMP version to 1
            param.Version = SnmpVersion.Ver1;


            // Define Oid that is the root of the MIB tree you wish to retrieve
            Oid rootOid = new Oid(oid);

            // This Oid represents last Oid returned by the SNMP agent
            Oid lastOid = (Oid)rootOid.Clone();

            // Pdu class used for all requests
            Pdu pdu = new Pdu(PduType.GetNext);

            // Loop through results
            while (lastOid != null)
            {
                // When Pdu class is first constructed, RequestId is set to a random value
                // that needs to be incremented on subsequent requests made using the
                // same instance of the Pdu class.
                if (pdu.RequestId != 0)
                {
                    pdu.RequestId += 1;
                }
                // Clear Oids from the Pdu class.
                pdu.VbList.Clear();
                // Initialize request PDU with the last retrieved Oid
                pdu.VbList.Add(lastOid);
                // Make SNMP request
                SnmpV1Packet result = (SnmpV1Packet)target.Request(pdu, param);
                // You should catch exceptions in the Request if using in real application.

                // If result is null then agent didn't reply or we couldn't parse the reply.
                if (result != null)
                {
                    // ErrorStatus other then 0 is an error returned by 
                    // the Agent - see SnmpConstants for error definitions
                    if (result.Pdu.ErrorStatus != 0)
                    {
                        // agent reported an error with the request
                        Console.WriteLine("Error in SNMP reply. Error {0} index {1}",
                            result.Pdu.ErrorStatus,
                            result.Pdu.ErrorIndex);
                        lastOid = null;
                        break;
                    }
                    else
                    {
                        // Walk through returned variable bindings
                        foreach (Vb v in result.Pdu.VbList)
                        {
                            // Check that retrieved Oid is "child" of the root OID
                            if (rootOid.IsRootOf(v.Oid))
                            {
                                //Convert OID to MAC
                                string[] macs = v.Oid.ToString().Split('.');
                                string mac = "";
                                int counter = 0;
                                foreach (string chunk in macs)
                                {
                                    if (counter >= macs.Length - 6)
                                    {
                                        mac += string.Format("{0:X2}", int.Parse(chunk));
                                    }
                                    counter += 1;
                                }

                                //Assumes a 48 port switch (52 actual). You need to know these values to correctly iterate through a stack of switches.
                                int dellSwitch = 1 + int.Parse(v.Value.ToString()) / 52;
                                int port = int.Parse(v.Value.ToString()) - (52 * (dellSwitch - 1));
                                KeyValuePair<string, string> Port = new KeyValuePair<string, string>(mac, switchNum + "/" + dellSwitch.ToString() + "/" + port.ToString());
                                portList.Add(Port);

                                //Exit Loop
                                lastOid = v.Oid;
                            }
                            else
                            {
                                //End of the requested MIB tree. Set lastOid to null and exit loop
                                lastOid = null;
                            }
                        }
                    }
                }
                else
                {
                    Console.WriteLine("No response received from SNMP agent.");
                }
            }
            target.Close();
        }

        private string GetHost(string ipAddress)
        {
            try
            {
                IPHostEntry entry = Dns.GetHostEntry(ipAddress);
                return entry.HostName;
            }
            catch
            {
                return "";
            }
        }
    }
}
Advertisements

SharePoint 2013 REST Services: Downloading a file and working with Json via the HttpClient

I was recently asked about how you would find and download a file using the HttpClient and REST.  I decided to try and accomplish the task in a good old fashion Console Application using the standard .NET libraries instead of the Windows Store this time and realized that even with my past experience and previous code to work from this was no small task and it was only with help from the three following sites I was able to come up with the code below. The lack of the Json classes in the Windows Store libraries makes working with REST responses far more involved, though a bit easier to leverage once you have all the pieces in place when working with a standard .NET application:

Json Serialization Tutorial:

http://www.codeproject.com/Articles/272335/JSON-Serialization-and-Deserialization-in-ASP-NET

Derive .NET classes for serialization from raw Json data:

http://json2csharp.com/

View Json Data in a structure format:

http://jsonviewer.stack.hu/

Once you get your Json responses to your REST API calls you need to take a look at them and create classes to feed to the JsonSerializer.  This leaves you with objects that perfectly mirror the Json data structure as you would view it in the jsonviewer for extracting data.   Once you have all that it’s pretty easy to call the REST API and retrieve the data you need to download files.

Of coarse, the Microsoft.SharePoint.Client library could do the same task in a fraction of the time, but I am often asked how to work with the REST services without using those libraries.

The CODE:

    public class JsonHelper
    {
        /// JSON Serialization
        public static string JsonSerializer<T>(T t)
        {
            DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
            MemoryStream ms = new MemoryStream();
            ser.WriteObject(ms, t);
            string jsonString = Encoding.UTF8.GetString(ms.ToArray());
            ms.Close();
            return jsonString;
        }
        /// JSON Deserialization
        public static T JsonDeserialize<T>(string jsonString)
        {
            DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(jsonString));
            T obj = (T)ser.ReadObject(ms);
            return obj;
        }
    }

    //Custom Json Classes
    public class RootObject
    {
        public D d { get; set; }
    }
    public class D
    {
        public GetContextWebInformation GetContextWebInformation { get; set; }
        public List<Result> results { get; set; }
    }
    public class GetContextWebInformation
    {
        public int FormDigestTimeoutSeconds { get; set; }
        public string FormDigestValue { get; set; }
        public string LibraryVersion { get; set; }
        public string SiteFullUrl { get; set; }
        public string WebFullUrl { get; set; }
    }
    public class Result
    {
        public ContentType ContentType { get; set; }
        public string EncodedAbsUrl { get; set; }
        public string FileLeafRef { get; set; }
        public Folder Folder { get; set; }
        public int FileSystemObjectType { get; set; }
        public int Id { get; set; }
        public string ContentTypeId { get; set; }
        public string Title { get; set; }
        public int? ImageWidth { get; set; }
        public int? ImageHeight { get; set; }
        public string ImageCreateDate { get; set; }
        public object Description { get; set; }
        public object Keywords { get; set; }
        public string OData__dlc_DocId { get; set; }
        public int ID { get; set; }
        public string Created { get; set; }
        public int AuthorId { get; set; }
        public string Modified { get; set; }
        public int EditorId { get; set; }
        public object OData__CopySource { get; set; }
        public int? CheckoutUserId { get; set; }
        public string OData__UIVersionString { get; set; }
        public string GUID { get; set; }
    }

    class Program
    {
        static void Main()
        {

            string url = "https://sharepoint.site/";
            string filename = "2010-07-23 13.32.22.jpg"; ;
            string digest = "";
            HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
            client.BaseAddress = new System.Uri(url);
            string cmd = "_api/contextinfo";
            client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");
            client.DefaultRequestHeaders.Add("ContentType", "application/json");
            client.DefaultRequestHeaders.Add("ContentLength", "0");
            StringContent httpContent = new StringContent("");
            HttpResponseMessage response = client.PostAsync(cmd, httpContent).Result;
            if (response.IsSuccessStatusCode)
            {
                string content = response.Content.ReadAsStringAsync().Result;
                RootObject sp = JsonHelper.JsonDeserialize<RootObject>(content);
                digest = sp.d.GetContextWebInformation.FormDigestValue;
            }
            client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
            client.BaseAddress = new System.Uri(url);
            client.DefaultRequestHeaders.Clear();
            client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");
            client.DefaultRequestHeaders.Add("X-RequestDigest", digest);
            client.DefaultRequestHeaders.Add("X-HTTP-Method", "GET");
            string uri = "_api/web/lists/GetByTitle('Your Pictures')/Items?$select=ID,FileLeafRef,EncodedAbsUrl&$filter=FileLeafRef eq '" + filename + "'";
            HttpResponseMessage response2 = client.GetAsync(uri).Result;
            response2.EnsureSuccessStatusCode();
            if (response2.IsSuccessStatusCode)
            {
                string listItems = response2.Content.ReadAsStringAsync().Result;
                RootObject sp = JsonHelper.JsonDeserialize<RootObject>(listItems);
                foreach (Result result in sp.d.results)
                {
                    MemoryStream stream = (MemoryStream)client.GetAsync(result.EncodedAbsUrl).Result.Content.ReadAsStreamAsync().Result;
                    using (FileStream fileStream = System.IO.File.Create(@"C:\" + result.FileLeafRef))
                    {
                        stream.WriteTo(fileStream);
                    }
                }
            }
            else
            {
                var content = response.Content.ReadAsStringAsync();
            }
        }
    }

Change the width of SharePoint:FormField elements

Ever want to change the width or height of a particular field or text box on a SharePoint form using SharePoint Designer?  If you have looked around you will realize it’s not the most obvious of solutions.

If you would like to change the size of all the elements on a page you can simply add a content editor with the following:

<style>
.ms-long{ width:100px; }
</style>

However if you want to get granular its a bit more involved.  You need to place the SharePoint:FormField element inside of a DIV and assign it an ID as shown below:

<div ID=”container1“>

<SharePoint:FormField runat=”server” id=”ff24{$Pos} ControlMode=”New” FieldName=”Phone” __designer:bind=”{ddwrt:DataBind(‘i’,concat(‘ff24′,$Pos),’Value’,’ValueChanged’,’ID’,ddwrt:EscapeDelims(string(@ID)),’@Phone’)}”/>

</div>

Once you have all of your fields inside of uniquely ID’ed DIV’s we can use CSS to select only the .ms-long classes within those elements.  Example:

<style>

#container1 .ms-long{ width:25px; }

#container2 .ms-long{ width:25px; }

#container3 .ms-long{ width:35px; }

</style>

Easy enough, and covered in part other places but hopefully this will save some time if anyone runs into my problem again.

SharePoint 2013 REST Services using C# and the HttpClient (for Windows Store Apps)

SharePoint Web Services have come a long way in the last few versions.  I have spent a lot of time working with the SharePoint Object Model And recently started to use the Client Object Model which are both wonderful libraries with great documentation and numerous articles about them.

However, if you have delved into mobile app development recently you will realize that Windows Store projects only give a very specific subset of the core .NET libraries and that Exchange Web Services and the SharePoint Client libraries and many others are not available.  Now this does introduce some difficulty but in SharePoint 2013 REST services will allow you to accomplish almost any task in SharePoint.  The problem is that Microsoft or the development community has decided that any of us working with REST services directly will need to use JavaScript, at least if we want any examples of what these requests would look like.  While I do not mind doing minor tasks in Jscript I vastly prefer C# for doing larger applications and after doing dozens of searches, reading over a hundred articles and posts, I finally broke down and reverse engineered some Jscript examples and massively reworked what few C# examples existed for SharePoint REST.

I will be showing four main things in this post.  How to create a digest, which is used in the headers of any REST query passed from the HttpClient for authentication.  I will demonstrate how to get a list of items and how to work with the Json Objects using only native Windows Store libraries.  I will demonstrate a file upload and finally how to create a new list item with a lookup column.

The Examples:

Creating your digest:

String retVal = "";
try
{
string url = "https://YourSite.com/";
HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
client.BaseAddress = new System.Uri(url);
string cmd = "_api/contextinfo";

client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");
client.DefaultRequestHeaders.Add("ContentType", "application/json");
client.DefaultRequestHeaders.Add("ContentLength", "0");
StringContent httpContent = new StringContent("");
var response = client.PostAsync(cmd, httpContent).Result;
if (response.IsSuccessStatusCode)
{
string content = response.Content.ReadAsStringAsync().Result;
JsonObject val = JsonValue.Parse(content).GetObject();
JsonObject d = val.GetNamedObject("d");
JsonObject wi = d.GetNamedObject("GetContextWebInformation");
retVal = wi.GetNamedString("FormDigestValue");
}
}
catch
{ }
return retVal;

Getting a list of items

In this examples and all that follow the object digest would be passed from the return in the function above:
string url = "https://YourSite.com/Subsite/";
HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
client.BaseAddress = new System.Uri(url);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");
client.DefaultRequestHeaders.Add("X-RequestDigest", digest);
client.DefaultRequestHeaders.Add("X-HTTP-Method", "POST");
StringContent strContent = new StringContent("");
HttpResponseMessage response = await client.PostAsync("_api/web/lists/GetByTitle('Your List Title')/GetItems(query=@v1)?@v1={'ViewXml':''}", strContent);
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
{
var content = response.Content.ReadAsStringAsync();
JsonObject d = JsonValue.Parse(content.Result).GetObject();
JsonObject results = d["d"].GetObject();
JsonArray jobs = results["results"].GetArray();
foreach (JsonValue job in jobs)
{
JobBox.Items.Add(job.GetObject()["Job"].GetObject().GetNamedString("Label"));
}
}
else
{
var content = response.Content.ReadAsStringAsync();
}

Upload a File

FileOpenPicker picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
picker.ViewMode = PickerViewMode.Thumbnail;
// Filter to include a sample subset of file types.
picker.FileTypeFilter.Clear();
picker.FileTypeFilter.Add(".bmp");
picker.FileTypeFilter.Add(".png");
picker.FileTypeFilter.Add(".jpeg");
picker.FileTypeFilter.Add(".jpg");
// Open the file picker.
StorageFile path = await picker.PickSingleFileAsync();
if (path != null)
{
string url = "https://YourSite.com/Subsite/";
HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
client.BaseAddress = new System.Uri(url);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("Accept", "application/json;odata=verbose");
client.DefaultRequestHeaders.Add("X-RequestDigest", digest);
client.DefaultRequestHeaders.Add("X-HTTP-Method", "POST");
client.DefaultRequestHeaders.Add("binaryStringRequestBody", "true");
IRandomAccessStream fileStream = await path.OpenAsync(FileAccessMode.Read);
var reader = new DataReader(fileStream.GetInputStreamAt(0));
await reader.LoadAsync((uint)fileStream.Size);
Byte[] content = new byte[fileStream.Size];
reader.ReadBytes(content);
ByteArrayContent file = new ByteArrayContent(content);
HttpResponseMessage response = await client.PostAsync("_api/web/lists/getByTitle(@TargetLibrary)/RootFolder/Files/add(url=@TargetFileName,overwrite='true')?@TargetLibrary='Project Photos'&@TargetFileName='TestUpload.jpg'", file);
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
{ }
}

Create List Item with a multivalue lookup field

string url = "https://YourSite.com/Subsite/";
HttpClient client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
client.BaseAddress = new System.Uri(url);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("X-RequestDigest", digest);
client.DefaultRequestHeaders.Add("X-HTTP-Method", "POST");
HttpContent content = new StringContent("{ '__metadata': { 'type': 'SP.Data.ReportListItem' }, 'Title': 'NewTitle', 'PhotosId': { 'results': [2] }, 'Details': 'Another successful day!' }");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
content.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("odata", "verbose"));
HttpResponseMessage response = await client.PostAsync("_api/web/lists/GetByTitle('Report')/items", content);
response.EnsureSuccessStatusCode();
if (response.IsSuccessStatusCode)
{
}
else
{
}

The Json for lookups is a bit tricky.  You have to specify the ID of the item in the other list or of the user if its a people picker.  Also the column name here has ‘Id’ appended onto the end of it.  If the column already ends in Id then you will end up with Id twice.  In a multivalue scenario the Json looks like this:

‘PhotosId’: { ‘results’: [2,3] }

Singe Value:

‘PhotosId’: 2

Also you need to figure out the type represented here:

‘SP.Data.ReportListItem’

This is broken up as SP . Data . YourListName ListItem.  I have not tested this with Lists that have a space in their titles so I would test that or avoid spaces.

Updating a list item by ID

client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });
client.BaseAddress = new System.Uri(url);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("X-RequestDigest", digest);
client.DefaultRequestHeaders.Add("X-HTTP-Method", "MERGE");
client.DefaultRequestHeaders.Add("IF-MATCH", "*");
HttpContent strContent = new StringContent(String.Concat("{ '__metadata': { 'type': 'SP.List' }, 'Title': '", filename, "' }"));
strContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
strContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("odata", "verbose"));
HttpResponseMessage updateResponse = await client.PostAsync(String.Concat("_api/web/lists/GetByTitle('Project Photos')/Items(", id, ")"), strContent);
updateResponse.EnsureSuccessStatusCode();
if (updateResponse.IsSuccessStatusCode)
{}

Hopefully everyone finds this article helpful.  I do not wish to spend the rest of my days writing JavaScript every time I need to work on applications for mobility and I assume that at least a few other developers share my sentiments.

Keeping an eye on PKI

Introduction

As Microsoft recently saw with Azure and I have personally run into many times, SSL certificates are becoming more and more of key piece of infrastructure that can be very difficult to keep a handle on.  It’s one of the few technologies that serves mission critical roles from granting access to secured websites to encrypting traffic for LDAP and other protocols that has a built in count down timer to disaster.  Now several monitoring solutions implement solutions for monitoring certificates on hosts by defining a certificates port and days ahead of time to alert you but if you are dealing with heavily secured systems that is not always ideal and it requires that the person setting up the service remembers to put that check in place.

I propose that there is a better way, at least if you are lucky enough to be using an in-house Microsoft Certificate Authority.  I recently developed an application to keep our server list in sync with what systems we actually have online in the datacenter and the idea of tracking expired certificates somehow worked its way into the project toward the end.  The idea is that we connect to the CA and ask it what certs are up for expiration.  Now that would be that if all certificates were important enough to be notified about via email but you probably don’t want to know every time a user’s certificate expires and auto renews.  Luckily most mission critical SSL certificates are from a narrowly used template the “Web Server” template or a customized version of this template.

The CODE – C# with .NET 4.5

Your code sheet must be using Interop.CERTADMINLib.dll – this can be found on any system that can manage your CA

//Configure Certificate Authority settings and prepare error parameters and containers
string strServer = "YourCA.company.domain";
string strCAName = "YourCAName";
TimeSpan twoWeeks = new System.TimeSpan(14, 0, 0, 0);
TimeSpan aMonth = new System.TimeSpan(30, 0, 0, 0);
StringBuilder certList = new StringBuilder();
StringBuilder critcertList = new StringBuilder();
int pkiErr = 0;
int critpkiErr = 0;

try
{
//Configure CA Variables
CCertView certView = null;
IEnumCERTVIEWROW certViewRow = null;
IEnumCERTVIEWCOLUMN certViewColumn = null;

// Connecting to the Certificate Authority
certView = new CCertView();
certView.OpenConnection(strServer + "\\" + strCAName);

//Configure required columns
certView.SetResultColumnCount(5);
var Index0 = certView.GetColumnIndex(0, "CommonName");
var Index1 = certView.GetColumnIndex(0, "NotAfter");
var Index2 = certView.GetColumnIndex(0, "Certificate Template");
var Index3 = certView.GetColumnIndex(0, "Request ID");
var Index4 = certView.GetColumnIndex(0, "Revocation Reason");
certView.SetResultColumn(Index0);
certView.SetResultColumn(Index1);
certView.SetResultColumn(Index2);
certView.SetResultColumn(Index3);
certView.SetResultColumn(Index4);

//Open the view and iterate column values
certViewRow = certView.OpenView();
for (int x = 0; certViewRow.Next() != -1; x++)
{
certViewColumn = certViewRow.EnumCertViewColumn();
string certName = "Missing certificate name";
DateTime certExpire = DateTime.Now;
string certTemplate = "";
string certID = "";
bool certRev = false;

while (certViewColumn.Next() != -1)
{
if (certViewColumn.GetValue(1) != null)
{
if (certViewColumn.GetDisplayName() == "Issued Common Name")
{
certName = certViewColumn.GetValue(1).ToString();
}
if (certViewColumn.GetDisplayName() == "Certificate Expiration Date")
{
certExpire = (DateTime)certViewColumn.GetValue(1);
}
if (certViewColumn.GetDisplayName() == "Certificate Template")
{
certTemplate = certViewColumn.GetValue(1).ToString();
}
if (certViewColumn.GetDisplayName() == "Request ID")
{
certID = certViewColumn.GetValue(1).ToString();
}
if (certViewColumn.GetDisplayName() == "Revocation Reason")
{
certRev = true;
}
}
}
//Check for certs expiring a month out and notify the CA administrator once
if (certExpire == DateTime.Now.Add(aMonth) &amp; certTemplate == "WebServer" | certTemplate == "CustomTemplateInternalID")
{
pkiErr = pkiErr + 1;
certList.AppendLine("ID: " + certID + " " + certName + " will expire on: " + certExpire.ToString() + Environment.NewLine);
}
//Check for certs expiring two weeks out and notify EVERYONE everyday
else if (certExpire &gt;= DateTime.Now &amp; certExpire &lt;= DateTime.Now.Add(twoWeeks) &amp; certRev == false) { if (certTemplate == "WebServer" | certTemplate == "CustomTemplateInternalID") { critpkiErr = critpkiErr + 1; critcertList.AppendLine("ID: " + certID + " " + certName + " will expire on: " + certExpire.ToString() + Environment.NewLine); } } } //close CA connection certViewRow.Reset(); certView = null; //Define mail message MailMessage mail = new MailMessage(); SmtpClient SmtpServer = new SmtpClient("yourSMTP.Server"); mail.From = new MailAddress("pkimanager@noreply.nrp"); mail.Subject = "PKI Alert!"; // Send e-mail message with errors if (pkiErr &gt;= 1)
{
mail.To.Add("Someone@your.company");
mail.Body = certList.ToString();
SmtpServer.Send(mail);
}
else if (critpkiErr &gt;= 1)
{
mail.To.Add("LotsofPeople@your.company");
mail.Body = critcertList.ToString();
SmtpServer.Send(mail);
}
}
catch (Exception e)
{
//Do something to make your CA talk to this application
}

If you run this small app once a day it will quickly spit out any certs that are about to expire which will need to be renewed, revoked or both to clean up the alert.  You will need to use an account that has privileges to read the CA but I recall this being rather painless.  Hopefully you will find this useful as a centralized check for SSL expiration rather than host based checks that are always prone to false positives and require significantly more work to maintain.

The Work from Home Dilemma

Introduction

As IT departments face growing demands for security coming from within and increasing demand for flexibility coming from end users we often find ourselves deploying laptops with VPN connectivity.  Which while functional has some considerable costs and risks associated with it.  Further exacerbated by the need of many organizations to encrypt the hard drives of any corporate laptops deployed in the field.

A cheaper solution would be to let users work from home using computing equipment they already own.  But how do you do that without creating security holes of all shapes and sizes tends to be the big question.  Citrix is one solution that certainly works for many people but it also has a significant cost and complexity making it a difficult investment for some smaller organizations.

This article will describe the steps needed to get a user securely connected to a desktop at work via a web page.  All they will need is a web browser, a Yubikey and an Active Directory username and password.  With those three items you will presenting them with a 2-factor authentication solution using a one-time password (OTP) that requires you to open only one port into your DMZ, HTTPS or 443 which connects you to Microsoft’s premier access platform for security.

This solution relies on the following products:

Microsoft Forefront Unified Access Gateway 2010

A secured firewall and access gateway which has numerous single sign on options and 2-factor authentication support.

Ericom AccessNow and Secure Gateway

An HTML 5 web server which delivers Remote Desktop services in a browser agnostic web page with no client software required.

Scorpion Soft. AuthAnvil, NordicEdge OTP or Yubico YubiRadius – any form of radius server that supports Yubikeys

A radius server that will authenticate your OTP device, which in our case is a YubiKey.

Yubico YubiKeys

Your physical OTP device.

The Dime Tour

The basic idea of the solution is using Microsoft UAG 2010 to present a secured login page which will contain a username, password and OTP password field for use with your OTP radius server of choice.  All three have advantages and disadvantages and UAG works equally well with all of them.  Once you have signed into UAG successfully your password will be passed to via UAG’s own forumlar engine to a custom login page which will create the cookies used by Ericom AccessNow for login.  Once that happens you will be passed to a Microsoft Remote Desktop server or connection broker which will be rendered directly in the users browser.   Below is a basic network diagram showing the connectivity described above and showing the ports used for each connection.

The Unified Access Gateway

The glue that holds these pieces together is Microsoft’s UAG 2010 product.  It is a firewall, can sit behind other firewalls, and can be a stand-alone system to provide domain based logins and 2-factor authentication and provides a reverse proxy for internal web sites once a user has authenticated.  UAG can be used with a myriad of Microsoft products which are all pretty well documented.

First install UAG and it’s latest security patches and service packs.  Once you launch the software you will have to configure and internal and external network.  This requires two physical network adapters as UAG is first and foremost a firewall.  Once you have configured the network you must create an HTTPS trunk and select a Portal Trunk.  You must provide a Trunk Name, external IP, HTTP and HTTPS ports, and a public host name (something like uag.fqdn.url) which should NOT be the name you plan to publish your Remote Desktop web sites as, I will cover that later but they must be different.

Then you must then configure your authentication providers which in our case will be Active Directory and a Radius server for our OTP solution.

I wanted to mention here that UAG does NOT need to be joined to the domain to do Active Directory authentication but you will have to setup the provider just right to get it working.

Below you can see screenshots of both providers.  The key to the Active Directory provider is to use the NetBIOS name for your domain at the bottom of the form.  The FQDN will not work.

RADIUS is a little easier, just setup the right hostname and port, which is usually 1812 and setup a shared secret key on your RADIUS server for use here.

When you have added both provider click the check box that tells UAG to use the same username for all providers.  This way your users will only need to specify 3 pieces of information.

         

Next you will need to select the SSL certificate you wish to use with this trunk.  This should be a publicly signed certificate with the FQDN (something like access.your.company) you wish to publish your Remote Desktop application with and the FQDN (something like uag.your.company) you used for your Portal Trunk as a Subject Alternative Name.  The reason you need two names is because UAG acts as a reverse proxy for your application server, which should have the same FQDN internally and externally but UAG relies on the Portal Trunk to provide login services.  You will also be installing this certificate during the setup of Ericom Secure Gateway.

Next are a few screens that define Access Control policies used by UAC.  You will need to configure these based on your corporate policy and Microsoft has a lot of documentation on working with this part of UAG.  For my purposes I have allowed all clients.

Now that you have your trunk you must configure an application.  You can click add under applications on your Trunk.  You will select “Other Web Application (application specific hostname)” for your new application.

Give the application the name and a type (the type is important but can be whatever you want it to be, this has to do with SSO).  This application will be used to publish your remote desktop services so use the FQDN you specified when creating your certificate.  Again this cannot be the same as your Portal Trunk’s FQDN.  Next you must select access control policies for this application and configure and application server.

Here you will specify the FQDN of your server, leave the path as “/” and select HTTPS.  The public host name should match the FQDN above otherwise you will run into problems with your SSL certificate.

At step 6 you will be able to choose to use SSO, you should add an authentication server and select the Active Directory provider you setup earlier and HTML form as the type. (We require 2-Factor to login to UAG but most web applications only support basic authentication which is the beauty of the UAG security model.)

Finally at step 7 make sure the application URL reads as:

https://access.your.company/AccessNow/Login.html

This will be important to get SSO working with Ericom.  For now we should be done with UAG though it is a complex product and you may have to make some changes to settings to get it working in your environment.

The HTML5 Remote Desktop Solution

Ericom (www.ericom.com) offers a variety of products but the two we will need to purchase and install here are the Secure Gateway and AccessNow.  Both have free trials if you wish to demo this solution before you buy it.

The easiest way to configure the Ericom server is to install AccessNow and Secure Gateway on one system, though they recommend installing AccessNow on your Remote Desktop server for better performance.

You should take all the defaults when installing AccessNow.  When installing Secure Gateway you will need to install and select the SSL certificate you also used for your UAG server.  Otherwise take the defaults.

To configure the Secure Gateway you will need to navigate to the following directory, assuming you took the default installation settings:

C:\Program Files (x86)\Ericom Software\Ericom Secure Gateway\WebServer\AccessNow

Within that folder we are going to modify the Config.js and add two html files:

Login.html and Logout.html

Config.JS

// Configuration settings for Ericom AccessNowvar defaults = {overrideSaved: true,               // These settings override   saved user settings

//     onlyHTTPS: true,                 // Don’t use WebSockets – go   straight to HTTPS

//     noHTTPS: true,                   // Don’t allow using HTTPS   instead of WebSockets (HTTPS requires Ericom Secure Gateway)

autostart: false,                   // Start session   automatically

oldStyleProtocol:   false,            // Set to true to use   version 1.0 protocol

singlePort: true,

wsport: 8080,                       // AccessNow Server   port

gwport: 443,                        // Ericom Secure   Gateway port

showAddress: false,                 // Show server address in   error dialogs

dialogTimeoutMinutes:   2,            // Dialog timeouts

sessionTimeoutMinutes:   0,           // Zero disables feature

hiddenUpdateRateSeconds:   20,

keepAliveRateSeconds: 30,

minSendInterval: 30,

disableImageReuse: false,

clipboard: true,

clipboardTimeoutSeconds:   15,

clipboardUseFlash: true,

clipboardKey: 12,                   // Key to open clipboard   paste dialog. Set to false to disable

printing: false,

fileDownload: false,

fileUpload: false,

specialKeys: true,                  // See   http://support.microsoft.com/kb/186624

chromeKeys: true,

rightToLeft: false,

noEndDialog: false,                 // Do not display end of   session dialog

//     message: “”,

leaveMessage: “Leaving   this page will disconnect Ericom AccessNow”,

//     keyboard_locale:   “00000409”,

//       convert_unicode_to_scancode: true,

endURL: “logout.html”,                      // URL to go to when   session has ended (# value closes window; ^ prefix to assign to window   instead of top)

address:   “localhost”,                       // address of AccessNow server

full_address: “Your.RDP.Server”,                // address of RDP host

//     username: “”,

//     password:   “”,                    //   plain text

domain: “YourNetBiosDomainName”,

//     remember: false,                 // password

//     encryption: false,

blaze_acceleration: true,

blaze_image_quality: 40,

//     resolution:   “1024,768”,

use_gateway: true,

     gateway_address: “Access.Your.Company:443”,   // The same FQDN used for your Remote   Desktop application in UAG

//     audiomode:0,

//     remoteapplicationmode:   false,

//     alternate_shell:   “”,             // startup   application

//     shell_working_directory:   “”,

//     console: false,

//     settingsURL:   “resources/blaze.txt”, // URL from which to download connection   settings

//     hidden:   “remember”,                   // Advanced button is showAdvanced

//     restrictHost:   “127.*,localhost”,

_last: 0

};

Login.HTML (This file is our login form which is essential for SSO as it sets some important cookies and the default Start.HTML provided by Ericom does not play nicely with UAG.)

<html><head><body onload=”javascript:createCookie()”>

<script>

function createCookie()

{

var username =   document.forms[‘cookieform’].username.value;

var password =   document.forms[‘cookieform’].password.value;

var tomorrow =   new Date();

tomorrow.setTime(tomorrow.getTime()   + (1000*3600*24));

document.cookie   = “EAN_autostart=true; expires=”+tomorrow+”; path=/”;

document.cookie   = “EAN_username=”+username+”; expires=”+tomorrow+”;   path=/”;

document.cookie   = “EAN_password=”+password+”; expires=”+tomorrow+”;   path=/”;

if   (navigator.appName == “Microsoft Internet Explorer”)

{

window.location   = “/accessnow/start.html”;

}

else

{

window.location =   “/accessnow/start.html”;

}

}

</script>

</head>

<body>

<form name=”cookieform” action=”#”><p>

<input name=”username” input   type=”hidden”><br/>

<input name=”password” input   type=”hidden”><br/>

</form>

</body>

There is a little redundancy in the script on the login page but some people may want to do browser checks.  Only IE9 and up, Firefox, Google Chrome and Safari will work with Ericom due to HTML 5 support.  This page serves as a place holder for the login and password info which it then uses javascript to store this information in the cookies that Ericom’s login page normally creates.  This page is self-submitting so there is no need to have UAG run any JavaScript but we do need to setup one last thing which I will cover later to get UAG to fill out these fields and make the SSO work correctly.

Logout.HTML

<html><head><body>

Please close this window to completely logout.

</body>

</html>

The logout.html is just there to give the users and ending point once they logoff there remote desktop session.  Otherwise they will end up back at the Ericom Start.html which will allow them to connect to systems other than what you’ve configured.  Theoretically they should never need to see that page.

Back to UAG to complete to SSO configuration and come full circle

Now that we have Ericom ready for connection, you should test this before trying to reach it via UAG.  If the page comes up and you have configured UAG correctly everything should work smoothly.  The last steps is to configure the Formular engine within UAG.  This is a daunting challenge for most, but luckily I have done all the work for you and provided you with the exact login form to use.

You will need to go to the following directory:

%UAG Installation dir%\von\Conf\WebSites\YourTrunkName\conf

Create a file called “custom_submit.js” which should contain a blank function like exactly like this:

“Function FormLoginSubmit(){}”

(We are setting this up to override the default UAG JavaScript.)

Next you will need to go to this directory:

%UAG Installation dir%\von\Conf\WizardDefaults\FormLogin\CustomUpdate

Here you will create a file called FormLogin.xml with the following contents:

<WHLFILTFORMLOGIN ver=”1.0″><APPLICATION><APPLICATION_TYPE>YourCustomUAGApplicationType </APPLICATION_TYPE>

<USAGE description=”form_login”>

<PRIMARY_HOST_URL>.*/accessnow/login\.html</PRIMARY_HOST_URL>

<SCRIPT_NAME   source=”file”>custom_submit.js</SCRIPT_NAME>

<USER_AGENT>

<AGENT_TYPE   search=”group”>all_supported</AGENT_TYPE>

<POLICY>multiplatform</POLICY>

<SCRIPT_NAME   source=”data_definition”>FormLoginHandler</SCRIPT_NAME>

</USER_AGENT>

<LOGIN_FORM>

<NAME></NAME>

<METHOD>POST</METHOD>

<CONTROL handling=”real_value”>

<TYPE>USER_NAME</TYPE>

<NAME>username</NAME>

<DEF_VALUE>siteuser</DEF_VALUE>

</CONTROL>

<CONTROL handling=”real_value”>

<TYPE>PASSWORD</TYPE>

<NAME>password</NAME>

<DEF_VALUE>sitepass</DEF_VALUE>

</CONTROL>

</LOGIN_FORM>

</USAGE>

</APPLICATION>

</WHLFILTFORMLOGIN>

The only thing you have to change in this file is the application name at the top.  This MUST match the application name we setup earlier in UAG for your Remote Desktop application.  If the name’s don’t match exactly, SSO will not occur and you will be left sitting at a blank page or form.

Once you have added both files you must Activate your UAG settings, something that must be done any time you change UAG in any way.

The Finale!

Assuming you have done everything just right you should be able to login to the external site on your UAG server using the Remote Desktop applications public host name (You will need this to match the UAG servers public IP on your client but UAG server should resolve the DNS for this host name as belonging to the IP address on the internal Ericom server, that way you tunnel through UAG and end up on the Ericom server all using one hostname and SSL certificate.  Doing this any other way will probably not work.)

You will login with your domain credentials and your Yubikey and end up right on a Remote Desktop presented in HTML5 directly in the browser.

Not only will your users love the functionality but because of the nature of the Yubikey’s OTP and very simple network and firewall requirements this is a very secure solution and in many ways is more secure than laptops which can be stolen.

This is obviously a fairly complicated architecture relying on many moving parts but as you deploy it you will find it has a simple elegance and you can generally figure out where you have made mistakes along the way pretty easily.  I hope this post is helpful and I will try to write up more promising enterprise class solutions for Small to Midsize budgets for your IT needs.