FULL PRODUCT VERSION :
Java Plug-in 1.6.0_01
Verwendung der JRE-Version 1.6.0_01 Java HotSpot(TM) Client VM
ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]
A DESCRIPTION OF THE PROBLEM :
When an HTTP response contains the following Headers:
Expires:0
Last-Modified: [current date]
Nevertheless, the response is put to the plugin cache and subsequent requests for the same resourcse are answered from that cache.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
0.) Create Source files from the provided source code (use markers to identify borders)
SimpleServer.java
PluginCacheTest.java
plugin_cache_test.html
1. Compile the Java Source files
2. Start the SimpleServer (assumes SimpleServer.class being in classpath)
java SimpleServer
3. Place the file PluginCacheTest.class to {user.home}
4. Specify your_ip in the file plugin_cache_test.html
5. Activate and clean your plugin (Applet) cache in the Java Control Panel
6. Load plugin_cache_test.html in a browser that uses the Java 6 Plugin
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The expected result is, that the second request for /zeroexpires.txt would not be delivered from cache. This would result in a different random value in the output (see actual result).
ACTUAL -
Content of initial zeroexpires.txt - delivers a random value
encoding:null, contentLength:18, expires:0(0)
0.6473260046689359
Content of subsequent zeroexpires.txt - if the value is equal to the initial request, the cache delivered the item
encoding:null, contentLength:18, expires:0(0)
0.6473260046689359
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
####################################### SimpleServer.java ###########
import java.io.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;
import java.util.zip.GZIPOutputStream;
import java.text.*;
/**
* Very minimalistic HTTP Server which servers 3 requests
*
* a) the zeroexpires.txt which contains an "Expires:0" response header
* b) the gzipped.txt which can be delivered gzipped/not gzipped
* c) the test Applet
*
* Note that this Program assumes the Applet's class file (PluginCacheTest.class)
* in $user.home
*
* @author Jan Kaiser <jan dot kaiser at interactivedata dot com>
*/
public class SimpleServer
{
static int buf_current;
static int buf_header_end;
static byte[] buf = new byte[4096];
static SimpleDateFormat expiresFormatter =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
public static void main(String[] args)
{
expiresFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
try
{
ServerSocket mySocket = new ServerSocket(8080);
while(true)
{
buf_current = 0;
buf_header_end = 0;
buf = new byte[4096];
Socket clientSocket = mySocket.accept();
System.out.println("New connection accepted " +
clientSocket.getInetAddress() + ":" + clientSocket.getPort());
OutputStream out = clientSocket.getOutputStream();
InputStream in = clientSocket.getInputStream();
while ((buf_header_end = findEndOfHeader(buf, 0, buf_current)) < 0)
{
buf_current += do_read(buf, buf_current, (buf.length - buf_current) - 1, in);
if (buf_current == buf.length)
{
throw new IOException("Header too long");
}
}
String header = new String(buf, 0, buf_header_end);
StringTokenizer tokens = new StringTokenizer(header,"\r\n");
String query = null;
Hashtable headers = new Hashtable();
while (tokens.hasMoreTokens())
{
String headerToken = tokens.nextToken();
int index = -1;
if ((index = headerToken.indexOf(":")) > -1)
{
String name = headerToken.substring(0,index);
String value = headerToken.substring(index+1,headerToken.length());
value = value.trim();
System.out.println("header:" + name + ":" + value + " from " + headerToken);
headers.put(name, value);
}
else // request
{
System.out.println("Query: " + headerToken);
StringTokenizer queryTokens = new StringTokenizer(headerToken, " ");
while (queryTokens.hasMoreTokens())
{
String queryToken = queryTokens.nextToken();
if (queryToken.startsWith("/"))
{
query = queryToken;
break;
}
}
}
}
if (query == null)
{
throw new IOException("No query available");
}
if (headers.size() == 0)
{
throw new IOException("No Headers");
}
// read the request body if there is some
if (headers.containsKey("ContentLength"))
{
int queryContentLength = Integer.parseInt((String)headers.get("Content-Length"));
readBody(queryContentLength, in);
}
String output = null;
// delivery of the zeroexpires content
if (query.equals("/zeroexpires.txt"))
{
String content = String.valueOf(Math.random());
output = "HTTP/1.0 200 OK\r\n" +
"Content-Length: "+ content.length() +"\r\n" +
"Content-Type: text/plain\r\n" +
"Last-Modified: Wed, 09 May 2007 08:13:10 GMT\r\n" +
"Expires: 0\r\n" +
"\r\n" +
content;
out.write(output.getBytes());
}
// delivery of the Applet itself
else if (query.indexOf("/PluginCacheTest") != -1)
{
FileInputStream fileStream = new FileInputStream(System.getProperty("user.home") + "/PluginCacheTest.class");
FileChannel channel = fileStream.getChannel();
long size = channel.size();
output = "HTTP/1.0 200 OK \r\n" +
"Content-Type: application/octet-stream\r\n" +
"Content-Length: " + size + "\r\n" +
"\r\n";
out.write(output.getBytes());
int read = -1;
while ((read = fileStream.read()) != -1)
{
try
{
out.write(read);
}
catch(Exception ex)
{
System.out.println("Caught exception when trying to write on out");
}
}
}
else
{
String content = query + " Not Found";
output = "HTTP/1.0 404 Not Found\r\n" +
"Content-Length: " + content.length() + "\r\n" +
"\r\n" +
content;
out.write(output.getBytes());
}
out.flush();
out.close();
}
}
catch(Exception ex)
{
System.out.println("Exception in main " + ex.getMessage());
ex.printStackTrace();
}
}
static int do_read(byte[] buf, int start, int length, InputStream input) throws IOException
{
int have_read = -1;
try
{
have_read = input.read(buf, start, length);
}
catch (InterruptedIOException ex)
{
throw new IOException("ReadTimeout");
}
if (have_read < 0)
{
throw new IOException("ServerSideClose");
}
else if (have_read == 0)
{
throw new IOException("ZeroBytesRead");
}
return have_read;
}
static byte[] do_read(byte[] buf, int current_body_length, InputStream i) throws IOException
{
int have_read = -1;
byte[] ret;
try
{
while (true)
{
have_read = i.read(buf, current_body_length, buf.length-current_body_length);
if (have_read >= 0)
{
if (have_read == (buf.length-current_body_length))
{
byte[] newBuf = new byte[2*buf.length];
System.arraycopy(buf, 0, newBuf, 0, buf.length);
buf = newBuf;
}
current_body_length += have_read;
}
else
{
break;
}
}
ret = new byte[current_body_length];
System.arraycopy(buf, 0, ret, 0, current_body_length);
}
catch (InterruptedIOException ex)
{
throw new IOException("ReadTimeout");
}
return ret;
}
static int findEndOfHeader(byte[] buf, int start, int length)
{
if (length < 2)
{
return -1;
}
int index = -1;
if ((index = new String(buf, start, length).indexOf("\r\n\r\n")) > -1)
{
return index;
}
return -1;
}
static byte[] readBody(int contentLength, InputStream input) throws IOException
{
byte[] body = new byte[contentLength];
int current_body_length = 0;
if (buf_header_end < buf_current) // read more than header
{
current_body_length = buf_current - buf_header_end;
if (current_body_length > contentLength)
{
current_body_length = contentLength;
System.arraycopy(buf, buf_header_end, body, 0, current_body_length); // copy current_body_length
int need_to_shift = buf_current - (buf_header_end + current_body_length);
System.arraycopy(buf, buf_current - need_to_shift, buf, 0, need_to_shift);
buf_current = need_to_shift;
}
else
{
System.arraycopy(buf, buf_header_end, body, 0, current_body_length);
buf_current = 0;
}
}
else
{
buf_current = 0;
}
buf_header_end = -1;
return getBody(current_body_length, contentLength, body, input);
}
static byte[] getBody(int current_body_length, int contentLength, byte[] body, InputStream input)
throws IOException
{
while (current_body_length < contentLength) // check wheter there is more to read
{
current_body_length += do_read(body, current_body_length, contentLength - current_body_length, input);
}
return body;
}
}
#################################### PluginCacheTest.java ###########
import java.applet.*;
import java.net.*;
import java.io.*;
import java.util.*;
import java.util.zip.*;
/**
* This applet reproduces the following behaviour of the plugin cache in java 6:
* - items that are delivered with the HTTP headers "Expires: 0" and "Last-Modified: [now]" are written in the cache and delivered
* from the cache on subsequent requests for the items
* - items that are requested with "Accept-Encoding: gzip" are delivered correctly the first time they are requested.
* On subsequent requests, these items are delivered from the Plugin Cache with the header "Content-Encoding:gzip" and
* "Content-Length: [gzipped-content-length]", but with decompressed content, so that the client tries to read the
* content having a length of gzipped-content-length and regards the content as gzipped, but can neither read the full content
* (because the content length is not correct) nor apply any gzip depcompression (because the content is not gzipped)
*
* This applet assumes the SimpleServer program running on its codebase.
*
* @author Jan Kaiser <jan dot kaiser at interactivedata dot com>
*/
public class PluginCacheTest extends Applet
{
public void start()
{
try
{
String url = getCodeBase() + "zeroexpires.txt";
System.out.println("Content of initial zeroexpires.txt - delivers a random value");
getContent(url, null);
Thread.sleep(5000);
System.out.println("Content of subsequent zeroexpires.txt - if the value is equal to the initial request, the cache delivered the item");
getContent(url, null);
}
catch(Exception ex)
{
reportException(ex);
}
}
private void getContent(String url, Hashtable headers) throws Exception
{
URL u = new URL(url);
URLConnection conn = u.openConnection();
if (headers != null)
{
Enumeration keys = headers.keys();
while (keys.hasMoreElements())
{
String key = (String)keys.nextElement();
String value = (String)headers.get(key);
conn.setRequestProperty(key, value);
}
}
InputStream is = conn.getInputStream();
boolean gzip = false;
String encoding = conn.getContentEncoding();
int contentLength = conn.getContentLength();
String expires = conn.getHeaderField("Expires");
long expiration = conn.getExpiration();
System.out.println("encoding:"+encoding+", contentLength:"+contentLength+", expires:" + expires+"("+expiration+")");
if (encoding != null && encoding.equals("gzip"))
{
gzip = true;
}
readStream(is, gzip);
}
private void readStream(InputStream is, boolean gzip) throws IOException
{
InputStream input = null;
if (gzip)
{
try
{
input = new GZIPInputStream(is);
}
catch(Exception ex)
{
System.out.println("Could not create GZIPInputStream, using normal InputStream: " + ex.getMessage());
input = is;
}
}
else
{
input = is;
}
int c = -1;
while ((c = input.read()) != -1)
{
System.out.print((char)c);
}
System.out.print("\n");
input.close();
}
private void reportException(Exception ex)
{
System.out.println("caught exception");
ex.printStackTrace();
}
}
############################### plugin_cache_test.html #########
<html>
<head>
<title>Java 6 - the story continues ...</title>
</head>
<body>
<applet code="PluginCacheTest" codebase="http://your_ip:8080/"></applet>
</body>
</html>
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
1) Disable the Plugin Cache (via Java Control Panel)
2) Use HTTP Headers
Pragma: no-cache
Cache-Control: no-cache
(which unfortunately disables caching on further intermediate caches, such as proxy servers)
Java Plug-in 1.6.0_01
Verwendung der JRE-Version 1.6.0_01 Java HotSpot(TM) Client VM
ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]
A DESCRIPTION OF THE PROBLEM :
When an HTTP response contains the following Headers:
Expires:0
Last-Modified: [current date]
Nevertheless, the response is put to the plugin cache and subsequent requests for the same resourcse are answered from that cache.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
0.) Create Source files from the provided source code (use markers to identify borders)
SimpleServer.java
PluginCacheTest.java
plugin_cache_test.html
1. Compile the Java Source files
2. Start the SimpleServer (assumes SimpleServer.class being in classpath)
java SimpleServer
3. Place the file PluginCacheTest.class to {user.home}
4. Specify your_ip in the file plugin_cache_test.html
5. Activate and clean your plugin (Applet) cache in the Java Control Panel
6. Load plugin_cache_test.html in a browser that uses the Java 6 Plugin
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The expected result is, that the second request for /zeroexpires.txt would not be delivered from cache. This would result in a different random value in the output (see actual result).
ACTUAL -
Content of initial zeroexpires.txt - delivers a random value
encoding:null, contentLength:18, expires:0(0)
0.6473260046689359
Content of subsequent zeroexpires.txt - if the value is equal to the initial request, the cache delivered the item
encoding:null, contentLength:18, expires:0(0)
0.6473260046689359
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
####################################### SimpleServer.java ###########
import java.io.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;
import java.util.zip.GZIPOutputStream;
import java.text.*;
/**
* Very minimalistic HTTP Server which servers 3 requests
*
* a) the zeroexpires.txt which contains an "Expires:0" response header
* b) the gzipped.txt which can be delivered gzipped/not gzipped
* c) the test Applet
*
* Note that this Program assumes the Applet's class file (PluginCacheTest.class)
* in $user.home
*
* @author Jan Kaiser <jan dot kaiser at interactivedata dot com>
*/
public class SimpleServer
{
static int buf_current;
static int buf_header_end;
static byte[] buf = new byte[4096];
static SimpleDateFormat expiresFormatter =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
public static void main(String[] args)
{
expiresFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
try
{
ServerSocket mySocket = new ServerSocket(8080);
while(true)
{
buf_current = 0;
buf_header_end = 0;
buf = new byte[4096];
Socket clientSocket = mySocket.accept();
System.out.println("New connection accepted " +
clientSocket.getInetAddress() + ":" + clientSocket.getPort());
OutputStream out = clientSocket.getOutputStream();
InputStream in = clientSocket.getInputStream();
while ((buf_header_end = findEndOfHeader(buf, 0, buf_current)) < 0)
{
buf_current += do_read(buf, buf_current, (buf.length - buf_current) - 1, in);
if (buf_current == buf.length)
{
throw new IOException("Header too long");
}
}
String header = new String(buf, 0, buf_header_end);
StringTokenizer tokens = new StringTokenizer(header,"\r\n");
String query = null;
Hashtable headers = new Hashtable();
while (tokens.hasMoreTokens())
{
String headerToken = tokens.nextToken();
int index = -1;
if ((index = headerToken.indexOf(":")) > -1)
{
String name = headerToken.substring(0,index);
String value = headerToken.substring(index+1,headerToken.length());
value = value.trim();
System.out.println("header:" + name + ":" + value + " from " + headerToken);
headers.put(name, value);
}
else // request
{
System.out.println("Query: " + headerToken);
StringTokenizer queryTokens = new StringTokenizer(headerToken, " ");
while (queryTokens.hasMoreTokens())
{
String queryToken = queryTokens.nextToken();
if (queryToken.startsWith("/"))
{
query = queryToken;
break;
}
}
}
}
if (query == null)
{
throw new IOException("No query available");
}
if (headers.size() == 0)
{
throw new IOException("No Headers");
}
// read the request body if there is some
if (headers.containsKey("ContentLength"))
{
int queryContentLength = Integer.parseInt((String)headers.get("Content-Length"));
readBody(queryContentLength, in);
}
String output = null;
// delivery of the zeroexpires content
if (query.equals("/zeroexpires.txt"))
{
String content = String.valueOf(Math.random());
output = "HTTP/1.0 200 OK\r\n" +
"Content-Length: "+ content.length() +"\r\n" +
"Content-Type: text/plain\r\n" +
"Last-Modified: Wed, 09 May 2007 08:13:10 GMT\r\n" +
"Expires: 0\r\n" +
"\r\n" +
content;
out.write(output.getBytes());
}
// delivery of the Applet itself
else if (query.indexOf("/PluginCacheTest") != -1)
{
FileInputStream fileStream = new FileInputStream(System.getProperty("user.home") + "/PluginCacheTest.class");
FileChannel channel = fileStream.getChannel();
long size = channel.size();
output = "HTTP/1.0 200 OK \r\n" +
"Content-Type: application/octet-stream\r\n" +
"Content-Length: " + size + "\r\n" +
"\r\n";
out.write(output.getBytes());
int read = -1;
while ((read = fileStream.read()) != -1)
{
try
{
out.write(read);
}
catch(Exception ex)
{
System.out.println("Caught exception when trying to write on out");
}
}
}
else
{
String content = query + " Not Found";
output = "HTTP/1.0 404 Not Found\r\n" +
"Content-Length: " + content.length() + "\r\n" +
"\r\n" +
content;
out.write(output.getBytes());
}
out.flush();
out.close();
}
}
catch(Exception ex)
{
System.out.println("Exception in main " + ex.getMessage());
ex.printStackTrace();
}
}
static int do_read(byte[] buf, int start, int length, InputStream input) throws IOException
{
int have_read = -1;
try
{
have_read = input.read(buf, start, length);
}
catch (InterruptedIOException ex)
{
throw new IOException("ReadTimeout");
}
if (have_read < 0)
{
throw new IOException("ServerSideClose");
}
else if (have_read == 0)
{
throw new IOException("ZeroBytesRead");
}
return have_read;
}
static byte[] do_read(byte[] buf, int current_body_length, InputStream i) throws IOException
{
int have_read = -1;
byte[] ret;
try
{
while (true)
{
have_read = i.read(buf, current_body_length, buf.length-current_body_length);
if (have_read >= 0)
{
if (have_read == (buf.length-current_body_length))
{
byte[] newBuf = new byte[2*buf.length];
System.arraycopy(buf, 0, newBuf, 0, buf.length);
buf = newBuf;
}
current_body_length += have_read;
}
else
{
break;
}
}
ret = new byte[current_body_length];
System.arraycopy(buf, 0, ret, 0, current_body_length);
}
catch (InterruptedIOException ex)
{
throw new IOException("ReadTimeout");
}
return ret;
}
static int findEndOfHeader(byte[] buf, int start, int length)
{
if (length < 2)
{
return -1;
}
int index = -1;
if ((index = new String(buf, start, length).indexOf("\r\n\r\n")) > -1)
{
return index;
}
return -1;
}
static byte[] readBody(int contentLength, InputStream input) throws IOException
{
byte[] body = new byte[contentLength];
int current_body_length = 0;
if (buf_header_end < buf_current) // read more than header
{
current_body_length = buf_current - buf_header_end;
if (current_body_length > contentLength)
{
current_body_length = contentLength;
System.arraycopy(buf, buf_header_end, body, 0, current_body_length); // copy current_body_length
int need_to_shift = buf_current - (buf_header_end + current_body_length);
System.arraycopy(buf, buf_current - need_to_shift, buf, 0, need_to_shift);
buf_current = need_to_shift;
}
else
{
System.arraycopy(buf, buf_header_end, body, 0, current_body_length);
buf_current = 0;
}
}
else
{
buf_current = 0;
}
buf_header_end = -1;
return getBody(current_body_length, contentLength, body, input);
}
static byte[] getBody(int current_body_length, int contentLength, byte[] body, InputStream input)
throws IOException
{
while (current_body_length < contentLength) // check wheter there is more to read
{
current_body_length += do_read(body, current_body_length, contentLength - current_body_length, input);
}
return body;
}
}
#################################### PluginCacheTest.java ###########
import java.applet.*;
import java.net.*;
import java.io.*;
import java.util.*;
import java.util.zip.*;
/**
* This applet reproduces the following behaviour of the plugin cache in java 6:
* - items that are delivered with the HTTP headers "Expires: 0" and "Last-Modified: [now]" are written in the cache and delivered
* from the cache on subsequent requests for the items
* - items that are requested with "Accept-Encoding: gzip" are delivered correctly the first time they are requested.
* On subsequent requests, these items are delivered from the Plugin Cache with the header "Content-Encoding:gzip" and
* "Content-Length: [gzipped-content-length]", but with decompressed content, so that the client tries to read the
* content having a length of gzipped-content-length and regards the content as gzipped, but can neither read the full content
* (because the content length is not correct) nor apply any gzip depcompression (because the content is not gzipped)
*
* This applet assumes the SimpleServer program running on its codebase.
*
* @author Jan Kaiser <jan dot kaiser at interactivedata dot com>
*/
public class PluginCacheTest extends Applet
{
public void start()
{
try
{
String url = getCodeBase() + "zeroexpires.txt";
System.out.println("Content of initial zeroexpires.txt - delivers a random value");
getContent(url, null);
Thread.sleep(5000);
System.out.println("Content of subsequent zeroexpires.txt - if the value is equal to the initial request, the cache delivered the item");
getContent(url, null);
}
catch(Exception ex)
{
reportException(ex);
}
}
private void getContent(String url, Hashtable headers) throws Exception
{
URL u = new URL(url);
URLConnection conn = u.openConnection();
if (headers != null)
{
Enumeration keys = headers.keys();
while (keys.hasMoreElements())
{
String key = (String)keys.nextElement();
String value = (String)headers.get(key);
conn.setRequestProperty(key, value);
}
}
InputStream is = conn.getInputStream();
boolean gzip = false;
String encoding = conn.getContentEncoding();
int contentLength = conn.getContentLength();
String expires = conn.getHeaderField("Expires");
long expiration = conn.getExpiration();
System.out.println("encoding:"+encoding+", contentLength:"+contentLength+", expires:" + expires+"("+expiration+")");
if (encoding != null && encoding.equals("gzip"))
{
gzip = true;
}
readStream(is, gzip);
}
private void readStream(InputStream is, boolean gzip) throws IOException
{
InputStream input = null;
if (gzip)
{
try
{
input = new GZIPInputStream(is);
}
catch(Exception ex)
{
System.out.println("Could not create GZIPInputStream, using normal InputStream: " + ex.getMessage());
input = is;
}
}
else
{
input = is;
}
int c = -1;
while ((c = input.read()) != -1)
{
System.out.print((char)c);
}
System.out.print("\n");
input.close();
}
private void reportException(Exception ex)
{
System.out.println("caught exception");
ex.printStackTrace();
}
}
############################### plugin_cache_test.html #########
<html>
<head>
<title>Java 6 - the story continues ...</title>
</head>
<body>
<applet code="PluginCacheTest" codebase="http://your_ip:8080/"></applet>
</body>
</html>
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
1) Disable the Plugin Cache (via Java Control Panel)
2) Use HTTP Headers
Pragma: no-cache
Cache-Control: no-cache
(which unfortunately disables caching on further intermediate caches, such as proxy servers)
- relates to
-
JDK-8146450 Java Web Start resource cache doesn't store all HTTP response headers
-
- Resolved
-