Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax Patterns And Best Practices (2006)

.pdf
Скачиваний:
39
Добавлен:
17.08.2013
Размер:
15.94 Mб
Скачать

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

129

public interface IURLRewriter {

bool IsResource(HttpRequest request);

void WriteRedirection( HttpRequest request);

}

public interface IRewriter {

bool WriteRedirection(string mimetype);

}

The interface IURLRewriter is the URL rewriter component and has two methods: IsResource and WriteRedirection. The method IsResource is used to recognize a resource URL, such as http://mydomain.com/account/joesmith. The method WriteRedirection is used to rewrite the URL. Even though it is not obvious, the IRewriter component is wired to the internals of the

IURLRewriter component.

The interface IRewriter also has a single method, WriteRedirection, that is used to rewrite the URL. The interface IRewriter is responsible for converting the resource into an appropriate representation. Relating the interface IRewriter to Figure 5-9, it would mean converting the URL /bankaccount/login to the URL /bankaccount/login.jsp. The way that the URL is rewritten is not based on the URL itself but on the MIME type. However, an IRewriter and IURLRewriter implementation could be coded to rewrite the URL based on multiple HTTP headers. When rewriting a URL by using multiple HTTP headers, a priority ordering is used (for example, Accept before Accept-Language). Getting back to the IRewriter interface, if the URL is rewritten, then the WriteRedirection method returns true; otherwise, a false is returned.

As will shortly be illustrated, the URLRewriterASPNet class implements the IURLRewriter interface, and DefaultRewriter implements the IRewriter interface. Based on the implementation types, the interface instances of IRewriter and IURLRewriter are wired together in the OnBeginRequest filter phase of an ASP.NET application. The following source code illustrates the implementation of OnBeginRequest in the global.asax file:

void Application_OnBeginRequest(Object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender;

IRewriter rewriter = new DefaultRewriter( app); IURLRewriter router = new URLRewriterASPNet( rewriter); if (router.IsResource(app.Request)) {

router.WriteRedirection(app.Request);

}

}

The OnBeginRequest function is named and defined based on the requirements of ASP.NET. When implementing any of the filter phases in ASP.NET, they are propagated as .NET events; therefore, the signature of the method is fixed to the first parameter being an Object instance, and the second parameter being an EventArgs instance. Sender is an instance of HttpApplication, which represents the ASP.NET application. Looking closer at the instantiation of URLRewriterASPNET, you can see that it is wired to an IRewriter instance by using the URLRewriterASPNet constructor. After the instantiations, the first if statement uses the method IsResource to test whether the HTTP request is a resource. If the HTTP request is a resource, the method WriteRedirection is called to rewrite the URL.

130

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

Implementing the Details of URL Rewriting

Looking a bit closer at the wiring of the IURLRewriter and IRewriter interface instances, you can see that some details were not explained in the OnBeginRequest function. These details indicate the logic of how to convert a resource into a representation and are described as follows:

1.Verify that the URL is referring to a resource.

2.If the URL is a resource, process the URL. Otherwise, ignore the URL and let the HTTP server process the request.

3.Read the Accept HTTP headers from the request and store them in an array.

4.Sort the array so that the highest-priority Accept header is at the beginning of the list.

5.Iterate the array and attempt to rewrite the URL for each item.

6.If during the looping the URL could be rewritten, exit the loop and let the other filters continue their processing.

The class RouterASPNet is responsible for steps 1, 2, 3, and 5. Step 4 is delegated to another yet-to-be-described class, and step 6 is implemented by DefaultRewriter.

The implementation of URLRewriterASPNet is defined as follows:

class URLRewriterASPNet : IURLRewriter { IRewriter _defaultRewriter;

public URLRewriterASPNet( IRewriter rewriter) { if (_defaultRewriter == null) {

throw new Exception( "Rewriter cannot be null");

}

_defaultRewriter = rewriter;

}

public bool IsResource(HttpRequest request) { FileAttributes attributes;

try {

attributes = File.GetAttributes(request.PhysicalPath);

}

catch (FileNotFoundException ex) { return false;

}

if ((attributes & FileAttributes.Directory) != 0) { return true;

}

else {

return false;

}

}

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

131

public void WriteRedirection(HttpRequest request) {

string[] elements = (string[])request.AcceptTypes.Clone(); Array.Sort( elements, new CompareMimeTypes());

Regex semiColon = new Regex(";"); foreach (string type in elements) {

String[] buffers = semiColon.Split(type);

if (_defaultRewriter.WriteRedirection(buffers[0])) { break;

}

}

}

}

