package gnu.java.net.protocol.http;
import gnu.java.net.protocol.http.event.RequestEvent;
import gnu.java.net.BASE64;
import gnu.java.net.LineInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
public class Request
{
protected final HTTPConnection connection;
protected final String method;
protected final String path;
protected final Headers requestHeaders;
protected RequestBodyWriter requestBodyWriter;
protected int requestBodyNegotiationThreshold;
protected ResponseBodyReader responseBodyReader;
protected Map responseHeaderHandlers;
protected Authenticator authenticator;
private boolean dispatched;
protected Request(HTTPConnection connection, String method,
String path)
{
this.connection = connection;
this.method = method;
this.path = path;
requestHeaders = new Headers();
responseHeaderHandlers = new HashMap();
requestBodyNegotiationThreshold = 4096;
}
public HTTPConnection getConnection()
{
return connection;
}
public String getMethod()
{
return method;
}
public String getPath()
{
return path;
}
public String getRequestURI()
{
return connection.getURI() + path;
}
public Headers getHeaders()
{
return requestHeaders;
}
public String getHeader(String name)
{
return requestHeaders.getValue(name);
}
public int getIntHeader(String name)
{
return requestHeaders.getIntValue(name);
}
public Date getDateHeader(String name)
{
return requestHeaders.getDateValue(name);
}
public void setHeader(String name, String value)
{
requestHeaders.put(name, value);
}
public void setRequestBody(byte[] requestBody)
{
setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody));
}
public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter)
{
this.requestBodyWriter = requestBodyWriter;
}
public void setResponseBodyReader(ResponseBodyReader responseBodyReader)
{
this.responseBodyReader = responseBodyReader;
}
public void setResponseHeaderHandler(String name,
ResponseHeaderHandler handler)
{
responseHeaderHandlers.put(name, handler);
}
public void setAuthenticator(Authenticator authenticator)
{
this.authenticator = authenticator;
}
public void setRequestBodyNegotiationThreshold(int threshold)
{
requestBodyNegotiationThreshold = threshold;
}
public Response dispatch()
throws IOException
{
if (dispatched)
{
throw new ProtocolException("request already dispatched");
}
final String CRLF = "\r\n";
final String HEADER_SEP = ": ";
final String US_ASCII = "US-ASCII";
final String version = connection.getVersion();
Response response;
int contentLength = -1;
boolean retry = false;
int attempts = 0;
boolean expectingContinue = false;
if (requestBodyWriter != null)
{
contentLength = requestBodyWriter.getContentLength();
if (contentLength > requestBodyNegotiationThreshold)
{
expectingContinue = true;
setHeader("Expect", "100-continue");
}
else
{
setHeader("Content-Length", Integer.toString(contentLength));
}
}
try
{
do
{
retry = false;
connection.fireRequestEvent(RequestEvent.REQUEST_SENDING, this);
OutputStream out = connection.getOutputStream();
LineInputStream in =
new LineInputStream(connection.getInputStream());
String requestUri = path;
if (connection.isUsingProxy() &&
!"*".equals(requestUri) &&
!"CONNECT".equals(method))
{
requestUri = getRequestURI();
}
String line = method + ' ' + requestUri + ' ' + version + CRLF;
out.write(line.getBytes(US_ASCII));
for (Iterator i = requestHeaders.keySet().iterator();
i.hasNext(); )
{
String name =(String) i.next();
String value =(String) requestHeaders.get(name);
line = name + HEADER_SEP + value + CRLF;
out.write(line.getBytes(US_ASCII));
}
out.write(CRLF.getBytes(US_ASCII));
if (requestBodyWriter != null && !expectingContinue)
{
byte[] buffer = new byte[4096];
int len;
int count = 0;
requestBodyWriter.reset();
do
{
len = requestBodyWriter.write(buffer);
if (len > 0)
{
out.write(buffer, 0, len);
}
count += len;
}
while (len > -1 && count < contentLength);
out.write(CRLF.getBytes(US_ASCII));
}
out.flush();
connection.fireRequestEvent(RequestEvent.REQUEST_SENT, this);
response = readResponse(in);
int sc = response.getCode();
if (sc == 401 && authenticator != null)
{
if (authenticate(response, attempts++))
{
retry = true;
}
}
else if (sc == 100 && expectingContinue)
{
requestHeaders.remove("Expect");
setHeader("Content-Length", Integer.toString(contentLength));
expectingContinue = false;
retry = true;
}
}
while (retry);
}
catch (IOException e)
{
connection.close();
throw e;
}
return response;
}
Response readResponse(LineInputStream in)
throws IOException
{
String line;
int len;
line = in.readLine();
if (line == null)
{
throw new ProtocolException("Peer closed connection");
}
if (!line.startsWith("HTTP/"))
{
throw new ProtocolException(line);
}
len = line.length();
int start = 5, end = 6;
while (line.charAt(end) != '.')
{
end++;
}
int majorVersion = Integer.parseInt(line.substring(start, end));
start = end + 1;
end = start + 1;
while (line.charAt(end) != ' ')
{
end++;
}
int minorVersion = Integer.parseInt(line.substring(start, end));
start = end + 1;
end = start + 3;
int code = Integer.parseInt(line.substring(start, end));
String message = line.substring(end + 1, len - 1);
Headers responseHeaders = new Headers();
responseHeaders.parse(in);
notifyHeaderHandlers(responseHeaders);
int codeClass = code / 100;
Response ret = new Response(majorVersion, minorVersion, code,
codeClass, message, responseHeaders);
switch (code)
{
case 204:
case 205:
break;
default:
boolean notify = (responseBodyReader != null);
if (notify)
{
if (!responseBodyReader.accept(this, ret))
{
notify = false;
}
}
readResponseBody(ret, in, notify);
}
return ret;
}
void notifyHeaderHandlers(Headers headers)
{
for (Iterator i = headers.entrySet().iterator(); i.hasNext(); )
{
Map.Entry entry = (Map.Entry) i.next();
String name =(String) entry.getKey();
if ("Set-Cookie".equalsIgnoreCase(name))
{
String value = (String) entry.getValue();
handleSetCookie(value);
}
ResponseHeaderHandler handler =
(ResponseHeaderHandler) responseHeaderHandlers.get(name);
if (handler != null)
{
String value = (String) entry.getValue();
handler.setValue(value);
}
}
}
void readResponseBody(Response response, InputStream in,
boolean notify)
throws IOException
{
byte[] buffer = new byte[4096];
int contentLength = -1;
Headers trailer = null;
String transferCoding = response.getHeader("Transfer-Encoding");
if ("chunked".equalsIgnoreCase(transferCoding))
{
trailer = new Headers();
in = new ChunkedInputStream(in, trailer);
}
else
{
contentLength = response.getIntHeader("Content-Length");
}
String contentCoding = response.getHeader("Content-Encoding");
if (contentCoding != null && !"identity".equals(contentCoding))
{
if ("gzip".equals(contentCoding))
{
in = new GZIPInputStream(in);
}
else if ("deflate".equals(contentCoding))
{
in = new InflaterInputStream(in);
}
else
{
throw new ProtocolException("Unsupported Content-Encoding: " +
contentCoding);
}
}
boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
"close".equalsIgnoreCase(response.getHeader("Connection")) ||
(connection.majorVersion == 1 && connection.minorVersion == 0) ||
(response.majorVersion == 1 && response.minorVersion == 0);
int count = contentLength;
int len = (count > -1) ? count : buffer.length;
len = (len > buffer.length) ? buffer.length : len;
while (len > -1)
{
len = in.read(buffer, 0, len);
if (len < 0)
{
connection.closeConnection();
break;
}
if (notify)
{
responseBodyReader.read(buffer, 0, len);
}
if (count > -1)
{
count -= len;
if (count < 1)
{
if (doClose)
{
connection.closeConnection();
}
break;
}
}
}
if (notify)
{
responseBodyReader.close();
}
if (trailer != null)
{
response.getHeaders().putAll(trailer);
notifyHeaderHandlers(trailer);
}
}
boolean authenticate(Response response, int attempts)
throws IOException
{
String challenge = response.getHeader("WWW-Authenticate");
if (challenge == null)
{
challenge = response.getHeader("Proxy-Authenticate");
}
int si = challenge.indexOf(' ');
String scheme = (si == -1) ? challenge : challenge.substring(0, si);
if ("Basic".equalsIgnoreCase(scheme))
{
Properties params = parseAuthParams(challenge.substring(si + 1));
String realm = params.getProperty("realm");
Credentials creds = authenticator.getCredentials(realm, attempts);
String userPass = creds.getUsername() + ':' + creds.getPassword();
byte[] b_userPass = userPass.getBytes("US-ASCII");
byte[] b_encoded = BASE64.encode(b_userPass);
String authorization =
scheme + " " + new String(b_encoded, "US-ASCII");
setHeader("Authorization", authorization);
return true;
}
else if ("Digest".equalsIgnoreCase(scheme))
{
Properties params = parseAuthParams(challenge.substring(si + 1));
String realm = params.getProperty("realm");
String nonce = params.getProperty("nonce");
String qop = params.getProperty("qop");
String algorithm = params.getProperty("algorithm");
String digestUri = getRequestURI();
Credentials creds = authenticator.getCredentials(realm, attempts);
String username = creds.getUsername();
String password = creds.getPassword();
connection.incrementNonce(nonce);
try
{
MessageDigest md5 = MessageDigest.getInstance("MD5");
final byte[] COLON = { 0x3a };
md5.reset();
md5.update(username.getBytes("US-ASCII"));
md5.update(COLON);
md5.update(realm.getBytes("US-ASCII"));
md5.update(COLON);
md5.update(password.getBytes("US-ASCII"));
byte[] ha1 = md5.digest();
if ("md5-sess".equals(algorithm))
{
byte[] cnonce = generateNonce();
md5.reset();
md5.update(ha1);
md5.update(COLON);
md5.update(nonce.getBytes("US-ASCII"));
md5.update(COLON);
md5.update(cnonce);
ha1 = md5.digest();
}
String ha1Hex = toHexString(ha1);
md5.reset();
md5.update(method.getBytes("US-ASCII"));
md5.update(COLON);
md5.update(digestUri.getBytes("US-ASCII"));
if ("auth-int".equals(qop))
{
byte[] hEntity = null; md5.update(COLON);
md5.update(hEntity);
}
byte[] ha2 = md5.digest();
String ha2Hex = toHexString(ha2);
md5.reset();
md5.update(ha1Hex.getBytes("US-ASCII"));
md5.update(COLON);
md5.update(nonce.getBytes("US-ASCII"));
if ("auth".equals(qop) || "auth-int".equals(qop))
{
String nc = getNonceCount(nonce);
byte[] cnonce = generateNonce();
md5.update(COLON);
md5.update(nc.getBytes("US-ASCII"));
md5.update(COLON);
md5.update(cnonce);
md5.update(COLON);
md5.update(qop.getBytes("US-ASCII"));
}
md5.update(COLON);
md5.update(ha2Hex.getBytes("US-ASCII"));
String digestResponse = toHexString(md5.digest());
String authorization = scheme +
" username=\"" + username + "\"" +
" realm=\"" + realm + "\"" +
" nonce=\"" + nonce + "\"" +
" uri=\"" + digestUri + "\"" +
" response=\"" + digestResponse + "\"";
setHeader("Authorization", authorization);
return true;
}
catch (NoSuchAlgorithmException e)
{
return false;
}
}
return false;
}
Properties parseAuthParams(String text)
{
int len = text.length();
String key = null;
StringBuffer buf = new StringBuffer();
Properties ret = new Properties();
boolean inQuote = false;
for (int i = 0; i < len; i++)
{
char c = text.charAt(i);
if (c == '"')
{
inQuote = !inQuote;
}
else if (c == '=' && key == null)
{
key = buf.toString().trim();
buf.setLength(0);
}
else if (c == ' ' && !inQuote)
{
String value = unquote(buf.toString().trim());
ret.put(key, value);
key = null;
buf.setLength(0);
}
else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
{
buf.append(c);
}
}
if (key != null)
{
String value = unquote(buf.toString().trim());
ret.put(key, value);
}
return ret;
}
String unquote(String text)
{
int len = text.length();
if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
{
return text.substring(1, len - 1);
}
return text;
}
String getNonceCount(String nonce)
{
int nc = connection.getNonceCount(nonce);
String hex = Integer.toHexString(nc);
StringBuffer buf = new StringBuffer();
for (int i = 8 - hex.length(); i > 0; i--)
{
buf.append('0');
}
buf.append(hex);
return buf.toString();
}
byte[] nonce;
byte[] generateNonce()
throws IOException, NoSuchAlgorithmException
{
if (nonce == null)
{
long time = System.currentTimeMillis();
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(Long.toString(time).getBytes("US-ASCII"));
nonce = md5.digest();
}
return nonce;
}
String toHexString(byte[] bytes)
{
char[] ret = new char[bytes.length * 2];
for (int i = 0, j = 0; i < bytes.length; i++)
{
int c =(int) bytes[i];
if (c < 0)
{
c += 0x100;
}
ret[j++] = Character.forDigit(c / 0x10, 0x10);
ret[j++] = Character.forDigit(c % 0x10, 0x10);
}
return new String(ret);
}
void handleSetCookie(String text)
{
CookieManager cookieManager = connection.getCookieManager();
if (cookieManager == null)
{
return;
}
String name = null;
String value = null;
String comment = null;
String domain = connection.getHostName();
String path = this.path;
int lsi = path.lastIndexOf('/');
if (lsi != -1)
{
path = path.substring(0, lsi);
}
boolean secure = false;
Date expires = null;
int len = text.length();
String attr = null;
StringBuffer buf = new StringBuffer();
boolean inQuote = false;
for (int i = 0; i <= len; i++)
{
char c =(i == len) ? '\u0000' : text.charAt(i);
if (c == '"')
{
inQuote = !inQuote;
}
else if (!inQuote)
{
if (c == '=' && attr == null)
{
attr = buf.toString().trim();
buf.setLength(0);
}
else if (c == ';' || i == len || c == ',')
{
String val = unquote(buf.toString().trim());
if (name == null)
{
name = attr;
value = val;
}
else if ("Comment".equalsIgnoreCase(attr))
{
comment = val;
}
else if ("Domain".equalsIgnoreCase(attr))
{
domain = val;
}
else if ("Path".equalsIgnoreCase(attr))
{
path = val;
}
else if ("Secure".equalsIgnoreCase(val))
{
secure = true;
}
else if ("Max-Age".equalsIgnoreCase(attr))
{
int delta = Integer.parseInt(val);
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(System.currentTimeMillis());
cal.add(Calendar.SECOND, delta);
expires = cal.getTime();
}
else if ("Expires".equalsIgnoreCase(attr))
{
DateFormat dateFormat = new HTTPDateFormat();
try
{
expires = dateFormat.parse(val);
}
catch (ParseException e)
{
buf.append(c);
continue;
}
}
attr = null;
buf.setLength(0);
if (i == len || c == ',')
{
Cookie cookie = new Cookie(name, value, comment, domain,
path, secure, expires);
cookieManager.setCookie(cookie);
}
if (c == ',')
{
name = null;
value = null;
comment = null;
domain = connection.getHostName();
path = this.path;
if (lsi != -1)
{
path = path.substring(0, lsi);
}
secure = false;
expires = null;
}
}
else
{
buf.append(c);
}
}
else
{
buf.append(c);
}
}
}
}