Silverlight Multipurpose WebRequest Proxy

A multipurpose cross-domain server side proxy handler for Silverlight. Easily make WebRequests to other domains where there is no cross-domain policy file present. Can be used with multiple applications.

One of the repetitive issues that presents itself to the Silverlight developer is the fact that the majority of web-based resources, content and APIs do not expose a crossdomain.xml or clientaccesspolicy.xml policy file on their domain. When a WebRequest of any type is made from a Silverlight application for such a resource that is not located on the same domain from which the Silverlight app was loaded, Silverlight first attempts to load either of these policy files. If the file is not present, or it is too restrictive, the WebRequest will fail with a security exception.

I made a first effort at solving this problem with my ASHX server-side request handler for images. This was well-received, so I decided to "take it up a notch". What kind of WebRequests - besides for images -- would a developer need to make?  The answers that I came up with are fourfold:

1) A plain-old GET request for a resource of any type. This would almost always return a string. It could be a string of XML, of JSON, plain old HTML, or a text string. The request could be a RESTful query, or something else.

2) A GET Request for an image - of any type. I have already covered this in the above - referenced article, including the ability to convert a request for a GIF image to a JPG image (Silverlight cannot consume GIF images currently). That code is reproduced here.

3) A POST request that contains FORM field data in " name=value" pairs. For example, a POST that would "fill in" a Login Form and submit it.

4) A POST request that sends an XML String, for example a SOAP POST  request to an ASMX or WCF WebService.

Those are the basic Requests that I came up with. I'm sure there are others, such as a PUT or HEAD verb, but I chose not to cover these here.

The underlying concept here is to enable Silverlight to make a "same domain" WebRequest for whatever it wants. Same domain --  means a request to the same domain from which the Silverlight application was originally served. The idea is that the receiving proxy handler class at the server will accept this request, figure out which type it is, then go out to the remote domain to GET  (or POST) to the resource, returning the response type that Silverlight expects. Since Silverlight sees that it is making this request to the same domain, it has no knowledge that the request is actually being proxied out to a remote domain, and therefore there are no security issues involved (at least - as far as Silverlight is concerned!). We also have to figure out a way to pass on any cookies and HTTP Headers that may be present in the original request.

What is involved here is two "pieces": First, we need a simple method to "convert" the Uri endpoint that Silverlight really wants to hit into a server-side proxied call. This is done with a simple static method that can be placed in the App.xaml.cs class so that it is accessible from anywhere in your Silverlight app:

public static string ConvertUrl(string targetUrl)
{
  string finalUrl = Application.Current.Host.Source.Scheme + "://" + Application.Current.Host.Source.Host + ":" +
  Application.Current.Host.Source.Port + "/ClientAccess.ashx?url=" + targetUrl;
  return finalUrl;
}

Switching over to the server, here is the code for my "enhanced" proxy ASHX handler. First code, then an explanation of what it does:

using System;
using System.Collections.Specialized;
using System.Drawing;
using System.IO;
using System.Net;
using System.Text;
using System.Web;

