Condividi tramite


Proxying Cross-domain Web Requests in Silverlight

If you want to get straight to the code, you can find it here:

I was going to think of a clever title for this post, but then I realized that I’d really rather prefer that people be able to find this post when they’re looking for a solution to this problem. All Silverlight application developers (and I’m sure a few Flash developers as well) have come across this problem sooner or later. You see a great web service, you realize it could really use a nice RIA frontend. You set out to build it, only to realize that the builders of the web service didn’t put up a crossdomain.xml or clientaccesspolicy.xml file that lets your Silverlight application make requests to their web service. Now you have one of three choices:

  1. Convince the web site owners to put your application on their server. Cheeky? Yes. And it could be a looong time before that happens :)
  2. Try selling them on the idea of putting up a cross-domain policy file. In my experience most web sites are amenable to the idea. In this case, you’re stuck waiting for them to put the file on the server
  3. Write some code on your web server that can proxy your requests to this web service for you. This gets you off running quick, but then you’ve coded up a frontend that relies on this proxy web service

Option 3 gets you up and running fastest, but it also sticks you with the task of migrating your code when the web service eventually adds a cross-domain policy file. Wouldn’t it be nice if there we a way for you to write code very similar to one you’d eventually use? So that you could just flip a switch (or change the fewest lines of code) when the real thing came along? Well I’m glad you asked :) Because of course there is.

First things first, this is NOT a replacement for the built in HttpWebRequest class. In fact, the code here is pure demoware, you have been warned. With that out of the way, let us first begin by looking at our proxy web service. All the code for the service is located in the Knowledgecast.HttpProxy.Server project. The ServiceContract is simple:

    1: /// <summary>
    2:     /// The service contract for a our proxy web request service
    3:     /// </summary>
    4:     [ServiceContract(Namespace = "https://Knowledgecast.HttpProxy.Server.IMakeWebRequest")]
    5:     interface IMakeWebRequest
    6:     {
    7:         /// <summary>
    8:         /// Make an HttpWebRequest on the client's behalf
    9:         /// </summary>
   10:         /// <param name="url">The url for the request</param>
   11:         /// <param name="method">The method, GET/POST/PUT/DELETE</param>
   12:         /// <param name="requestBody">The request body (if applicable)</param>
   13:         /// <param name="contentType">The content type of the request</param>
   14:         /// <param name="headers">Any headers that need sending out as part of the request</param>
   15:         /// <returns>The response for the web request formatted as a 
   16:         /// custom data contract called ProxyWebResponse. We can't use HttpWebResponse
   17:         /// because it can't be serialized. Also, its a framework class.</returns>
   18:         [OperationContract]
   19:         ProxyWebResponse MakeHttpWebRequest(string url, string method, byte[] requestBody, string contentType, Dictionary<string, string> headers);
   20:     }

The contract has only one method called MakeHttpWebRequest. I’m not going to show off the method code here (link to the code is at the bottom of this post) because all it does is create a new HttpWebRequest based on the paramters in the MakeHttpWebRequest operation. There’s some special handling for headers such as “accept” and “useragent” because HttpWebRequest expects them to be set as properties on the HttpWebRequest instance rather than as additions to the Headers collection.