When implementing step 1 in the method IsResource, the challenge is to figure out whether the URL is a resource or a file reference. A file reference, simply put, would have an extension in the URL indicating the referencing of a specific file type. The decision chosen by the URLRewriterASPNet implementation is to test whether the absolute path of the URL refers to a directory. If a directory is referenced, the URL is a resource; otherwise, the URL is something else. In other IURLRewriter implementations, other logic might be used. Maybe a regular expression is used to validate the URL to see whether a reference to a file exists. Whatever logic is used, a true is returned to indicate a URL resource, and false is used to indicate the URL is something else. If there are multiple IURLRewriter implementations, they are wired together and called by using the Chain of Responsibility pattern.

If the URL needs to be rewritten as per step 2, the method WriteRedirection is called. In the implementation of WriteRedirection, which executes steps 3 and 4, the Accept headers are sorted from highest priority to lowest priority. The sorting is carried out by cloning the Accept headers (request.AcceptTypes) and then calling the method Array.Sort. The default algorithm used by Array.Sort will not work, and therefore the class CompareMimeTypes is used. I will explain that class in a moment. After the Accept identifiers have been sorted, they are iterated, and for each one the method defaultRewriter.WriteRedirection is called. As each identifier is called from highest to lowest priority, the IRewriter implementation tests to see whether the URL can be rewritten. If the test returns a true value, an identifier is found and the URL is rewritten. If the URL has been rewritten, defaultRewriter.WriteRedirection returns true and all processing stops.

The sorting of the individual Accept identifiers will now be discussed. When using a custom sorting routine with Array.Sort, the custom sorting routine would have to implement the IComparer interface. The IComparer interface has a single method that compares two values from the list to be sorted. The single method implementation returns a positive, negative, or zero integer value indicating which value is greater than the other. Following is the implementation of CompareMimeTypes:

class CompareMimeTypes : IComparer { Regex _wildcard = new Regex(@"/\*"); Regex _semiColon = new Regex(";");

public void CalculateValue(string val, out int level, out double qvalue) { String[] buffers = _semiColon.Split(val);

double multiplier = 1.0;

132 C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

if (buffers.Length > 1) {

multiplier = double.Parse(buffers[1].Substring(2));

}

qvalue = multiplier; level = 0;

if (String.Compare(buffers[0], "*/*") == 0) { level = 1;

}

else if (_wildcard.IsMatch(val)) { level = 2;

}

else if (String.Compare(buffers[0], "application/xhtml+xml") == 0) { level = 4;

}

else {

level = 3;

}

}

public int Compare(object x, object y) { int levelx = 0, levely = 0;

double qvaluex = 0.0, qvaluey = 0.0; CalculateValue((string)x, out levelx, out qvaluex); CalculateValue((string)y, out levely, out qvaluey); if (levelx < levely) {

return 1;

}

else if (levelx > levely) { return -1;

}

else {

if (qvaluex < qvaluey) { return 1;

}

else if (qvaluex > qvaluey) { return -1;

}

else {

return 0;

}

}

}

}

CompareMimeTypes has two methods: CalculateValue and Compare. The Compare method is required by the IComparer interface and compares two Accept header identifiers. CalculateValue converts the Accept header identifier into a value that can be used for comparison purposes. The calculation of the greater-than value of an individual item is based on the MIME-type specification and its q value. The method CalculateValue has three parameters. The first

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

133

parameter is the MIME type to test. The second and third parameters are numeric values returned to the caller that indicate the priority of the MIME type. The priority calculation is based on levels and its associated q values. The levels result from the priority precedence of text/xml to text/*. The q values are associated with the Accept identifier.

In the implementation of the method Compare, there are two parameters: x and y. The method implementation has to figure out which value is greater than the other. To get a priority level, the method CalculateValue is called for each parameter. Then the levels (levelx and levely) are compared. If one of the levels is higher, the appropriate integer value is returned. If the levels are equal, the q values (qvaluex and qvaluey) are tested and the appropriate integer value is returned.

After the MIME types have been sorted, URLRewriterASPNet will call the rewriter DefaultRewriter to generate the return content, which is step 5. Following is the implementation of DefaultRewriter:

public class DefaultRewriter : IRewriter { protected HttpApplication _app; private Regex _xml = new Regex("xml");

private Regex _html = new Regex("html"); private Regex _text = new Regex("plain");

public DefaultRewriter(HttpApplication app) { _app = app;

}

private bool DoesFileExistAndRewrite(string filename) { string path = _app.Request.PhysicalPath + filename; FileAttributes attributes;

try {

attributes = File.GetAttributes(path);

}

catch (FileNotFoundException ex) { return false;

}

if ((attributes & FileAttributes.Directory) == 0) { _app.Context.RewritePath(filename);

return true;

}

else {

return false;

}

}

public virtual bool WriteRedirection(string mimetype) { if (_xml.IsMatch(mimetype)) {

return DoesFileExistAndRewrite("default.xhtml");

}

if (_html.IsMatch(mimetype)) {

return DoesFileExistAndRewrite("default.html");

}

134 C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

if (_text.IsMatch(mimetype)) {

return DoesFileExistAndRewrite("default.txt");

}

if (String.Compare(mimetype, "*/*") == 0) {

