[added section on cookies Jeremy Shaw **20101024195144 Ignore-this: dba04a9bea3d48a1dc0d3430426533dc ] addfile ./CookieCounter.lhs hunk ./CookieCounter.lhs 1 + + +

Simple Cookie Demo

+ +

The cookie interface is pretty small. There are two parts to the interface: setting a cookie and looking up a cookie.

+ +

To create a Cookie value, we use the mkCookie function:

+ +
+#ifdef HsColour +> -- | create a 'Cookie' +> mkCookie :: String -- ^ cookie name +> -> String -- ^ cookie value +> -> Cookie +> +#endif +
+ +

Then we use the addCookie function to send the cookie to the user. This adds the Set-Cookie header to the Response. So the cookie will not actually be set until the Response is sent.

+ +
+#ifdef HsColour +> -- | add the 'Cookie' to the current 'Response' +> addCookie :: (MonadIO m, FilterMonad Response m) => CookieLife -> Cookie -> m () +> +#endif +
+ +

The first argument of addCookie specifies how long the browser should keep the cookie around. See the cookie lifetime section for more information on CookieLife.

+ +

To lookup a cookie, we use some HasRqData functions. There are only three cookie related functions:

+ +
+#ifdef HsColour +> -- | lookup a 'Cookie' +> lookCookie :: (Monad m, HasRqData m) => +> String -- ^ cookie name +> -> m Cookie +> +> -- | lookup a 'Cookie' and return its value +> lookCookieValue :: (Functor m, Monad m, HasRqData m) => +> String -- ^ cookie name +> -> m String +> +> -- | look up a 'Cookie' value and try to convert it using 'read' +> readCookieValue :: (Functor m, Monad m, HasRqData m, Read a) => +> String -- ^ cookie name +> -> m a +#endif +
+ +

The cookie functions work just like the other HasRqData functions. That means you can use checkRq, etc.

+ +

The following example puts all the pieces together. It uses the cookie to store a simple counter specifying how many requests have been made:

+ +
+ +> module Main where +> import Control.Monad +> import Control.Monad.Trans +> import Happstack.Server +> import Control.Monad ( msum ) +> import Happstack.Server ( CookieLife(Session), ServerPart, addCookie +> , look, mkCookie, nullConf, ok, readCookieValue +> , simpleHTTP ) +> +> homePage :: ServerPart String +> homePage = +> msum [ do rq <- askRq +> liftIO $ print (rqPaths rq) +> mzero +> , do requests <- readCookieValue "requests" +> addCookie Session (mkCookie "requests" (show (requests + 1))) +> ok $ "You have made " ++ show requests ++ " requests to this site." +> , do addCookie Session (mkCookie "requests" (show 2)) +> ok $ "This is your first request to this site." +> ] +> +> main :: IO () +> main = simpleHTTP nullConf $ homePage + +
+

[Source code for the app is here.]

+ +

Now if you visit http://localhost:8000/ you will get a message like:

+ +
+
+This is your first request to this site.
+
+
+ +

If you hit reload you will get:

+ +
+
+You have made 3 requests to this site.
+
+
+ +

Now wait a second! How did we go from 1 to 3, what happened to 2? The browser will send the cookie with every request it makes to the server. In this example, we ignore the request path and send a standard response to every request that is made. The browser first requests the page, but it also requests the favicon.ico for the site. So, we are really getting two requests everytime we load the page. Hence the counting by twos. It is important to note that the browser does not just send the cookie when it is expecting an html page -- it will send it when it is expecting a jpeg, a css file, a js, or anything else.

+ +

There is also a race-condition bug in this example. See the cookie issues section for more information.

+ addfile ./CookieFeatures.lhs hunk ./CookieFeatures.lhs 1 + + +

Other Cookie Features

+ +

The mkCookie function uses some default values for the Cookie. The Cookie type itself includes extra parameters you might want to control such as the cookie path, the secure cookie option, etc. addfile ./CookieIssues.lhs hunk ./CookieIssues.lhs 1 + + +

