Silverlight 2 Beta 2: Doing Data Part VI: A Generic Request WebService

Recently I wanted to try to get a user timeline using the Twitter API, which method requires Basic authentication. To my surprise, the Authorization header -- and many others -- are classified as "Restricted" for Silverlight's HttpWebRequest.

Procrastination isn't the problem, it's the solution. So procrastinate now, don't put it off.   - - Ellen DeGeneres

 

Normally, you would add an "Authorization" header with the value "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" where the gobbledegook after the word Basic is "username:password", converted to a Base64 string.  That's standard W3C Header protocol. With this header present on your Request, you would bypass the Login dialog and the request would complete normally.

Unfortunately, even though there is sample code illustrating this from Karen Corby here, it does not work (she does not actually add an Authorization header, just illustrates how to add a header). See here for a complete listing of restricted headers. So, if you have been trying to do this, or add any similar header that is on the restricted list, you'll only get strange exceptions that make no sense at all, and  you can give up and look for a workaround.

I'm still not 100% sure of the logic behind having an often-used request header being "restricted", but I strongly suspect it is because in it's effort to be a true cross-browser plug-in, Silverlight must use the NPAPI plug-in architecture which enforces various limitations including being able to only make asynchronous HTTP requests. Don't blame Microsoft for that - you'd be shooting the messenger. If you want to be browser- neutral you need to implement NPAPI - at least until submissions like Microsoft's advanced cross-domain XHR proposals are accepted by W3C.

Well, how about if we had a "generic" ASMX webservice that could make any kind of WebRequest call we wanted using the full Framework (without the "restrictions"), and return the appropriate results to our Silverlight apps?  We could give it the target url (including querystring), the method ("GET" or "POST"), username and password if authentication is required, the names and values of any request headers we want, and the names and values of any form POST values we needed.  I think this approach has a lot of useability since we would only need one webservice -- which could potentially accomodate any number and type of Silverlight apps. Basically the service is saying, "I'll do whatever you want and I'll send it to wherever you want, just give me all the info I need, and I'll send back your results."

The service I present here is not 100% feature-complete in this regard, but it goes a long way toward this goal. You can send a GET or POST request, you can add as many Request Headers as you want, you can supply a username:password pair for authentication, and you can supply arrays of formfield names and values for a POST. In order to keep things "generic" it always returns a string -- but that's not a big problem since you can Parse this directly into an XElement and LINQ your way to nirvana for databinding.  Besides, webservices are always returning strings over the wire. With that in mind, here's the WebService class and WebMethod to illustrate the idea:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Services;

namespace GenericRequest
{
    /// <summary>
    /// Generic Request Service for Silverlight Apps
    /// </summary>
    [WebService(Namespace = "http://genericrequest.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    public class Service1 : System.Web.Services.WebService
    {
        /// <summary>
        /// Makes an HttpRequest.
        /// </summary>
        /// <param name="targetUrl">The target URL.</param>
        /// <param name="headerNames">The header names.</param>
        /// <param name="headerValues">The header values.</param>
        /// <param name="method">The method.</param>
        /// <param name="postValueNames">The post value names.</param>
        /// <param name="postValues">The post values.</param>
        /// <param name="userName">username.</param>
        /// <param name="password">password.</param>
        /// <returns>string (xml, html, etc.)</returns>
        [WebMethod]
        public string MakeRequest(string targetUrl, string[] headerNames,string[] headerValues,
            string method, string[] postValueNames, string[] postValues, string userName, string password)
        {
            string returnString = "Bad Request";
            // This is a GET request with no form values and optional Basic auth
            if(method.ToUpper()=="GET" ) 
            {
                using(WebClient wc = new WebClient() )
                {
                    wc.Proxy = null;
                    if(userName!=null && password!=null)
                    wc.Credentials = new NetworkCredential(userName, password);
                    if(headerNames!=null && headerNames.Length >0)
                    {
                        for(int i=0;i<headerNames.Length ;i++)
                        {
                            wc.Headers.Add(headerNames[i],headerValues[i]);
                        }
                    }
                 returnString=    wc.DownloadString(targetUrl);
                }
            }
            // This is a POST request with form values and optional Basic auth
            if(method.ToUpper()=="POST" && postValueNames !=null) 
            {
                using (WebClient wc = new WebClient())
                {
                    wc.Proxy = null;
                    if(userName!=null && password !=null)
                        wc.Credentials = new NetworkCredential(userName, password);
                    if (headerNames != null && headerValues !=null)
                    {
                        for (int i = 0; i < headerNames.Length; i++)
                        {
                            wc.Headers.Add(headerNames[i], headerValues[i]);
                        }
                    }
                    var data = new NameValueCollection();
                    for(int i =0;i<postValueNames.Length ;i++)
                    {
                      data.Add(postValueNames[i],postValues[i]);
                    }
                    byte[] result = wc.UploadValues(targetUrl, "POST", data);
                    returnString = System.Text.Encoding.UTF8.GetString(result);
                }
            }
            return returnString;
        }
    }
}
To use this, the following code will provide an example:
private void button1_Click(object sender, RoutedEventArgs e)
        {
            ServiceReference1.Service1SoapClient client = new Service1SoapClient();

            // this is for the authenticated friends timeline and requires http Basic auth. 
            string targetUrl = "http://twitter.com/statuses/friends_timeline.xml";
             
            client.MakeRequestCompleted += new EventHandler(client_MakeRequestCompleted);
          
           ArrayOfString headerNames = new ArrayOfString();
           ArrayOfString headerValues = new ArrayOfString();
            // add one header:
           headerNames.Add("Cache-Control");
           headerValues.Add("no-cache");
            ArrayOfString formValueNames = new ArrayOfString();
            ArrayOfString formValues = new ArrayOfString();
            // this is the generic service "GET" request with Basic auth, a header, and no form fields
            client.MakeRequestAsync(targetUrl, headerNames, headerValues, "GET", formValueNames, formValues,"username","password");
        }

        void client_MakeRequestCompleted(object sender, MakeRequestCompletedEventArgs e)
        {
            string s = e.Result;
            //  very simple binding just to show what we got back
           XElement elem= XElement.Parse(s);
            var query = from f in elem.Elements("status").Descendants() select f.Value;
            Grid1.ItemsSource = query;
        }
I don't have a method there for Forms Authentication yet , but it would not be very difficult. Sample code would look something like this:
string loginUri = "http://www.webshots.com/login";
string username = "username";
string password = "password";
string reqString = "username=" + username + "&password=" + password;
byte[] requestData = Encoding.UTF8.GetBytes (reqString);

CookieContainer cc = new CookieContainer(  );
var request = (HttpWebRequest)WebRequest.Create (loginUri);
request.Proxy = null;
request.CookieContainer = cc;
request.Method = "POST";

request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = requestData.Length;
using (Stream s = request.GetRequestStream(  ))
  s.Write (requestData, 0, requestData.Length);

using (var response = (HttpWebResponse) request.GetResponse(  ))
  foreach (Cookie c in response.Cookies)
    Console.WriteLine (c.Name + " = " + c.Value);

// We're now logged in. As long as you store and assign cc to subsequent WebRequest
// objects, you can do such things as download photos.

You can download the complete Silverlight solution including a Test Silverlight app here. Of course, you'll need to supply your own Twitter username and password to try it.
By Peter Bromberg   Popularity  (3190 Views)