return DoesFileExistAndRewrite("content.html");

}

return false;

}

}

The implementation of the method WriteRedirection will iterate a series of if statements to test which MIME type has been passed in. If any one particular MIME type matches the type, the method DoesFileExistAndRewrite is called. The method DoesFileExistAndRewrite will test whether the proposed rewritten URL references a file that exists, and if so the URL is rewritten. The big idea of the operation for the URL rewriter is to generate a potential URL and test whether there is a file available on the storage medium. If the file exists, the URL can be rewritten; otherwise, another MIME type is tested for availability. If a representation exists, WriteRedirection will return true and consider the URL rewritten, which causes an exit, thus implementing the last step, step 6.

The defined DefaultRewriter will work for static content, but not for dynamic content such as PHP, JSP, or even ASP.NET because the redirections always reference static extensions such as XML, HTML, and XHTML. Suppose PHP is used for generating XML and HTML content. If a request (for example, /content) requires generation of XML content, the generated filename will end with .php (for example, /content.php). Yet if the request requires dynamic generation of HTML, the generated filename will end with .php again (for example, /content.php). One solution would be to append the dynamic script extension and type (for example, HTML would be /content.html.php). The appending of two extensions is used by Apache when sending language-specific content.

Generating the Content

When the rewriting component executes its code, the rewriting of the URL /bankaccount/login to the URL /bankaccount/login.jsp occurs transparently. The next step is to test whether the URL is indeed rewritten, so let’s watch Firefox’s decision process as it calls the URL. Figure 5-10 illustrates how the browser loads the appropriate document, and that the server tests what the appropriate document would be.

The bottom window of Figure 5-10 is a Secure Shell (SSH) console window of the Mono XSP ASP.NET server running on Linux. The asterisks represent the beginning and ending of an HTTP request. The first line, which starts with Path, is the absolute path of the ASP.NET application’s physical path appended with the URL. The next line, which reads Is routable, indicates that a resource has been requested. Then the HTTP Accept header sent by Mozilla is reorganized and then tested to see whether the content can be downloaded. Notice how the various MIME types are iterated and tested. The last MIME type tested is text/html, because the path associated with the MIME type exists. There are other MIME types, but they are not iterated because a MIME type has been found.

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

135

Figure 5-10. Illustration of URL rewriting in action

Using the Accept Header on the Client Side

By default, all browsers send the content that they are interested in. It is possible when using the XMLHttpRequest type to specify the Accept HTTP header, as illustrated by the following source code example:

var xmlhttp = FactoryXMLHttpRequest(); xmlhttp.open( "GET", "/url", true);

xmlhttp.setRequestHeader( "Accept", "application/xml");

The method setRequestHeader is used to specify the Accept HTTP header. Based on the HTTP header, the server can generate the proper source code and send it to the client.

An Example Shopping Cart Application

Back in Figure 5-9, you saw the second URL, /bankaccount/maryjane, which is rewritten, performs an authorization, and references the bank account resource of maryjane. This sort of scenario

136

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

is commonplace; a simple example is a shopping cart. A shopping cart is a resource, requires the identification of some user, and uses URL rewriting. Additionally, the shopping cart adds the complexity of performing a URL redirection to an unknown resource.

Imagine that I will be buying something at Amazon.com. The Amazon shopping cart will contain what I want to buy. What is unknown is the shopping cart that Amazon uses to reference the items that I want to buy. Shopping carts can be associated with and authorized by only a single person. From a logic perspective, while shopping at Amazon, I do not want somebody to add or remove items from my shopping cart. Additionally, I do not want somebody to be able to create a shopping cart in my name and ship it to another address. So in the end, even though it is not obvious, a shopping cart is a very personal resource.

