com.twelvemonkeys.net.HttpURLConnection Maven / Gradle / Ivy
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.net;
import com.twelvemonkeys.lang.StringUtil;
import com.twelvemonkeys.util.BASE64;
import java.io.*;
import java.net.*;
import java.util.*;
/**
* A URLConnection with support for HTTP-specific features. See
* the spec for details.
* This version also supports read and connect timeouts, making it more useful
* for clients with limitted time.
*
* Note that the timeouts are created on the socket level, and that
*
* Note: This class should now work as expected, but it need more testing before
* it can enter production release.
*
* --.k
*
* @author Harald Kuhr
* @author last modified by $Author: haku $
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $
* @todo Write JUnit TestCase
* @todo ConnectionMananger!
* @see RFC 2616
*/
public class HttpURLConnection extends java.net.HttpURLConnection {
/**
* HTTP Status-Code 307: Temporary Redirect
*/
public final static int HTTP_REDIRECT = 307;
private final static int HTTP_DEFAULT_PORT = 80;
private final static String HTTP_HEADER_END = "\r\n\r\n";
private static final String HEADER_WWW_AUTH = "WWW-Authenticate";
private final static int BUF_SIZE = 8192;
private int mMaxRedirects = (System.getProperty("http.maxRedirects") != null)
? Integer.parseInt(System.getProperty("http.maxRedirects"))
: 20;
protected int mTimeout = -1;
protected int mConnectTimeout = -1;
private Socket mSocket = null;
protected InputStream mErrorStream = null;
protected InputStream mInputStream = null;
protected OutputStream mOutputStream = null;
private String[] mResponseHeaders = null;
protected Properties mResponseHeaderFields = null;
protected Properties mRequestProperties = new Properties();
/**
* Creates a HttpURLConnection.
*
* @param pURL the URL to connect to.
*/
protected HttpURLConnection(URL pURL) {
this(pURL, 0, 0);
}
/**
* Creates a HttpURLConnection with a given read and connect timeout.
* A timeout value of zero is interpreted as an
* infinite timeout.
*
* @param pURL the URL to connect to.
* @param pTimeout the maximum time the socket will block for read
* and connect operations.
*/
protected HttpURLConnection(URL pURL, int pTimeout) {
this(pURL, pTimeout, pTimeout);
}
/**
* Creates a HttpURLConnection with a given read and connect timeout.
* A timeout value of zero is interpreted as an
* infinite timeout.
*
* @param pURL the URL to connect to.
* @param pTimeout the maximum time the socket will block for read
* operations.
* @param pConnectTimeout the maximum time the socket will block for
* connection.
*/
protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) {
super(pURL);
setTimeout(pTimeout);
mConnectTimeout = pConnectTimeout;
}
/**
* Sets the general request property. If a property with the key already
* exists, overwrite its value with the new value.
*
* NOTE: HTTP requires all request properties which can
* legally have multiple instances with the same key
* to use a comma-seperated list syntax which enables multiple
* properties to be appended into a single property.
*
* @param pKey the keyword by which the request is known
* (e.g., "accept
").
* @param pValue the value associated with it.
* @see #getRequestProperty(java.lang.String)
*/
public void setRequestProperty(String pKey, String pValue) {
if (connected) {
throw new IllegalAccessError("Already connected");
}
String oldValue = mRequestProperties.getProperty(pKey);
if (oldValue == null) {
mRequestProperties.setProperty(pKey, pValue);
}
else {
mRequestProperties.setProperty(pKey, oldValue + ", " + pValue);
}
}
/**
* Returns the value of the named general request property for this
* connection.
*
* @param pKey the keyword by which the request is known (e.g., "accept").
* @return the value of the named general request property for this
* connection.
* @see #setRequestProperty(java.lang.String, java.lang.String)
*/
public String getRequestProperty(String pKey) {
if (connected) {
throw new IllegalAccessError("Already connected");
}
return mRequestProperties.getProperty(pKey);
}
/**
* Gets HTTP response status from responses like:
*
* HTTP/1.0 200 OK
* HTTP/1.0 401 Unauthorized
*
* Extracts the ints 200 and 401 respectively.
* Returns -1 if none can be discerned
* from the response (i.e., the response is not valid HTTP).
*
*
*
* @return the HTTP Status-Code
* @throws IOException if an error occurred connecting to the server.
*/
public int getResponseCode() throws IOException {
if (responseCode != -1) {
return responseCode;
}
// Make sure we've gotten the headers
getInputStream();
String resp = getHeaderField(0);
// should have no leading/trailing LWS
// expedite the typical case by assuming it has the
// form "HTTP/1.x 2XX "
int ind;
try {
ind = resp.indexOf(' ');
while (resp.charAt(ind) == ' ') {
ind++;
}
responseCode = Integer.parseInt(resp.substring(ind, ind + 3));
responseMessage = resp.substring(ind + 4).trim();
return responseCode;
}
catch (Exception e) {
return responseCode;
}
}
/**
* Returns the name of the specified header field.
*
* @param pName the name of a header field.
* @return the value of the named header field, or null
* if there is no such field in the header.
*/
public String getHeaderField(String pName) {
return mResponseHeaderFields.getProperty(StringUtil.toLowerCase(pName));
}
/**
* Returns the value for the n
th header field.
* It returns null
if there are fewer than
* n
fields.
*
* This method can be used in conjunction with the
* getHeaderFieldKey
method to iterate through all
* the headers in the message.
*
* @param pIndex an index.
* @return the value of the n
th header field.
* @see java.net.URLConnection#getHeaderFieldKey(int)
*/
public String getHeaderField(int pIndex) {
// TODO: getInputStream() first, to make sure we have header fields
if (pIndex >= mResponseHeaders.length) {
return null;
}
String field = mResponseHeaders[pIndex];
// pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK").
if ((pIndex == 0) || (field == null)) {
return field;
}
int idx = field.indexOf(':');
return ((idx > 0)
? field.substring(idx).trim()
: ""); // TODO: "" or null?
}
/**
* Returns the key for the n
th header field.
*
* @param pIndex an index.
* @return the key for the n
th header field,
* or null
if there are fewer than n
* fields.
*/
public String getHeaderFieldKey(int pIndex) {
// TODO: getInputStream() first, to make sure we have header fields
if (pIndex >= mResponseHeaders.length) {
return null;
}
String field = mResponseHeaders[pIndex];
if (StringUtil.isEmpty(field)) {
return null;
}
int idx = field.indexOf(':');
return StringUtil.toLowerCase(((idx > 0)
? field.substring(0, idx)
: field));
}
/**
* Sets the read timeout for the undelying socket.
* A timeout of zero is interpreted as an
* infinite timeout.
*
* @param pTimeout the maximum time the socket will block for read
* operations, in milliseconds.
*/
public void setTimeout(int pTimeout) {
if (pTimeout < 0) { // Must be positive
throw new IllegalArgumentException("Timeout must be positive.");
}
mTimeout = pTimeout;
if (mSocket != null) {
try {
mSocket.setSoTimeout(pTimeout);
}
catch (SocketException se) {
// Not much to do about that...
}
}
}
/**
* Gets the read timeout for the undelying socket.
*
* @return the maximum time the socket will block for read operations, in
* milliseconds.
* The default value is zero, which is interpreted as an
* infinite timeout.
*/
public int getTimeout() {
try {
return ((mSocket != null)
? mSocket.getSoTimeout()
: mTimeout);
}
catch (SocketException se) {
return mTimeout;
}
}
/**
* Returns an input stream that reads from this open connection.
*
* @return an input stream that reads from this open connection.
* @throws IOException if an I/O error occurs while
* creating the input stream.
*/
public synchronized InputStream getInputStream() throws IOException {
if (!connected) {
connect();
}
// Nothing to return
if (responseCode == HTTP_NOT_FOUND) {
throw new FileNotFoundException(url.toString());
}
int length;
if (mInputStream == null) {
return null;
}
// "De-chunk" the output stream
else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) {
if (!(mInputStream instanceof ChunkedInputStream)) {
mInputStream = new ChunkedInputStream(mInputStream);
}
}
// Make sure we don't wait forever, if the content-length is known
else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) {
if (!(mInputStream instanceof FixedLengthInputStream)) {
mInputStream = new FixedLengthInputStream(mInputStream, length);
}
}
return mInputStream;
}
/**
* Returns an output stream that writes to this connection.
*
* @return an output stream that writes to this connection.
* @throws IOException if an I/O error occurs while
* creating the output stream.
*/
public synchronized OutputStream getOutputStream() throws IOException {
if (!connected) {
connect();
}
return mOutputStream;
}
/**
* Indicates that other requests to the server
* are unlikely in the near future. Calling disconnect()
* should not imply that this HttpURLConnection
* instance can be reused for other requests.
*/
public void disconnect() {
if (mSocket != null) {
try {
mSocket.close();
}
catch (IOException ioe) {
// Does not matter, I guess.
}
mSocket = null;
}
connected = false;
}
/**
* Internal connect method.
*/
private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException {
// Find correct port
final int port = (pURL.getPort() > 0)
? pURL.getPort()
: HTTP_DEFAULT_PORT;
// Create socket if we don't have one
if (mSocket == null) {
//mSocket = new Socket(pURL.getHost(), port); // Blocks...
mSocket = createSocket(pURL, port, mConnectTimeout);
mSocket.setSoTimeout(mTimeout);
}
// Get Socket output stream
OutputStream os = mSocket.getOutputStream();
// Connect using HTTP
writeRequestHeaders(os, pURL, method, mRequestProperties, usingProxy(), pAuth, pAuthType);
// Get response input stream
InputStream sis = mSocket.getInputStream();
BufferedInputStream is = new BufferedInputStream(sis);
// Detatch reponse headers from reponse input stream
InputStream header = detatchResponseHeader(is);
// Parse headers and set response code/message
mResponseHeaders = parseResponseHeader(header);
mResponseHeaderFields = parseHeaderFields(mResponseHeaders);
//System.err.println("Headers fields:");
//mResponseHeaderFields.list(System.err);
// Test HTTP response code, to see if further action is needed
switch (getResponseCode()) {
case HTTP_OK:
// 200 OK
mInputStream = is;
mErrorStream = null;
break;
/*
case HTTP_PROXY_AUTH:
// 407 Proxy Authentication Required
*/
case HTTP_UNAUTHORIZED:
// 401 Unauthorized
// Set authorization and try again.. Slightly more compatible
responseCode = -1;
// IS THIS REDIRECTION??
//if (instanceFollowRedirects) { ???
String auth = getHeaderField(HEADER_WWW_AUTH);
// Missing WWW-Authenticate header for 401 response is an error
if (StringUtil.isEmpty(auth)) {
throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage);
}
// Get real mehtod from WWW-Authenticate header
int SP = auth.indexOf(" ");
String method;
String realm = null;
if (SP >= 0) {
method = auth.substring(0, SP);
if (auth.length() >= SP + 7) {
realm = auth.substring(SP + 7); // " realm=".lenght() == 7
}
// else no realm
}
else {
// Default mehtod is Basic
method = SimpleAuthenticator.BASIC;
}
// Get PasswordAuthentication
PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port,
pURL.getProtocol(), realm, method);
// Avoid infinite loop
if (pRetries++ <= 0) {
throw new ProtocolException("Server redirected too many times (" + mMaxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does
}
else if (pa != null) {
connect(pURL, pa, method, pRetries);
}
break;
case HTTP_MOVED_PERM:
// 301 Moved Permanently
case HTTP_MOVED_TEMP:
// 302 Found
case HTTP_SEE_OTHER:
// 303 See Other
/*
case HTTP_USE_PROXY:
// 305 Use Proxy
// How do we handle this?
*/
case HTTP_REDIRECT:
// 307 Temporary Redirect
//System.err.println("Redirecting " + getResponseCode());
if (instanceFollowRedirects) {
// Redirect
responseCode = -1; // Because of the java.net.URLConnection
// getResponseCode implementation...
// ---
// I think redirects must be get?
//setRequestMethod("GET");
// ---
String location = getHeaderField("Location");
URL newLoc = new URL(pURL, location);
// Test if we can reuse the Socket
if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) {
mSocket.close(); // Close the socket, won't need it anymore
mSocket = null;
}
if (location != null) {
//System.err.println("Redirecting to " + location);
// Avoid infinite loop
if (--pRetries <= 0) {
throw new ProtocolException("Server redirected too many times (5)");
}
else {
connect(newLoc, pAuth, pAuthType, pRetries);
}
}
break;
}
// ...else, fall through default (if no Location: header)
default :
// Not 200 OK, or any of the redirect responses
// Probably an error...
mErrorStream = is;
mInputStream = null;
}
// --- Need rethinking...
// No further questions, let the Socket wait forever (until the server
// closes the connection)
//mSocket.setSoTimeout(0);
// Probably not... The timeout should only kick if the read BLOCKS.
// Shutdown output, meaning any writes to the outputstream below will
// probably fail...
//mSocket.shutdownOutput();
// Not a good idea at all... POSTs need the outputstream to send the
// form-data.
// --- /Need rethinking.
mOutputStream = os;
}
private static interface SocketConnector extends Runnable {
/**
* Method getSocket
*
* @return the socket
* @throws IOException
*/
public Socket getSocket() throws IOException;
}
/**
* Creates a socket to the given URL and port, with the given connect
* timeout. If the socket waits more than the given timout to connect,
* an ConnectException is thrown.
*
* @param pURL the URL to connect to
* @param pPort the port to connect to
* @param pConnectTimeout the connect timeout
* @return the created Socket.
* @throws ConnectException if the connection is refused or otherwise
* times out.
* @throws UnknownHostException if the IP address of the host could not be
* determined.
* @throws IOException if an I/O error occurs when creating the socket.
* @todo Move this code to a SocetImpl or similar?
* @see Socket#Socket(String,int)
*/
private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException {
Socket socket;
final Object current = this;
SocketConnector connector;
Thread t = new Thread(connector = new SocketConnector() {
private IOException mConnectException = null;
private Socket mLocalSocket = null;
public Socket getSocket() throws IOException {
if (mConnectException != null) {
throw mConnectException;
}
return mLocalSocket;
}
// Run method
public void run() {
try {
mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks...
}
catch (IOException ioe) {
// Store this exception for later
mConnectException = ioe;
}
// Signal that we are done
synchronized (current) {
current.notify();
}
}
});
t.start();
// Wait for connect
synchronized (this) {
try {
/// Only wait if thread is alive!
if (t.isAlive()) {
if (pConnectTimeout > 0) {
wait(pConnectTimeout);
}
else {
wait();
}
}
}
catch (InterruptedException ie) {
// Continue excecution on interrupt? Hmmm..
}
}
// Throw exception if the socket didn't connect fast enough
if ((socket = connector.getSocket()) == null) {
throw new ConnectException("Socket connect timed out!");
}
return socket;
}
/**
* Opens a communications link to the resource referenced by this
* URL, if such a connection has not already been established.
*
* If the connect
method is called when the connection
* has already been opened (indicated by the connected
* field having the value true
), the call is ignored.
*
* URLConnection objects go through two phases: first they are
* created, then they are connected. After being created, and
* before being connected, various options can be specified
* (e.g., doInput and UseCaches). After connecting, it is an
* error to try to set them. Operations that depend on being
* connected, like getContentLength, will implicitly perform the
* connection, if necessary.
*
* @throws IOException if an I/O error occurs while opening the
* connection.
* @see java.net.URLConnection#connected
* @see RFC 2616
*/
public void connect() throws IOException {
if (connected) {
return; // Ignore
}
connected = true;
connect(url, null, null, mMaxRedirects);
}
/**
* TODO: Proxy support is still missing.
*
* @return this method returns false, as proxy suport is not implemented.
*/
public boolean usingProxy() {
return false;
}
/**
* Writes the HTTP request headers, for HTTP GET method.
*
* @see RFC 2616
*/
private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy,
PasswordAuthentication pAuth, String pAuthType) {
PrintWriter out = new PrintWriter(pOut, true); // autoFlush
if (!pUsingProxy) {
out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath())
? pURL.getPath()
: "/") + ((pURL.getQuery() != null)
? "?" + pURL.getQuery()
: "") + " HTTP/1.1"); // HTTP/1.1
// out.println("Connection: close"); // No persistent connections yet
/*
System.err.println(pMethod + " "
+ (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/")
+ (pURL.getQuery() != null ? "?" + pURL.getQuery() : "")
+ " HTTP/1.1"); // HTTP/1.1
*/
// Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0)
out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1)
? ":" + pURL.getPort()
: ""));
/*
System.err.println("Host: " + pURL.getHost()
+ (pURL.getPort() != -1 ? ":" + pURL.getPort() : ""));
*/
}
else {
////-- PROXY (absolute) VERSION
out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1)
? ":" + pURL.getPort()
: "") + pURL.getPath() + ((pURL.getQuery() != null)
? "?" + pURL.getQuery()
: "") + " HTTP/1.1");
}
// Check if we have authentication
if (pAuth != null) {
// If found, set Authorization header
byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes();
// "Authorization" ":" credentials
out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass));
/*
System.err.println("Authorization: " + pAuthType + " "
+ BASE64.encode(userPass));
*/
}
// Iterate over properties
for (Map.Entry