Teeps started off the fun last month with this blog post about countless errors in his logs about not being able to set cookies. I get the errors too, and they usually look like this:
[code]07/11 15:58:19 error Cannot create cookie: domain = .notingdetails.com
07/11 16:19:01 error Cannot create cookie: domain = .notingdetails.com
07/11 16:39:12 error Cannot create cookie: domain = .notingdetails.com
07/11 16:45:05 error Cannot create cookie: domain = .notingdetails.com
[/code]
Jochem chimed in with an explanation last week explaining that clients (most likely poorly written bots) were mis-interpreting cookie headers. Consider the following headers which come back from the server to the client:
[code]HTTP/1.1 200 OK
Date: Sat, 12 Jul 2008 01:36:02 GMT
Server: Apache/2.2.4 (Linux/SUSE)
Set-Cookie: cookie_name=cookie_value;domain=.notingdetails.com;path=/admin
Set-Cookie: JSESSIONID=5c30ce51736762297c2f;path=/
Content-Language: en-US
Content-Type: text/html; charset=UTF-8
Content-Length: 10903
[/code]
This header sets two cookies. Once called "cookie_name", and another called "JSESSIONID". The former cookie specifies a domain and a path, the latter just a path. Certain bots don't obey RFC specs and try to send back a cookie called "domain" like my logs above show. As it turns out ColdFusion's error logs get an entry in them for EVERY cookie that is attempted to be set with the one of the following names (the actual list is slightly larger than Jochem's original one):
  • Comment
  • Discard
  • Domain
  • Expires
  • Max-Age
  • Pathv
  • Secure
  • Version
Jochem posed the question, "I wonder if we can get CF to ignore that error without polluting the logfile." Charlie Arehart suggested we could have "someone write a Java Servlet filter that looks for cookies with these names and drops them from the request before it's passed on to CF." Well, I took Charlie up on his idea and created a servlet filter to do just that. There was one problem though-it didn't work. It turns out the error was not coming from ColdFusion at all, but was being logged long before ColdFusion was even invoked by the servlet container. I needed to dig further. JRUN parses the request header and creates all the cookies via Sun's servlet spec before it calls any servlet filters. I wanted to know where and how, so I added the following lines to my JVM startup args:
[code]-Xdebug  -Xrunjdwp:transport=dt_socket,address=28000, server=y, suspend=n
[/code]
This tells the JVM to start in debug mode and listen on port 28000 for a debugger to connect. suspend=n tells the JVM NOT to suspend until the debugger connects. The port number is arbitrary, just make sure it's not already in use by something. Then I placed Eclipse in the Debug perspective, and chose Run > Debug... > Remote Java Application. I entered the address of my server for host and 28000 for the port number and then clicked Debug. This connected to my ColdFusion server's JVM and brought up all the threads in its existance. I created a test page with a large loop so the page would run long enough for me to catch it. Using MS Fiddler I created a request that tried to set several cookies with reserved names such as "expires", "path", and "secure". I then sent the request, tabbed over to SeeFusion to get the thread name handling my request, and tabbed back over to Eclipse and did a Ctrl-F to filter down that thread and pause it. Now that I had my thread trapped, and back out to the JRunRequestDispatcher.invoke() method by using the drop to frame feature of the debugger. (rewinds the stack as if you were going back in time.) I then carefully stepped through JRUN until I saw the errors appear in my logs. This is where they went in:
[code]Cookie.<init>(String,String) LinL 170
JRunCookieUtils.parseCookieString(String, Vector, Logger) line: 180
JRunCookieUtils.getCookies(Enumeration, Logger) line: 152
JRunCookieUtils.getCookies(Enumeration, Logger) line: 152
JRunRequest.getCookies() line: 335
ForwardRequest(HttpServletRequestWrapper).getCookies() line: 108
SessionService.getCookieSessionID(HttpServletRequest) line: 1066
ForwardRequest.getRequestedSessionId() line: 42
ForwardRequest.isRequestedSessionIdValid() line: 467
ForwardRequest.getSession(boolean) line: 332
ForwardRequest.create(HttpServletRequest, HttpServletResponse, WebApplication, String, String, String, String, String, MultiKeyContainer) line: 117
JRunRequestDispatcher.invoke(ServletConnection) line: 257
...[/code]
The log entry happened when JRUN tried to create an instance of the javax.servlet.http.Cookie class. That code isn't part of ColdFusion OR JRUN. That comes from Sun. Luckily, Java is open source now, so I pulled up the source code for the javax.servlet.http.Cookie class. There it was, plain as day in the constructor:
[code] if (!isToken(name)
	|| name.equalsIgnoreCase("Comment") // rfc2019
	|| name.equalsIgnoreCase("Discard") // 2019++
	|| name.equalsIgnoreCase("Domain")
	|| name.equalsIgnoreCase("Expires") // (old cookies)
	|| name.equalsIgnoreCase("Max-Age") // rfc2019
	|| name.equalsIgnoreCase("Path")
	|| name.equalsIgnoreCase("Secure")
	|| name.equalsIgnoreCase("Version")
	|| name.startsWith("$")
        )
	{
		String  errMsg = lStrings.getString("err.cookie_name_is_token");
		Object [] errArgs = new Object [1];
		errArgs[0] = name;
		errMsg = MessageFormat.format(errMsg, errArgs);
		throw new IllegalArgumentException (errMsg);
	}
[/code]
The Cookie constructor will throw an IllegalArgumentException if any of those conditions are met. $ is used for special information like $version. The isToken method looks like this:
[code] private static final String  tspecials = ",; ";

private boolean isToken(String  value) 
	{
		int len = value.length();
	
		for (int i = 0; i &lt; len; i++) 
			{
				char c = value.charAt(i);
			
				if (c &lt; 0x20 || c &gt;= 0x7f || tspecials.indexOf(c) != -1)
				return false;
			}
		return true;
	} 
[/code]
That basically means any of the letters in a token can't be an ASCII control character (like line feed or DEL) and any of the characters can't be a comma or semi-colon. Interestingly enough, this does NOT follow RFC 2068 (which disallows the following: ()<>@,;:\\\"/[]?={} \t) but there is a note in there about "full Netscape compatibility". So, unless Sun changes their code, the only thing JRUN can do is not log errors thrown when creating instances of the Cookie class. Here's the thing though, as Jochem pointed out, I'm not convinced that Sun's code is correct. They cite RFC 2019 in their code, but I spent several hours reading through the RFC today (freaking dry as a dessert) and I don't get it. Here are some highlights from the RFC I pulled out:
  • "[Cookie] NAMEs that begin with $ are reserved for other uses and must not be used by applications."
  • In section 4.1 it states that attribute values pairs where the attributes must be a token confirming to RFC 2068.
  • RFC 2068 (http://www.faqs.org/rfcs/rfc2068.html) says a token is 1 or more characters that are anything BUT CTLs or tspecials. CTLs = tspecials = "(" | ")" | "<">" | "@" | "," | ";" | ":" | "\" | <"/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
    SP = <US-ASCII SP, space (32)>
    HT = <US-ASCII HT, horizontal-tab (9)>
  • Futhermore, " The NAME=VALUE attribute-value pair must come first in each cookie. "
  • And, "When [the server] receives a Cookie header, the origin server should treat cookies with NAMEs whose prefix is $ specially, as an attribute for the adjacent . The value for such a NAME is to be interpreted as applying to the lexically (left-to-right) most recent cookie whose name does not have the $ prefix."
So, back to Sun's code now-- I get the !isToken(name) part, and I understand the name.startsWith("$") part, but where did the RFC say a cookie name can't be "Comment", "Discard", "Domain", "Expires", "Max-Age", "Path", "Secure", or "Version"? If the name=value pair has to come first, and the ONLY place you can find a semicolon is in the delimiter between cookies, then it shouldn't be any problem to parse the following headers: Server to client
[code]Set-Cookie: domain=foobar;domain=.yoursever.com[/code]
In this example, the name and value of the cookie is required to be first, so there should be no reason why the domain attribute would be confused with the name. Client to server
[code]Cookie: domain=foobar;$domain=.yourserver.com[/code]
Here $domain refers to the proceeding cookie called "domain" just like the RFC says. Frankly I don't think Sun's code is correct. I wouldn't be surprised if they did this because of some browser bug back in the day, but it will still be lame. In the mean time I think the only option to get rid of our errors might be to look into an Apache module that rewrites headers like mod_headers.