If the user is authenticated, the shopping cart is associated with the authenticated user. If the user is not authenticated, the shopping cart is associated with the client using a cookie. In either case, a cookie could be used to authorize who is allowed to manipulate the shopping cart. The URL for the shopping cart would be /shoppingcart/12324, but the shopping cart can be accessed only by the authenticated user or cookie of the anonymous user. What is never done is the association of the URL /shoppingcart with a specific authenticated user or cookie.

Defining the User Identification Interfaces

Authenticating a user is the process of creating a user identifier, and there are multiple ways to create a user identifier. This means that when implementing HTTP authentication, some thought should be given to keeping everything neutral so that other user identification implementations could be switched at runtime without affecting how authentication is managed. The solution is to use the Bridge and Factory patterns to define an intention of identifying the user and then define the implementations that technically identify the user.

The following source code defines the interfaces for the intention of identifying a user:

public interface IUserIdentificationResolver<WebReference> { IUserIdentification Resolve(WebReference reference);

}

public interface IUserIdentificationFactory { IUserIdentification Create( string identifier); IUserIdentification Create();

}

public interface IUserIdentification { string Identifier { get; }

bool IsIdentified { get; }

}

The interface IUserIdentificationResolver<> is defined by using .NET Generics and has a single method, Resolve. .NET Generics are used to define the interface, allowing the interface to be used in multiple user identification implementation contexts. When using Generics, the interface is saying, “Given the WebReference type, I will resolve what the user identification mechanism is.”

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

137

The interface IUserIdentification is returned by the method

IUserIdentificationResolver<>.Resource and has two properties, Identifier and

IsIdentified. The Identifier property is used to identify the user, and IsIdentified to indicate whether a user has been identified. In the definition, the interface IUserIdentification has only two properties, but depending on your particular context could have more properties or methods. The purpose of the interface is to provide enough information to uniquely identify who is making the called request and to allow the application to use that information for managing the authorization of a resource.

The interface IUserIdentificationFactory is used by IUserIdentificationResolve<> to instantiate an IUserIdentification instance whenever a user identity has been found.

The interfaces make up an important basis of user identification and should be used regardless of the user identification scheme used.

Using HTTP Authentication

The first user identification implementation is HTTP authentication. Using HTTP authentication is probably one of the most underused techniques of creating a user identifier. Most web applications tend to prefer HTTP cookies, but HTTP authentication offers some yet-to-be- discussed options that HTTP cookies do not.

In the early nineties, HTTP authentication was not well known and considered generally insecure because the client would constantly be sending the username and password to the server whenever an authorization was performed. To get around the security issue, a more secure form of HTTP authentication was created, called HTTP digest authentication. HTTP digest authentication in the early Web days was not widely distributed. Of course today that is not the case as every browser, or at least most browsers, support HTTP digest authentication.

Understanding How HTTP Authentication Functions at a Practical Level

HTTP authentication is a very good way of creating a user identifier because the authentication mechanism is formal and requires participation by the user. If the user declines, authentication will not occur, and no information is sent to the server. The user can remain anonymous. Granted, the user might not be able to access all of the content, but there is anonymity and some people treasure their anonymity. Figure 5-11 illustrates how HTTP authentication is presented to the user via current browsers.

Also illustrated in Figure 5-11 is the ability of current browsers to remember past HTTP authentication sessions. HTTP authentication is both a blessing and curse in that users must authenticate themselves whenever they exit and restart the browser. The blessing is that authentication information is not sent automatically, and the curse is that the user must authenticate themselves before starting a session at a website. Some may consider requiring authentication a downside, but when security is important, using HTTP authentication ensures giving the correct rights to the identified user.

At a technical level, HTTP authentication is a mechanism whereby a user requests the contents of a resource and the server issues a challenge, asking for identification. The browser converts the challenge into something similar to Figure 5-11. After the user enters the appropriate information, the server will authenticate the user. If the authentication works, the representation of the resource is downloaded by the browser.

138

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

Figure 5-11. HTTP authentication dialog box prefilled with authentication information

A typical HTTP digest authentication conversation is described in the following steps. The process starts with the client requesting a resource:

GET /test/ HTTP/1.1 Host: jupiter:8100

User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.8) Gecko/20050511

Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5

Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate

Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300

Connection: keep-alive

The resource is protected, and therefore the server will challenge for an authentication:

HTTP/1.1 401 Authorization Required

Date: Sat, 27 Aug 2005 14:00:05 GMT

Server: Apache/2.0.53 (Ubuntu) PHP/4.3.10-10ubuntu4 WWW-Authenticate: Digest realm="Private Domain", nonce="0hvlrVH/ AwA=8225d4804076a334d81181695204fee405adaaee",

algorithm=MD5, domain="/test", qop="auth"