The other interesting that’s worth noting is the return type. You might have expected to see HttpWebRequest here but there’s a couple of reasons I don’t use it. For one thing, HttpWebRequest is a class that belongs to the .NET framework so it’s not a good idea to use it in our service contract. The other problem is that HttpWebRequest isn’t marked with the DataContract attribute. This is the same reason the ProxyWebRequest class used here does not inherit the WebResponse class like you’d probably expect it to. In fact, ProxyWebRequest is a plain-jane data contract. Here’s what it looks like:

    1: /// <summary>
    2:     /// This data contract is used to return the data that came out as part of 
    3:     /// the HttpWebResponse
    4:     /// </summary>
    5:     [DataContract(Namespace = "https://Knowledgecast.HttpProxy.Server.IMakeWebRequest")]
    6:     public class ProxyWebResponse
    7:     {
    8:         /// <summary>
    9:         /// Takes in an HttpWebResponse and populates itself with the relevant
   10:         /// values
   11:         /// </summary>
   12:         /// <param name="response"></param>
   13:         public ProxyWebResponse(HttpWebResponse response)
   14:             : this(response.IsFromCache, response.ResponseUri, response.IsMutuallyAuthenticated)
   15:         {
   16:             ContentLength = response.ContentLength;
   17:             ContentType = response.ContentType;
   18:             Stream s = response.GetResponseStream();
   19:  
   20:             int bytesRead = 0;
   21:             int numBytesToRead = (int)ContentLength;
   22:             if (s.CanRead)
   23:             {
   24:                 ResponseBody = new byte[ContentLength];
   25:                 while (true)
   26:                 {                    
   27:                     int n = s.Read(ResponseBody, bytesRead, numBytesToRead);
   28:                     if (n == 0)
   29:                     {
   30:                         break;
   31:                     }
   32:  
   33:                     bytesRead += n;
   34:                     numBytesToRead -= n;
   35:                 }
   36:  
   37:                 s.Close();
   38:             }
   39:  
   40:             if (response.Headers != null && response.Headers.Count > 0)
   41:             {
   42:                 Headers.Add(response.Headers);
   43:             }
   44:         }
   45:  
   46:         /// <summary>
   47:         /// Constructor
   48:         /// </summary>
   49:         /// <param name="isFromCache">Is the response from a cache?</param>
   50:         /// <param name="responseUri">The response URL</param>
   51:         /// <param name="isMutuallyAuthenticated">Are the server/client mutually authenticated?</param>
   52:         public ProxyWebResponse(bool isFromCache, Uri responseUri, bool isMutuallyAuthenticated)
   53:         {
   54:             IsFromCache = isFromCache;
   55:             ResponseUri = responseUri;
   56:             IsMutuallyAuthenticated = isMutuallyAuthenticated;
   57:         }
   58:  
   59:         /// <summary>
   60:         /// The number of bytes returned by the request. Content length does not include
   61:         ///     header information.
   62:         /// </summary>
   63:         [DataMember]
   64:         public long ContentLength { get; set; }
   65:  
   66:         /// <summary>
   67:         /// A string that contains the content type of the response.
   68:         /// </summary>
   69:         [DataMember]
   70:         public string ContentType { get; set; }
   71:  
   72:         HeaderCollection _headers;
   73:         /// <summary>
   74:         /// A HeaderCollection that contains the header information returned
   75:         ///     with the response.
   76:         /// </summary>
   77:         [DataMember]
   78:         public HeaderCollection Headers
   79:         {
   80:             get
   81:             {
   82:                 if (_headers == null)
   83:                 {
   84:                     _headers = new HeaderCollection();
   85:                 }
   86:  
   87:                 return _headers;
   88:             }
   89:             set
   90:             {
   91:                 _headers = value;
   92:             }
   93:         }
   94:  
   95:         /// <summary>
   96:         /// true if the response was taken from the cache; otherwise, false.
   97:         /// </summary>
   98:         [DataMember]
   99:         public bool IsFromCache { get; set; }
  100:  
  101:         /// <summary>
  102:         /// A string that contains the name of the server that sent the response.
  103:         /// </summary>
  104:         [DataMember]
  105:         public Uri ResponseUri { get; set; }
  106:  
  107:         /// <summary>
  108:         /// true if mutual authentication occurred; otherwise, false.
  109:         /// </summary>
  110:         [DataMember]
  111:         public bool IsMutuallyAuthenticated { get; set; }
  112:  
  113:         /// <summary>
  114:         /// The body of the response as a byte buffer. This is obtained
  115:         /// by reading in the stream of bytes returned by 
  116:         /// HttpWebResponse.GetResponseStream
  117:         /// </summary>
  118:         [DataMember]
  119:         public byte[] ResponseBody { get; set; }
  120:     }

 

 

