RemoteApp and Desktop Connection Feed Extensibility

RemoteApp and Desktop Connection Feed Extensibility

  • Comments 6

In our earlier blog post, we introduced RemoteApp and Desktop Connections to enable users to have their RemoteApp and Desktop icons integrated on their Windows 7 start menu.  RemoteApp and Desktop Connections works with this new feature of Remote Desktop Web Access (RD Web Access)--the RemoteApp and Desktop Connection feed. This RemoteApp and Desktop Connection feed provides significant extensibility to partners and customers in presenting remote resources in various ways. The connection feed feature contains information about published remote resources (e.g. RDP files and their associated icon and image files) in a software-parsable XML format.

On Remote Desktop Web Access (RD Web Access) Server, there are two URLs available to serve the connection feed. You need to choose one depending on the extensibility scenario.

  1. “/rdweb/pages/webfeed.aspx” URL:
    This URL is used for RD Web Access website extensibility. You can create a new web page similar to the RD Web Access default webpage. Below are some of the features that are possible by using this URL
    • Branding or customizations for your customers or organization.
    • Visually sorting or segregating remote resource types (e.g. RemoteApp and Remote Desktop resources).
    • Filtering remote resources at display time (for example, additional filtering apart from RemoteApp filtering ).
    • Silverlight-based webpage for a rich user experience.

By using this connection feed URL

  • There is no need to write a custom authentication mechanism because the webpage runs within the RD Web Access web application.
  • It is easier to support Single Sign On for launching remote resources. In a future post, we will cover on how to enable SSO.
  1. “/rdweb/feed/webfeed.aspx” URL:
    This URL can also be used to provide extensibility and is more flexible than the above URL. In addition to above feature set, below are some of the other features that are possible by using this URL:
    • Creating new client applications similar to RemoteApp and Desktop Connections for downlevel windows clients and for thin clients.
    • Creating new web portal similar to RD Web Access, not just a webpage alone.
    • Integrating remote resource presentation with your existing web portal.

By using this connection feed URL

    • Client applications get an authentication cookie that will not expire. This can be used for easier automatic updates without re-prompting user for credentials.

Here is a sample connection XML containing information about remote resources:

<?xml version="1.0" encoding="utf-8"?>

<ResourceCollection PubDate="2009-07-09T17:57:30.323Z" SchemaVersion="1.1" xmlns="http://schemas.microsoft.com/ts/2007/05/tswf">
  <Publisher LastUpdated="2009-07-09T17:57:12.588625Z" Name="Remote Desktop Services Default Connection" ID="Contoso" Description="">
    <Resources>
      <Resource ID="60fb077b94a241a473cf982140337213e4d93177" Alias="mspaint" Title="Paint" LastUpdated="2009-07-09T17:57:12.588625Z" Type="RemoteApp" ExecutableName="mspaint.exe">
        <Icons>
          <IconRaw FileType="Ico" FileURL="/RDWeb/Pages/rdp/mspaint032x32.ico" />
          <Icon32 Dimensions="32x32" FileType="Png" FileURL="/RDWeb/Pages/rdp/mspaint032x32.png" />
        </Icons>
        <FileExtensions />
        <HostingTerminalServers>
          <HostingTerminalServer>
            <ResourceFile FileExtension=".rdp" URL="/RDWeb/Pages/rdp/Contoso-mspaint.rdp" />
            <TerminalServerRef Ref="Contoso" />
          </HostingTerminalServer>
        </HostingTerminalServers>
      </Resource>
    </Resources>
    <TerminalServers>
      <TerminalServer ID="Contoso" Name="Contoso" LastUpdated="2009-07-09T17:57:12.588625Z" />
    </TerminalServers>
  </Publisher>
</ResourceCollection>

The bolded attributes in the connection provide information about the remote resources. The schema file for the connection is located at %windir%\schemas\tsworkspace\tswf.xsd in Windows Server 2008 R2 or on a Window 7 client machine.

Website extensibility: Using the connection to create a new web page

Following is a code sample in C# (ASP.NET) on how to use the connection to create a new web page as demonstrated at PDC 2008. This code sample uses a connection at the “/rdweb/pages/webfeed.aspx” location. Below are the steps that need to complete in creating the new web page.

  1. In Page PreInit, check for user authentication. If the user is not authenticated, redirect the request to the login page.
  2. In Page Load, get the connection by calling Server.Execute on the webfeed.aspx page.
  3. Parse the connection XML to get information on the remote resources. Use this information to present remote resources in your customized way.
  4. Replace the default.aspx at %windir%\Web\RDWeb\Pages\en-US folder with this newly created page. This webpage will run inside the RD Web Access web application using an out-of-the box authentication mechanism.