Cookie Issues

+ +

Despite their apparently simplicity, Cookies are the source of many bugs and security issues in web applications. Here are just a few of the things you need to keep in mind.

+ +

Security issues

+ +

To get an understanding of cookie security issues you should search for cookie security issues and cookie XSS

+ +

One important thing to remember is that the user can modify the cookie. So it would be a bad idea to do, addCookie Session (mkCookie "userId" "1234") because the user could modify the cookie and change the userId at will to access other people's accounts.

+ +

Also, if you are not using https the cookie will be sent unencrypted.

+ +

Delayed Effect

+ +

When you call addCookie the Cookie will not be available until after that Response has been sent and a new Request has been received. So the following code will not work:

+ +
+#ifdef HsColour +> do addCookie Session (mkCookie "newCookie" "newCookieValue") +> v <- look "newCookie" +> ... +> +#endif +
+ +

The first time it runs, look will fail because the cookie was not set in the current Request. Subsequent times look will return the old cookie value, not the new value.

+ +

Cookie Size

+ +

Browsers impose limits on how many cookies each site can issue, and how big those cookies can be. The RFC recommends browsers accept a minimum of 20 cookies per site, and that cookies can be at least 4096 bytes in size. But, implementations may vary. Additionally, the cookies will be sent with every request to the domain. If your page has dozens of images, the cookies will be sent with every request. That can add a lot of overhead and slow down site loading times.

+ +

A common alternative is to store a small session id in the cookie, and store the remaining information on the server, indexed by the session id. Though that brings about its own set of issues.

+ +

One way to avoid having cookies sent with every image request is to host the images on a different sub-domain. You might issues the cookies to www.example.org, but host images from images.example.org. Note that you do not actually have to run two servers in order to do that. Both domains can point to the same IP address and be handled by the same application. The app itself may not even distinguish if the requests were sent to images or www.

+ +

Server Clock Time

+ +

In order to calculate the expires date from the max-age or the max-age from the expires date, the server uses getCurrentTime. This means your system clock should be reasonably accurate. If your server is not synchronized using NTP or something similar it should be.

+ +

Cookie Updates are Not Atomic

+ +

Cookie updates are not performed in any sort of atomic manner. As a result, the simple cookie demo contains a race condition. We get the Cookie value that was including in the Request and use it to create an updated Cookie value in the Response. But remember that the server can be processing many requests in parallel and the browser can make multiple requests in parallel. If the browser, for example, requested 10 images at once, they would all have the same initial cookie value. So, even though they all updated the counter by 1, they all started from the same value and ended with the same value. The count could even go backwards depending on the order Requests are received and Responses are processed.

+ + addfile ./CookieLife.lhs hunk ./CookieLife.lhs 1 + + +

Cookie Lifetime

+ +

When you set a cookie, you also specify the lifetime of that cookie. Cookies are referred to as session cookies or permanent cookies depending on how their lifetime is set.

+ +
+
session cookie
+
A cookie which expires when the browser is closed.
+
permanent cookie
+
A cookie which is saved (to disk) and is available even if the browser is restarted. The expiration time is set by the server.
+
+ +

The lifetime of a Cookie is specified using the CookieLife type: + +

+#ifdef HsColour +> -- | the lifetime of the cookie +> data CookieLife +> = Session -- ^ expire when the browser is closed +> | MaxAge Seconds -- ^ expire after the specified number of seconds +> | Expires UTCTime -- ^ expire at a specific date and time +> | Expired -- ^ expire immediately +#endif +
+ +

If you are intimately familiar with cookies, you may know that cookies have both an expires directive and a max-age directive, and wonder how they related to the constructors in CookieLife. Internet Explorer only supports the obsolete expires directive, instead of newer max-age directive. Most other browser will honor the max-age directive over expires if both are present. To make everyone happy, we always set both.

+ +