That takes care of the web service. Now lets move on to the client. This is where all the clever code is anyway. We need to make sure it is very easy for you to switch from using this custom proxy service to the real HttpWebRequest when the web site owners eventually put a cross-domain policy online. So we should find a way to plug in to the existing WebRequest infrastructure on the client. Turns out that this is really straightforward to do. First, we start out by implementing a custom version of the WebRequest class. Here’s what our ProxyWebRequest looks like:

    1: /// <summary>
    2:     /// The ProxyWebRequest internally forwards the web request made
    3:     /// on it to the Web Request Proxy service. This means that the server
    4:     /// actually makes the request on the Silverlight application's behalf
    5:     /// thus removing the cross-domain web request restrictions of Silverlight. Be
    6:     /// mindful however that this puts stress on the server and also
    7:     /// complicates authentication (double hop et al)
    8:     /// </summary>
    9:     public class ProxyWebRequest : WebRequest
   10:     {
   11:         ProxyService.ProxyWebResponse _response = null;
   12:         Knowledgecast.HttpProxy.Client.ProxyService.MakeWebRequestClient client = null;
   13:         byte[] _requestBody;
   14:  
   15:         /// <summary>
   16:         /// Create a ProxyWebRequest based on the url
   17:         /// </summary>
   18:         /// <param name="url">The URL to which a request must be created</param>
   19:         public ProxyWebRequest(Uri url)
   20:         {
   21:             _requestUri = url;
   22:             client = new Knowledgecast.HttpProxy.Client.ProxyService.MakeWebRequestClient();
   23:             client.MakeHttpWebRequestCompleted += MakeHttpsWebRequestCompleted;
   24:         }
   25:  
   26:         void MakeHttpsWebRequestCompleted(object sender, Knowledgecast.HttpProxy.Client.ProxyService.MakeHttpWebRequestCompletedEventArgs e)
   27:         {
   28:             if (e.Cancelled == false && e.Error == null)
   29:             {
   30:                 object[] state = e.UserState as object[];
   31:                 if (state != null)
   32:                 {
   33:                     AsyncCallback callback = state[0] as AsyncCallback;
   34:                     object originalState = state[1];
   35:                     _response = e.Result;
   36:                     callback(new ProxyWebRequestAsyncResult(originalState, true));
   37:                 }
   38:             }
   39:         }
   40:  
   41:         /// <summary>
   42:         /// Not implemented
   43:         /// </summary>
   44:         public override void Abort()
   45:         {
   46:             throw new NotImplementedException();
   47:         }
   48:  
   49:         /// <summary>
   50:         /// Return a request stream that can be used to fill in request
   51:         /// data. This is an asynchronous method
   52:         /// </summary>
   53:         /// <param name="callback">Callback method to call when the request stream is ready</param>
   54:         /// <param name="state">State to pass on to the callback method</param>
   55:         /// <returns>An IAsyncResult indicating the status of the request</returns>
   56:         public override IAsyncResult BeginGetRequestStream(AsyncCallback callback, object state)
   57:         {
   58:             var result = new ProxyWebRequestAsyncResult(state, true);
   59:             callback(result);
   60:             return result;
   61:         }
   62:  
   63:         /// <summary>
   64:         /// Call this method in the callback method passed to BeginGetRequestStream
   65:         /// to actually get the stream. Ensure that you set the ContentLength property
   66:         /// before you call this method or you will get a stream with 0 capacity
   67:         /// </summary>
   68:         /// <param name="asyncResult">IAsyncResult that was passed to the callback method</param>
   69:         /// <returns>Stream which can be used to populate the request data (if applicable)</returns>
   70:         public override Stream EndGetRequestStream(IAsyncResult asyncResult)
   71:         {
   72:             _requestBody = new byte[(int)ContentLength];
   73:             Stream s = new MemoryStream(_requestBody, true);
   74:             return s;
   75:         }
   76:  
   77:         /// <summary>
   78:         /// Begins an asynchronous request for
   79:         ///     an Internet resource.
   80:         /// </summary>
   81:         /// <param name="callback">The System.AsyncCallback delegate</param>
   82:         /// <param name="state">An object containing state information for this asynchronous request</param>
   83:         /// <returns>An System.IAsyncResult that references the asynchronous request</returns>
   84:         public override IAsyncResult BeginGetResponse(AsyncCallback callback, object state)
   85:         {
   86:             var result = new ProxyWebRequestAsyncResult(state, false);
   87:  
   88:             Dictionary<string, string> headersDictionary = null;
   89:             if (Headers != null && Headers.Count > 0)
   90:             {
   91:                 headersDictionary = new Dictionary<string, string>();
   92:                 foreach (var key in Headers.AllKeys)
   93:                 {
   94:                     headersDictionary.Add(key, Headers[key]);
   95:                 }
   96:             }
   97:  
   98:             client.MakeHttpWebRequestAsync(RequestUri.AbsoluteUri, Method, _requestBody, ContentType, headersDictionary, new object[] { callback, state });
   99:             return result;
  100:         }
  101:  
  102:         /// <summary>
  103:         /// Returns a System.Net.WebResponse.
  104:         /// </summary>
  105:         /// <param name="asyncResult">An System.IAsyncResult that references a pending request for a response</param>
  106:         /// <returns>A System.Net.WebResponse that contains a response to the Internet request.</returns>
  107:         public override WebResponse EndGetResponse(IAsyncResult asyncResult)
  108:         {
  109:             return new ProxyWebResponse(_response);
  110:         }
  111:  
  112:         /// <summary>
  113:         /// The content type of the request data
  114:         /// </summary>
  115:         public override string ContentType { get; set; }
  116:  
  117:         /// <summary>
  118:         /// The number of bytes of request data being sent
  119:         /// </summary>
  120:         public override long ContentLength { get; set; }
  121:  
  122:         /// <summary>
  123:         /// A System.Net.WebHeaderCollection containing the header name/value pairs associated
  124:         ///     with this request.
  125:         /// </summary>
  126:         public override WebHeaderCollection Headers { get; set; }
  127:  
  128:         /// <summary>
  129:         /// The protocol method to use in this request
  130:         /// </summary>
  131:         public override string Method { get; set; }
  132:  
  133:         private Uri _requestUri;
  134:         /// <summary>
  135:         /// A System.Uri representing the resource associated with the request
  136:         /// </summary>
  137:         public override Uri RequestUri
  138:         {
  139:             get
  140:             {
  141:                 return _requestUri;
  142:             }
  143:         }
  144:     }

 