Notes on this sample code:

  1. It does not show how to enable RemoteApp and Desktop Connection’s Single Sign On feature.
  2. It does not show how to differentiate the page content between the admin and a regular user.
  3. It assumes that application locale is en-US.
<%@ Page Language="C#" Debug="false" Trace="false" %>

<% @Import Namespace="System.IO" %>
<% @Import Namespace="System.Collections.Generic" %>
<% @Import Namespace="System.Web.Configuration" %>
<% @Import Namespace="System.Xml" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script language="C#" runat="server">
    protected void Page_PreInit(object sender, EventArgs e)
    {
        AuthenticationMode eAuthenticationMode = AuthenticationMode.None;
        AuthenticationSection objAuthenticationSection = ConfigurationManager.GetSection("system.web/authentication") as AuthenticationSection;

        if (objAuthenticationSection != null)
        {
            eAuthenticationMode = objAuthenticationSection.Mode;
        }

        if (eAuthenticationMode == AuthenticationMode.Forms)
        {
            if (HttpContext.Current.User.Identity.IsAuthenticated == false)
            {
                // User is not logged in, so redirect the request to login page.
                Response.Redirect("login.aspx?ReturnUrl=" + Request.FilePath);
            }
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        try
        {
            string connectionXml;

            // Execute the handler for webfeed.aspx in the context of current request 
            // and capture output connectionXml.
            using (StringWriter stringWriter = new StringWriter())
            {
                // This new web page is running under the same web application, and 
                // with using existing authentication.
                // So We need run the Server.Execute to get the connection Xml.
                HttpContext.Current.Server.Execute("..//webfeed.aspx", stringWriter);
                connectionXml = stringWriter.ToString();
            }

            // Set current reponse content type
            Response.ContentType = "text/HTML";

            //Strip out the BOM (U+FEFF)
            connectionXml = connectionXml.Trim();

            // Sample code to parse the connection xml. Instead of parsing the connection xml, we can also use XSL transformations directly to display the remote resources
            List<ConnectionResourceInfo> listConnectionResourceInfos = GetConnectionResourceInfo(connectionXml);

            dataListResources.DataSource = listConnectionResourceInfos;
            dataListResources.DataBind();

            labelStatus.Text = "Connection Resources : " + listConnectionResourceInfos.Count;
        }
        catch (Exception ex)
        {
            labelStatus.Text = "Error occurred ~ " + ex.Message;
        }
    }

    private String GetAbsolutePath(string relativePath)
    {
        return String.Format("{0}://{1}{2}", Request.Url.Scheme, Request.Url.Host, relativePath);
    }

    // Parse the XML file for demo purpose. In this function we are not using all 
    // the xml information available, just the title and resource's image file path.
    private List<ConnectionResourceInfo> GetConnectionResourceInfo(string connectionXml)
    {
        List<ConnectionResourceInfo> listConnectionResourceInfos = new List<ConnectionResourceInfo>();
        XmlDocument xmlDocument = new XmlDocument();

        // Load the connection xml string
        xmlDocument.LoadXml(connectionXml);

        XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);
        xmlNamespaceManager.AddNamespace("tswf", "http://schemas.microsoft.com/ts/2007/05/tswf");

        // Get the list of Resource elements
        XmlNodeList xmlNodeList = xmlDocument.SelectNodes("/tswf:ResourceCollection/tswf:Publisher/tswf:Resources/tswf:Resource", xmlNamespaceManager);

        // For each resource get the title and image file relative path
        for (int i = 0; i < xmlNodeList.Count; i++)
        {
            ConnectionResourceInfo connectionResourceInfo = new ConnectionResourceInfo();

            connectionResourceInfo.Title = xmlNodeList[i].Attributes["Title"].Value;

            XmlNode xmlNodeIcon32 = xmlNodeList[i].SelectSingleNode("tswf:Icons/tswf:Icon32", xmlNamespaceManager);

            // Convert the image's relative path to ablosute path
            connectionResourceInfo.ImageFilePath = GetAbsolutePath(xmlNodeIcon32.Attributes["FileURL"].Value);

            listConnectionResourceInfos.Add(connectionResourceInfo);

            // Here you would want to retrieve any other XML information you
            // want for each resource from the connection
        }

        return listConnectionResourceInfos;
    }

    protected void SignOut_Click(object sender, EventArgs e)
    {
        //Redirect the request to logoff page
        Response.Redirect("LogOff.aspx");
    }

    // Entity class to hold connection resource info, for now it contains only the resource title, and its image path
    private class ConnectionResourceInfo
    {
        private string title;
        private string imageFilePath;

        public string Title
        {
            get { return title; }
            set { title = value; }
        }

        public string ImageFilePath
        {
            get { return imageFilePath; }
            set { imageFilePath = value; }
        }
    }
