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.