We override all of the overrideable properties and methods available in the WebResponse class. The most important method in the above code is our implementation of the BeginGetResponse method. The implementation actually calls the MakeHttpWebRequest service method on the service proxy. As with all calls to web services in Silverlight, the call is asynchronous. In the completed event (the MakeHttpsWebRequestCompleted method) we save the returned ProxyWebResponse into a class member variable. We then call the callback that the client code registered when it called BeginGetResponse. When the EndGetResponse method gets called (usually from the callback method) we instantiate a new ProxyWebResponse (there’s a client version as well, which inherits from WebResponse) based on the ProxyWebResponse returned by the service. The reason we need to do this is because the EndGetResponse method MUST return a type that inherits from WebResponse.

Now the last thing we need to do is make sure client code has a way of getting a handle to our custom ProxyWebRequest. The best way to do this is to register it with the WebRequest class. Before that however, we need to create an IWebRequestCreator that can create ProxyWebRequest objects for WebRequest. So here’s our ProxyWebRequestCreator:

    1: /// <summary>
    2:     /// Creates a ProxyWebRequest. Used to register the ProxyWebRequest
    3:     /// prefix (phttp and phttps)
    4:     /// </summary>
    5:     public class ProxyWebRequestCreator : IWebRequestCreate
    6:     {
    7:         #region IWebRequestCreate Members
    8:  
    9:         /// <summary>
   10:         /// Create a ProxyWebRequest based on a URL
   11:         /// </summary>
   12:         /// <param name="uri">URL of the request</param>
   13:         /// <returns>A ProxyWebRequest instance</returns>
   14:         public WebRequest Create(Uri uri)
   15:         {
   16:             return new ProxyWebRequest(uri);
   17:         }
   18:  
   19:         #endregion
   20:     }

Pretty simple that bit of code. Just creates a new ProxyWebRequest and returns it. Now we’re going to add a static method to our ProxyWebRequest class to register our ProxyWebRequestCreator with a prefix. Here’s that code:

    1: /// <summary>
    2:         /// Register the prefixes (phttp and phttps) used to create 
    3:         /// a ProxyWebRequest
    4:         /// </summary>
    5:         public static void RegisterHttpProxyPrefix()
    6:         {
    7:             WebRequest.RegisterPrefix("phttps", new ProxyWebRequestCreator());
    8:             WebRequest.RegisterPrefix("phttp", new ProxyWebRequestCreator());
    9:         }

 

 

Right! Now that all pieces are in place, what exactly does one need to be able to use this code? Simple. Here’s how you would instantiate a ProxyWebRequest:

    1: var request = WebRequest.Create("phttps://search.twitter.com/search.atom?q=" + HttpUtility.UrlEncode(query) + "&rpp=10");

Notice the extra p at the front of the address? That’s what ensures we get a ProxyWebRequest instead of a regular HttpWebRequest. Before we use this code however, we must register the phttp prefix with WebRequest. So we would need to do this at some point in our code:

    1: ProxyWebRequest.RegisterHttpProxyPrefix();

 

 

And what do you do when you want to switch back to the regular HttpWebRequest? Simple again. You just change your WebRequest.Create to this:

    1: var request = WebRequest.Create("https://search.twitter.com/search.atom?q=" + HttpUtility.UrlEncode(query) + "&rpp=10");

 

That’s it! Keep in mind that for the proxy service to work, you will also need a ServiceReferences.ClientConfig in your client project (the sample has an example of this) which registers the proxy service’s endpoint configuration. Here’s the code for this post and a quick sample of how to use it:

Have fun!