</script>

<html>
<head id="Head1" runat="server">
    <meta http-equiv="CONTENT-TYPE" content="TEXT/HTML; CHARSET=UTF-8">
    <meta http-equiv="CACHE-CONTROL" content="NO-CACHE">
    <meta http-equiv="PRAGMA" content="NO-CACHE" />
    <title>New RD Web Access</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Button ID="btnSignOut" runat="server" Text="SignOut" OnClick="SignOut_Click" />
        <br />
        <asp:Label ID="labelStatus" runat="server" Text=""></asp:Label>
        <br />
        <asp:DataList ID="dataListResources" runat="server" RepeatDirection="Horizontal"
            RepeatColumns="4" BorderColor="black" CellPadding="1" BorderWidth="1" ItemStyle-BorderWidth="1">
            <ItemTemplate>
                <asp:Image ID="ResImage" ImageUrl='<%# DataBinder.Eval(Container.DataItem, "ImageFilePath") %>'
                    runat="server" /><br />
                <%# DataBinder.Eval(Container.DataItem, "Title") %>
            </ItemTemplate>
        </asp:DataList>
    </div>
    </form>
</body>
</html>

The sample page will appear as below.

image

Customized client applications: Using the connection to create a new client application

Following is a code sample in C# on how to use the connection to create a client application. This code sample uses the connection at the “/rdweb/feed/webfeed.aspx” location.

  1. Send request to the connection URL page with auto-redirect enabled along with user credentials. The connection URL uses cookie-based authentication, so this initial request sent to the connection URL will be redirected to the login page. The login page, which uses Windows-based authentication, responds with the authentication cookie. The supplied user credentials in the request will be used in RemoteApp filtering.
  2. Cache the authentication cookie for all subsequent requests. The cookie will not expire, so you can also reuse the same cookie for easy automatic connection updating without prompting the user for credentials.
  3. Send the request to the connection URL with the authentication cookie, and get the connection XML in response.
  4. Parse the connection XML, and download subsequent resource files and their associated icon and image files.
using System.IO; 
using System.Net;
using System.Web.Security;

private void GetConnectionContents()
{
    System.Diagnostics.ConsoleTraceListener trace = new System.Diagnostics.ConsoleTraceListener();

    //RemoteApp and Desktop Connections uses HTTPS to connect to the server. 
    //In order to connect properly, the client operating system must trust the SSL certificate of the RD Web Access server. 
    //Also, the server name in the URL must match the one in the server’s SSL certificate

    // User credentials to access the connection. Fill in <username>, <password>, <domainname> with your user credentials.
    NetworkCredential networkCredential = new NetworkCredential("<username>", "<password>", "<domainname>");            

    //This URL will generally be of this form.  Fill in <servername> with your server’s name
    string connectionUrl = "https://<servername>/rdweb/feed/webfeed.aspx";            

    string formsAuthenticationCookie = GetFormsAuthenticationCookie(connectionUrl, networkCredential, trace);

    string connectionXml = GetConnectionXml(connectionUrl, formsAuthenticationCookie, trace);

    //Fill in your code to parse the connection, and to download resource files and associated icon & image files. Use earlier cache cookie for authentication. 
}