namespace SLImageHandler.Web
{
    public class ClientAccess : IHttpHandler
    {
         public void ProcessRequest(HttpContext context)
         {
             string theUrl = context.Request.Url.ToString().ToLower();
             // handle image requests
            if (context.Request.HttpMethod == "GET" &&
                 (theUrl.Contains(".jpg") || theUrl.Contains(".gif") || theUrl.Contains(".png")))
            {
                 string imageUrl = context.Request["url"];
                var client = new WebClient();
                 //add cookie headers
                client = SetCookies(client, context.Request);
                 byte[] bytes = client.DownloadData(new Uri(imageUrl));
                 // convert gif to jpg  here for Silverlight consumption:
                 if (imageUrl.Contains(".gif"))
                {
                    var bmp = (Bitmap) Image.FromStream(new MemoryStream(bytes));
                    bytes = GifConverter.ConvertGif(bmp);
                 }
                 context.Response.BinaryWrite(bytes);
                 client.Dispose();
             }
             // handle a plain GET request...
            else if (context.Request.HttpMethod == "GET")
            {
                var client = new WebClient();
                 //add cookie headers
                client = SetCookies(client, context.Request);
                 string s = client.DownloadString(context.Request["url"]);
                 context.Response.Write(s);
                 client.Dispose();
            }
            // handle a POST request
            if (context.Request.HttpMethod == "POST")
            {
                 if (context.Request.ContentType == "text/xml") // xml post, as to webservice
                {
                    var wc = new WebClient();
                    wc = SetCookies(wc, context.Request);
                    string soapAction = context.Request.Headers["SOAPAction"];
                    wc.Headers["Content-Type"] = "text/xml";
                    wc.Headers["SOAPAction"] = soapAction;
                    byte[] bdata = context.Request.BinaryRead(context.Request.TotalBytes);
                    byte[] result = null;
                    try
                    {
                        result = wc.UploadData(context.Request["url"], bdata);
                    }
                    catch (Exception ex)
                    {
                        System.Diagnostics.Debug.WriteLine(ex.ToString() );
                    }
                    string returnData = System.Text.Encoding.UTF8.GetString(result);
                    context.Response.BinaryWrite(result);
                    wc.Dispose();

}
                 else // Form data post
                {
                    var wc = new WebClient();
                     //add cookie headers
                    wc = SetCookies(wc, context.Request);
                      wc.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
                    NameValueCollection nameValueCollection = context.Request.Form;
                      byte[] responseArray = wc.UploadValues(context.Request["url"], "POST", nameValueCollection);
                     string returnString = Encoding.UTF8.GetString(responseArray);
                      context.Response.Write(returnString);
                      wc.Dispose();
                 }
             }
         }
         // transfer any cookies from original Silverlight request to real outbound request
        private WebClient SetCookies(WebClient wc, HttpRequest request)
        {
            HttpCookieCollection coll = request.Cookies;
             foreach (string cookieName in coll.Keys)
             {
                 wc.Headers.Add("Cookie", cookieName + "=" + coll[cookieName].Value);
             }
             return wc;
        }
private WebClient SetHeaders(WebClient wc, NameValueCollection headers)
        {
            string hdrvalue = "";
            foreach(string headerName in headers.Keys )
            {
                hdrvalue = headers[headerName];
                if(headerName =="Content-Type" || headerName =="SOAPAction" )
                wc.Headers.Add(headerName,  hdrvalue ); 

            }
            return wc;
        }
public bool IsReusable
        {
            get { return false; }
        }
     }
}


The first "test" in the code above is to see if we have a request for an image. if so, I am simply implementing the code from the previous article. We also test for ".gif". Since Silverlight cannot display GIF images natively, I do a simple conversion to JPG and serve it out so Silverlight will be happy.

The next test is for a "plain old" GET Request that isn't for an image. This could be a RESTful call, or it could be something else. It might even be for the contents of a web page that the Silverlight developer wants to "screen scrape" for some values or content. Or it could be a request for an RSS feed. In any case, we are returning the result as a string - so it is up to the Silverlight app to know what it is expecting to receive, and also what to do with the content.


The third "test" is for a POST where the Content-Type is "text/xml". This is typically what gets sent to an ASMX or WCF WebService. The resulting XML response is sent back to Silverlight as a string, where it is up to the Silverlight developer to parse the returned XML - for example, with XElement.Parse(string). Note that I do not use the SetHeaders method here as there may be conflicting headers coming from the Silverlight request. Instead, I retrieve the SOAPAction header from the client, and add that plus the Content-Type header to the outbound WebClient instance. If you are working with a WebService that requires similar headers to "SOAPAction", you'll need to add them into the code I provide. You may also want to add in some code to check for nulls.

Finally, the fourth test is also for a POST, but a "Form" post - where the data is passed as a NameValueCollection of form field names and values. Note that in either of the POST cases, the appropriate Content-Type request header must be set.

In each case, I try to replicate the cookies and any request headers that are present so that they get proxied through to the real endpoint.

The rest of this app is simply a "Proof of concept" that tests and shows the results of each of the four methods. I'll leave that out here, you can see the usage in the downloadable demo solution. One last thing - whiile the solution is for Silverlight 3, it will work just fine in Silverlight 2. All you need to do is create a new Silverlight 2 application and ASP.NET Web app to go with it, and bring in the xaml and class files in each.

What does this buy me?

What this does for you as a developer is to provide an easy way to make cross-domain requests from your Silverlight application in a transparent way and get back your result, whatever it may be, regardless of the type of request.

You can download the Silverlight 3 Visual Studio 2008 solution here.

By Peter Bromberg   Popularity  (5882 Views)