So, when setting CookieLife you can use MaxAge or Expires -- which ever is easiest, and the other directive will be calculated automatically.

+ +

Deleting a Cookie

+ +

There is no explicit Response header to delete a cookie you have already sent to the client. But, you can convince the client to delete a cookie by sending a new version of the cookie with an expiration date that as already come and gone. You can do that by using the Expired constructor. Or, you can use the more convenient, expireCookie function. + +

+#ifdef HsColour +> -- | Expire the cookie immediately and set the cookie value to "" +> expireCookie :: (MonadIO m, FilterMonad Response m) => +> String -- ^ cookie name +> -> m () +#endif +
addfile ./Cookies.lhs hunk ./Cookies.lhs 1 + + +

Working with Cookies

+ + +

What are Cookies?

+

HTTP is a stateless protocol. Each incoming Request is processed +with out any memory of any previous communication with the +client. Though, from using the web, you know that it certainly doesn't +feel that way. A website can remember that you logged in, items in +your shopping cart, etc. That functionality is implemented by using +Cookies.

+ +

When the server sends a Response to the client, it can include a special Response header named Set-Cookie, which tells the client to remember a certain Cookie. A Cookie has a name, a string value, and some extra control data, such as a lifetime for the cookie.

+ +

The next time the client talks to the server, it will include a copy of the Cookie value in its Request headers. One possible use of cookies is to store a session id. When the client submits the cookie, the server can use the session id to look up information about the client and remember who they are. Sessions and session ids are not built-in to the HTTP specification. They are merely a common idiom which is provided by many web frameworks.

+ +#include "CookieCounter.lhs" +#include "CookieLife.lhs" +#include "CookieIssues.lhs" +#include "CookieFeatures.lhs" hunk ./Makefile 8 -RQDATA_DEPS := RqDataLimiting.lhs RqDataParsing.lhs -RQDATA_DEMOS := HelloRqData.lhs RqDataPost.lhs RqDataError.lhs RqDataUpload.lhs RqDataRead.lhs RqDataFromData.lhs RqDataCheck.lhs RqDataCheckOther.lhs RqDataOptional.lhs -TEMPLATES_DEPS := +RQDATA_DEPS := RqDataLimiting.lhs RqDataParsing.lhs Cookies.lhs +RQDATA_DEMOS := HelloRqData.lhs RqDataPost.lhs RqDataError.lhs RqDataUpload.lhs RqDataRead.lhs RqDataCheck.lhs RqDataCheckOther.lhs RqDataOptional.lhs +COOKIE_DEPS := CookieLife.lhs CookieIssues.lhs CookieFeatures.lhs +COOKIE_DEMOS := CookieCounter.lhs +TEMPLATES_DEPS := hunk ./Makefile 14 -DEMOS := $(RQDATA_DEMOS) $(ROUTE_FILTER_DEMOS) $(HELLOWORLD_DEMOS) $(TEMPLATES_DEPS) $(TEMPLATES_DEMOS) +DEMOS := $(RQDATA_DEMOS) $(ROUTE_FILTER_DEMOS) $(HELLOWORLD_DEMOS) $(TEMPLATES_DEPS) $(TEMPLATES_DEMOS) $(COOKIE_DEMOS) hunk ./Makefile 23 -RqData.html: $(RQDATA_DEPS) $(RQDATA_DEMOS) +RqData.html: $(RQDATA_DEPS) $(RQDATA_DEMOS) $(COOKIE_DEPS) $(COOKIE_DEMOS) hunk ./Makefile 82 +# extract a demo hunk ./Makefile 96 -.PHONY: test all clean +%.demo : $(DESTDIR)%.hs + ghc --make -c -o$@ $< + +check-demos: $(subst .lhs,.demo,$(DEMOS)) + +.PHONY: test all clean check-demos hunk ./RqData.lhs 29 +#include "Cookies.lhs" hunk ./theme.css 38 +h4 +{ + font-weight: bold; + } +