private string GetFormsAuthenticationCookie(string connectionUrl, NetworkCredential networkCredential, System.Diagnostics.ConsoleTraceListener trace)
{
    //
    // Request connection page is protected by Forms Authentication Cookie. So making a request to that page will be redirected to login page
    // The login page is
    //
    trace.Write("Requesting : " + connectionUrl);
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(connectionUrl);
    CredentialCache credentialCache = new CredentialCache();
    credentialCache.Add(new Uri(connectionUrl), "Negotiate", networkCredential);
    httpWebRequest.Credentials = credentialCache;
    httpWebRequest.AllowAutoRedirect = true;

    HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();

    if (httpWebResponse.StatusCode == HttpStatusCode.OK)
    {
        trace.Write("Response: 200 ");
        trace.WriteLine(httpWebResponse.StatusCode);
    }
    else
    {
        trace.Fail("Response status is " + httpWebResponse.StatusCode + ". Expected was OK");
    }

    trace.WriteLine("Response will be the Forms Authentication Cookie");
    trace.WriteLine("");

    string formsAuthenticationCookie;

    using (StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
    {
        formsAuthenticationCookie = streamReader.ReadToEnd();
        streamReader.Close();
    }

    trace.WriteLine("formsAuthenticationCookie " + formsAuthenticationCookie);

    return formsAuthenticationCookie;
}

private string GetConnectionXml(string connectionUrl, string formsAuthenticationCookie, System.Diagnostics.ConsoleTraceListener trace)
{
    //
    // Request connection page
    //
    trace.Write("Requesting : " + connectionUrl);
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(connectionUrl);

    httpWebRequest.CookieContainer = new CookieContainer();

    //
    // Set Froms Authentication Cookie
    //
    httpWebRequest.CookieContainer.Add(new Cookie(FormsAuthentication.FormsCookieName, formsAuthenticationCookie, "/", httpWebRequest.RequestUri.Host));

    // Get response
    HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();

    if (httpWebResponse.StatusCode == HttpStatusCode.OK)
    {
        trace.Write("Response: 200 ");
        trace.WriteLine(httpWebResponse.StatusCode);
    }
    else
    {
        trace.Fail("Response status is " + httpWebResponse.StatusCode + ". Expected was OK");
    }

    trace.WriteLine("Response will be the connectionXml");
    trace.WriteLine("");

    string connectionXml;

    using (StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
    {
        connectionXml = streamReader.ReadToEnd();
    }

    trace.WriteLine("connectionXml " + connectionXml);

    return connectionXml;
}

As shown above, RemoteApp and Desktop Connection provides an easy way to customize the standard look and feel of RD Web Access or to present remote resources in new ways.

Leave a Comment
  • Please add 4 and 6 and type the answer here:
  • Post
  • This example does not show how to launch the remote application based on the connecionXML and so is not of much help

  • Any chance of an example of how to launch the applications?

  • Can we do the same in Server 2012?  and how do we launch the remote application in our own web page?

  • When I try the code above in a new page I get the following error:

    Error occurred ~ Data at the root level is invalid. Line 1, position 1.

    I'm not very good with XML services, but I can tell it's not reading it correctly - anyone know where to start with this??

  • I was able to get the feed on a 2012 server by changing the code as below, however as other people have mentioned how do you actually launch the resource??

    private List<ConnectionResourceInfo> GetConnectionResourceInfo(string connectionXml)

           {

               List<ConnectionResourceInfo> listConnectionResourceInfos = new List<ConnectionResourceInfo>();

               XmlDocument xmlDocument = new XmlDocument();

               // Load the connection xml string

               byte[] encodedString = Encoding.UTF8.GetBytes(connectionXml);

               // Put the byte array into a stream and rewind it to the beginning

               using (var ms = new MemoryStream(encodedString))

               {

                   ms.Flush();

                   ms.Position = 0;

                   // Build the XmlDocument from the MemorySteam of UTF-8 encoded bytes

                   xmlDocument.Load(ms);

               }

               XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);

               xmlNamespaceManager.AddNamespace("tswf", "schemas.microsoft.com/.../tswf");

               // Get the list of Resource elements

               XmlNodeList xmlNodeList = xmlDocument.SelectNodes("/tswf:ResourceCollection/tswf:Publisher/tswf:Resources/tswf:Resource", xmlNamespaceManager);

               // For each resource get the title and image file relative path

               for (int i = 0; i < xmlNodeList.Count; i++)

               {

                   ConnectionResourceInfo connectionResourceInfo = new ConnectionResourceInfo();

                   connectionResourceInfo.Title = xmlNodeList[i].Attributes["Title"].Value;

                   XmlNode xmlNodeIcon32 = xmlNodeList[i].SelectSingleNode("tswf:Icons/tswf:Icon32", xmlNamespaceManager);

                   // Convert the image's relative path to ablosute path

                   connectionResourceInfo.ImageFilePath = GetAbsolutePath(xmlNodeIcon32.Attributes["FileURL"].Value);

                   listConnectionResourceInfos.Add(connectionResourceInfo);

                   // Here you would want to retrieve any other XML information you

                   // want for each resource from the connection

               }

               return listConnectionResourceInfos;

           }

  • Just another useless example, brought to you by Microsoft!  Useless because you can't launch an app.

    WTF!!!

Page 1 of 1 (6 items)