// Copyright (C) 2012 jOVAL.org. All rights reserved.
// This software is licensed under the AGPL 3.0 license available at http://www.joval.org/agpl_v3.txt
package jwsmv.http;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import jwsmv.Message;
import jwsmv.util.RFC822;
/**
* An HTTP 1.1 connection implementation that re-uses a single socket connection. This is useful when a single TCP connection
* is needed to communicate repeatedly with a particular URL, for example, when performing NTLM authentication negotiation.
*
* Thanks to James Marshall for his concise discussion of HTTP/1.1:
* @see http://www.jmarshall.com/easy/http/#http1.1c2
*
* @author David A. Solin
* @version %I% %G%
*/
abstract class AbstractConnection extends HttpURLConnection {
static final byte[] CRLF = {'\r', '\n'};
static int TIMEOUT = 90000; // 90 sec, which is greater than the 60 sec wsmv:receive SOAP response timeout
/**
* Set the timeout value for all sockets.
*/
static void setTimeout(int msecs) {
TIMEOUT = msecs;
}
/**
* Set a property of a multi-valued map.
*/
static void setMapProperty(String key, String value, Map<String, List<String>> map) {
List<String> values = new ArrayList<String>();
values.add(value);
boolean found = false;
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
if (key.equalsIgnoreCase(entry.getKey())) {
entry.setValue(values);
found = true;
break;
}
}
if (!found) {
map.put(key, values);
}
}
/**
* Add a key/value pair to a multi-valued map. If the value is comma-delimited, its values are parsed and added to
* the map accordingly.
*/
static void addMapProperties(KVP pair, Map<String, List<String>> map) {
for (String val : pair.value().split(", ")) {
addMapProperty(pair.key(), val, map);
}
}
/**
* Add a property to a multi-valued map.
*/
static void addMapProperty(String key, String value, Map<String, List<String>>map) {
List<String> values = null;
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
if (key.equalsIgnoreCase(entry.getKey())) {
values = entry.getValue();
values.add(value);
break;
}
}
if (values == null) {
values = new ArrayList<String>();
values.add(value);
map.put(key, values);
}
}
/**
* Reads a line as a key-value-pair, or returns null when CRLF is reached.
*/
static KVP readKVP(InputStream in) throws IOException {
StringBuffer sb = new StringBuffer();
boolean done = false;
boolean cr = false;
while(!done) {
int ch = in.read();
switch(ch) {
case -1:
throw new IOException("Connection was closed!");
case '\r':
if (sb.length() == 0) {
cr = true;
}
break;
case '\n':
if (cr) {
return null;
} else if (sb.length() > 0) {
return new KVP(sb.toString());
}
// fall-thru
default:
cr = false;
sb.append((char)ch);
break;
}
}
throw new ProtocolException("Failed to parse header from " + sb.toString());
}
/**
* Container for a Key-Value Pair.
*/
static class KVP {
private String key, value;
KVP(String header) throws IllegalArgumentException {
int ptr = header.indexOf(": ");
if (ptr == -1) {
key = "";
value = header;
} else {
key = header.substring(0,ptr);
value = header.substring(ptr+2);
}
}
KVP(String key, String value) {
this.key = key;
this.value = value;
}
KVP(Map.Entry<String, List<String>> entry) {
key = entry.getKey();
StringBuffer sb = new StringBuffer();
for (String s : entry.getValue()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(s);
}
value = sb.toString();
}
@Override
public String toString() {
if (key.length() == 0) {
return value;
} else {
return new StringBuffer(key).append(": ").append(value).toString();
}
}
String key() {
return key;
}
String value() {
return value;
}
}
// Instance
Map<String, List<String>> headerFields, requestProperties;
List<KVP> orderedHeaderFields;
int contentLength;
String contentType, contentEncoding;
long expiration, date, lastModified;
InputStream responseData;
/**
* Initialize with the URL.
*/
AbstractConnection(URL url) {
super(url);
initialize();
}
/**
* Initialize all fields to their default states.
*/
void initialize() {
//
// initialize inherited fields
//
connected = false;
chunkLength = -1;
fixedContentLength = -1;
method = "GET";
responseCode = -1;
responseMessage = null;
allowUserInteraction = getDefaultAllowUserInteraction();
doInput = true;
doOutput = false;
ifModifiedSince = 0;
useCaches = getDefaultUseCaches();
//
// initialize inherited fields
//
orderedHeaderFields = null;
headerFields = null;
requestProperties = new HashMap<String, List<String>>();
responseData = null;
contentLength = 0;
contentType = null;
contentEncoding = null;
expiration = 0;
date = 0;
lastModified = 0;
}
/**
* Perform the request/response exchange; implemented by subclasses.
*
* Subclasses must set all the protected fields, including the responseData InputStream, which is returned by both
* getInputStream and getErrorStream, as appropriate according to the value of responseCode.
*/
abstract void getResponse() throws IOException;
// Overrides for HttpURLConnection
@Override
public int getContentLength() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return contentLength;
}
@Override
public String getContentType() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return contentType;
}
@Override
public String getContentEncoding() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return contentEncoding;
}
@Override
public long getExpiration() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return expiration;
}
@Override
public long getDate() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return date;
}
@Override
public long getLastModified() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return lastModified;
}
@Override
public Map<String, List<String>> getHeaderFields() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
return headerFields;
}
@Override
public String getHeaderField(String header) {
for (Map.Entry<String, List<String>> entry : getHeaderFields().entrySet()) {
if (header.equalsIgnoreCase(entry.getKey())) {
return entry.getValue().get(0);
}
}
return null;
}
@Override
public int getHeaderFieldInt(String header, int def) {
String s = getHeaderField(header);
if (s != null) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
}
return def;
}
@Override
public long getHeaderFieldDate(String header, long def) {
String s = getHeaderField(header);
if (s != null) {
try {
return RFC822.valueOf(s);
} catch (IllegalArgumentException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
}
return def;
}
@Override
public String getHeaderFieldKey(int index) {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
if (orderedHeaderFields != null && orderedHeaderFields.size() > index) {
return orderedHeaderFields.get(index).key();
} else {
return null;
}
}
@Override
public String getHeaderField(int index) {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
if (orderedHeaderFields != null && orderedHeaderFields.size() > index) {
return orderedHeaderFields.get(index).value();
} else {
return null;
}
}
/**
* Closing the resulting stream will automatically reset this connection.
*/
@Override
public InputStream getInputStream() throws IOException {
getResponse();
if (responseCode == HTTP_OK) {
return responseData;
} else {
throw new IOException("Response error: " + responseCode);
}
}
/**
* Closing this stream will automatically reset this connection.
*/
@Override
public InputStream getErrorStream() {
try {
getResponse();
} catch (IOException e) {
Message.getLogger().warn(Message.getMessage(Message.ERROR_EXCEPTION), e);
}
if (responseCode == HTTP_OK) {
return null;
} else {
return responseData;
}
}
@Override
public int getResponseCode() throws IOException {
getResponse();
return responseCode;
}
@Override
public String getResponseMessage() throws IOException {
getResponse();
return responseMessage;
}
}