/*
GNU GENERAL LICENSE
Copyright (C) 2006 The Lobo Project. Copyright (C) 2014 - 2017 Lobo Evolution
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
verion 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General License for more details.
You should have received a copy of the GNU General Public
along with this program. If not, see <http://www.gnu.org/licenses/>.
Contact info: lobochief@users.sourceforge.net; ivan.difrancesco@yahoo.it
*/
package org.lobobrowser.http;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
/**
* <p>
* Represents a user's "session" on the web. Think of it as a "tab" in a tabbed
* web browser. It may access multiple web sites during one "session", but
* remembers the cookies for all of them.
* </p>
*
* <p>
* Sessions also contain all the information needed to keep track of the
* progress of a request/response cycle (including uploading and downloading of
* data). The values are reset at the beginning of a request and maintain their
* values until the next request is made. The property change events are fired
* on whatever thread called the execute method -- most likely a background
* thread (not the EDT). Be careful of that when binding GUI widgets to these
* properties.
* </p>
*
* @author rbair
*/
public class Session extends AbstractBean {
/**
* Specifies a value to use for security, either Low, Medium, or High. This
* is currently used for determining how to treat SSL connections.
*
* @see #setSslSecurityLevel
*/
public enum SecurityLevel {
/** The Low. */
Low,
/** The Medium. */
Medium,
/** The High. */
High
};
/** The ssl security. */
private SecurityLevel sslSecurity;
/** The handler. */
private SecurityHandler handler;
/**
* Keeps track of the state of the Session when performing a
* request/response cycle.
*/
private State state = State.READY;
/**
* Keeps track of the total number of bytes that are to be sent or
* receieved. This is reset when DONE, or switching from upload to download.
* This is only used if the content-length is known, otherwise it is set to
* -1.
*/
private long totalBytes = -1;
/**
* Keeps track of the total number of bytes transfered upstream or
* downstream. This is reset when DONE, or when switching from upload to
* download. This is used whether or not the content-length is known
*/
private long bytesSoFar = 0;
/**
* Creates a new Session. Automatically installs the {@link CookieManager}.
*/
public Session() {
this(true);
}
/**
* Creates a new Session. If <code>installCookieManager</code> is true, then
* the CookieManager is installed automatically. Otherwise, the
* <code>CookieManager</code> will not be installed, allowing you to use
* some other cookie manager.
*
* @param installCookieManager
*/
public Session(boolean installCookieManager) {
setSslSecurityLevel(SecurityLevel.Medium);
// register a default security handler
setMediumSecurityHandler(new DefaultSecurityHandler());
if (installCookieManager) {
// TODO CookieManager.install();
}
}
/** Sets the ssl security level.
*
* @param level
* the new ssl security level
*/
public void setSslSecurityLevel(SecurityLevel level) {
SecurityLevel old = getSslSecurityLevel();
sslSecurity = level;
firePropertyChange("sslSecurityLevel", old, getSslSecurityLevel());
}
/** Gets the ssl security level.
*
* @return the ssl security level
*/
public final SecurityLevel getSslSecurityLevel() {
return sslSecurity;
}
/** Sets the medium security handler.
*
* @param h
* the new medium security handler
*/
void setMediumSecurityHandler(SecurityHandler h) {
SecurityHandler old = getMediumSecurityHandler();
this.handler = h;
firePropertyChange("mediumSecurityHandler", old,
getMediumSecurityHandler());
}
/** Gets the medium security handler.
*
* @return the medium security handler
*/
SecurityHandler getMediumSecurityHandler() {
return handler;
}
private SSLSocketFactory createSocketFactory(String host) {
try {
TrustManager tm = null;
Session.SecurityLevel level = getSslSecurityLevel();
if (level == Session.SecurityLevel.Low) {
tm = new LowSecurityX509TrustManager(null);
} else if (level == Session.SecurityLevel.Medium) {
tm = new MediumSecurityX509TrustManager(host,
getMediumSecurityHandler(), null);
} else {
tm = new HighSecurityX509TrustManager(null);
}
SSLContext context = SSLContext.getInstance("SSL");
context.init(null, new TrustManager[] { tm }, null);
return context.getSocketFactory();
} catch (Exception e) {
throw new AssertionError(e);
}
}
/** Gets the keeps track of the total number of bytes that are to be sent
* or receieved.
*
* @return the keeps track of the total number of bytes that are to be sent
* or receieved
*/
public final long getTotalBytes() {
return totalBytes;
}
/** Sets the keeps track of the total number of bytes that are to be sent
* or receieved.
*
* @param bytes
* the new keeps track of the total number of bytes that are to
* be sent or receieved
*/
private void setTotalBytes(long bytes) {
long old = totalBytes;
float oldProgress = getProgress();
firePropertyChange("totalBytes", old, this.totalBytes = bytes);
firePropertyChange("progress", oldProgress, getProgress());
}
/** Gets the keeps track of the total number of bytes transfered upstream
* or downstream.
*
* @return the keeps track of the total number of bytes transfered upstream
* or downstream
*/
public final long getBytesSoFar() {
return bytesSoFar;
}
/** Sets the keeps track of the total number of bytes transfered upstream
* or downstream.
*
* @param bytes
* the new keeps track of the total number of bytes transfered
* upstream or downstream
*/
private void setBytesSoFar(long bytes) {
long old = this.bytesSoFar;
float oldProgress = getProgress();
firePropertyChange("bytesSoFar", old, this.bytesSoFar = bytes);
firePropertyChange("progress", oldProgress, getProgress());
}
/** Gets the progress.
*
* @return the progress
*/
public final float getProgress() {
if (totalBytes <= 0) {
return -1f;
}
float total = totalBytes;
float num = bytesSoFar;
return num / total;
}
/** Gets the keeps track of the state of the Session when performing a
* request/response cycle.
*
* @return the keeps track of the state of the Session when performing a
* request/response cycle
*/
public final State getState() {
return state;
}
/** Sets the keeps track of the state of the Session when performing a
* request/response cycle.
*
* @param s
* the new keeps track of the state of the Session when
* performing a request/response cycle
*/
protected void setState(State s) {
State old = this.state;
firePropertyChange("state", old, this.state = s);
}
/**
* Constructs and executes a {@link Request} using the Method.GET method.
* This method blocks.
*
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response get(String url) throws Exception {
return execute(Method.GET, url);
}
/**
* Constructs and executes a {@link Request} using the Method.GET method.
* This method blocks.
*
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @param params
* The params to include in the request. This may be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response get(String url, Parameter... params)
throws Exception {
return execute(Method.GET, url, params);
}
/**
* Constructs and executes a {@link Request} using the Method.POST method.
* This method blocks.
*
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response post(String url) throws Exception {
return execute(Method.POST, url);
}
/**
* Constructs and executes a {@link Request} using the Method.POST method.
* This method blocks.
*
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @param params
* The params to include in the request. This may be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response post(String url, Parameter... params)
throws Exception {
return execute(Method.POST, url, params);
}
/**
* Constructs and executes a {@link Request} using the Method.PUT method.
* This method blocks.
*
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response put(String url) throws Exception {
return execute(Method.PUT, url);
}
/**
* Constructs and executes a {@link Request} using the Method.PUT method.
* This method blocks.
*
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @param params
* The params to include in the request. This may be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response put(String url, Parameter... params)
throws Exception {
return execute(Method.PUT, url, params);
}
/**
* Constructs and executes a {@link Request}, and returns the
* {@link Response}. This method blocks. The given <code>method</code>,
* <code>url</code> will be used to construct the <code>Request</code>. All
* other <code>Request</code> properties are left in their default state.
*
* @param method
* The HTTP {@link Method} to use. This must not be null.
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response execute(String method, String url) throws Exception {
return execute(method, url, new Parameter[0]);
}
/**
* Constructs and executes a {@link Request}, and returns the
* {@link Response}. This method blocks. The given <code>method</code>,
* <code>url</code>, and <code>params</code> will be used to construct the
* <code>Request</code>. All other <code>Request</code> properties are left
* in their default state.
*
* @param method
* The HTTP {@link Method} to use. This must not be null.
* @param url
* The url to hit. This url may contain a query string (ie:
* params). The url cannot be null.
* @param params
* The params to include in the request. This may be null.
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public final Response execute(String method, String url,
Parameter... params) throws Exception {
if (method == null) {
throw new NullPointerException("method cannot be null");
}
if (url == null) {
throw new NullPointerException("url cannot be null");
}
// create and handle the request
Request req = new Request();
req.setParameters(params);
req.setMethod(method);
req.setUrl(url); // make sure the URL is set after the params, or else
// if the url had any params, they will be hosed!
return execute(req);
}
/**
* Executes the given {@link Request}, and returns a {@link Response}. This
* method blocks.
*
* @return the {@link Response} to the {@link Request}.
* @throws Exception
* if an error occurs while creating or executing the
* <code>Request</code> on the client machine. That is, if
* normal http errors occur, they will not throw an exception
* (such as BAD_GATEWAY, etc).
*/
public Response execute(Request req) throws Exception {
try {
// initialize the state and such
setTotalBytes(-1);
setBytesSoFar(0);
setState(State.CONNECTING);
// 0. Create the URL
StringBuffer surl = new StringBuffer(req.getUrl());
if (surl.length() == 0) {
setState(State.FAILED);
throw new IllegalStateException(
"Cannot excecute a request that has no URL specified");
}
char delim = '?';
for (Parameter p : req.getParameters()) {
surl.append(delim);
delim = '&';
String name = URLEncoder.encode(p.getName(), "UTF-8");
String value = URLEncoder.encode(p.getValue(), "UTF-8");
surl.append(name + "=" + value);
}
// 1. Create the HttpURLConnection
URL url = createURL(surl.toString());
URLConnection conn = url.openConnection();
if (!(conn instanceof HttpURLConnection)) {
setState(State.FAILED);
throw new IllegalStateException(
"Must be an HTTP or HTTPS based URL");
}
HttpURLConnection http = (HttpURLConnection) conn;
// 2. Configure the connection
http.setRequestMethod(req.getMethod());
http.setInstanceFollowRedirects(req.getFollowRedirects());
// TODO support chunked streaming?
// http.setChunkedStreamingMode(req.getChunkSize() > 0 ?
// req.getChunkSize() : -1);
// TODO support connection timeout? (probably a good idea)
// http.setConnectTimeout(req.getConnectionTimeout());
// TODO fixed length streaming?
// http.setFixedLengthStreamingMode(contentLength);
for (Header h : req.getHeaders()) {
http.setRequestProperty(h.getName(), h.getValue());
}
// 3. If I supported a cache, this is where I'd configure it!
// 4. Configure the request parameters
if (http instanceof HttpsURLConnection) {
HttpsURLConnection https = (HttpsURLConnection) http;
// set the ssl socket factory such that it respects the security
// levels
https.setSSLSocketFactory(createSocketFactory(url.getHost()));
}
// If the content-length has been specified, then use it
// otherwise I won't know the content length until it is too late
long contentLength = -1;
Header contentLengthHeader = req.getHeader("Content-Length");
if (contentLengthHeader != null) {
try {
contentLength = Long
.parseLong(contentLengthHeader.getValue().trim());
} catch (NumberFormatException ex) {
// unexpected, set contentlength to -1
contentLength = -1;
}
}
setTotalBytes(contentLength);
// 5. Set the request body, if any.
setState(State.SENDING);
OutputStream out = null;
InputStream body = req.getBody();
if (body != null) {
try {
http.setDoOutput(true);
out = http.getOutputStream();
byte[] buffer = new byte[8096];
int length = -1;
while ((length = body.read(buffer)) != -1) {
out.write(buffer, 0, length);
setBytesSoFar(bytesSoFar + length);
}
} catch (Exception e) {
setState(State.FAILED);
throw e;
} finally {
if (out != null) {
out.close();
}
body.close();
}
}
// 6. Get the response
// Read the response headers
// TODO Content-Encoding might not be in this set of headers. Need
// to test.
setState(State.SENT);
http.connect();
setBytesSoFar(0);
setTotalBytes(http.getContentLength());
setState(State.RECEIVING);
Set<Header> headers = new HashSet<Header>();
Header contentType = null;
for (Map.Entry<String, List<String>> entry : http.getHeaderFields()
.entrySet()) {
String headerKey = entry.getKey();
String headerValue = http.getHeaderField(headerKey);
if (headerKey == null) {
continue;
}
List<String> values = entry.getValue();
Header.Element[] elements = new Header.Element[values.size()];
for (int j = 0; j < elements.length; j++) {
elements[j] = new Header.Element(
new Parameter(values.get(j), values.get(j)));
}
Header h = new Header(headerKey, headerValue, elements);
headers.add(h);
if ("Content-Type".equalsIgnoreCase(headerKey)) {
contentType = h;
}
}
// Read the response, possibly from the error stream. Automatically
// unzip the response if it was gzip encoded
byte[] responseBody = null;
StatusCode responseCode = StatusCode.INTERNAL_SERVER_ERROR;
InputStream responseStream = null;
try {
// connects and returns the stream
responseStream = http.getInputStream();
responseCode = StatusCode.valueOf(http.getResponseCode());
// if this is GZIP encoded, then wrap the input stream
String contentEncoding = http.getContentEncoding();
if ("gzip".equals(contentEncoding)) {
responseStream = new GZIPInputStream(responseStream);
}
responseBody = readFully(responseStream);
} catch (FileNotFoundException e) {
// check for an error stream
responseStream = http.getErrorStream();
responseBody = readFully(responseStream);
} catch (HttpRetryException e) {
// TODO not sure what to do on a retry exception
setState(State.FAILED);
return new Response(StatusCode.NOT_FOUND,
"HttpRetryException: " + e.getMessage(), null, null,
null, req.getUrl());
} catch (UnknownHostException e) {
setState(State.FAILED);
return new Response(StatusCode.NOT_FOUND, "Unknown host", null,
null, null, req.getUrl());
} catch (IOException ex) {
String msg = ex.getMessage();
if (msg.contains("Server returned HTTP response code:")) {
int startIndex = msg.indexOf("code: ") + 6;
String s = msg.substring(startIndex, startIndex + 3);
responseCode = StatusCode.valueOf(Integer.parseInt(s));
responseStream = http.getErrorStream();
responseBody = readFully(responseStream);
} else {
throw ex;
}
} finally {
if (responseStream != null) {
responseStream.close();
}
}
// figure out the "base url" from which relative urls would be
// computed
String foo = "foo";
URI uri = new URI(req.getUrl());
URI uu = uri.resolve(new URI(foo));
String baseUrl = uu.toString().substring(0,
uu.toString().length() - foo.length());
// learn what the content type is
String charset = null;
if (contentType != null) {
String tmp = contentType.getValue();
// find the ; following the content type (if there is one)
int index = tmp.indexOf(";");
if (index >= 0) {
index = tmp.indexOf("=", index + 1);
if (index > 0) {
charset = contentType.getValue().substring(index + 1);
}
}
}
// construct the response
Response response = new Response(responseCode,
http.getResponseMessage(), responseBody, charset, headers,
baseUrl);
// TODO
// 7. Disconnect (as it is unclear how to reuse the
// HttpURLConnection, for Session anyway)
// http.disconnect();
setState(State.DONE);
return response;
} catch (InterruptedException ex) {
setState(State.ABORTED);
throw ex;
} finally {
}
}
/**
* This method exists for the sake of testing. I can create a url while
* testing even without having internet access by overriding this method to
* return an HttpURLConnection subclass that doesn't actually connect to the
* internet. I can then fake out all the operations of the URL connection.
*
* This method is not to be overridden by any classes other than the test
* class.
*
* @param conn
* @return
* @throws java.io.IOException
*/
protected URL createURL(String surl) throws MalformedURLException {
return new URL(surl.toString());
}
private byte[] readFully(InputStream in) throws IOException {
if (in == null) {
return new byte[0];
}
ByteArrayOutputStream out = new ByteArrayOutputStream(8096);
byte[] buffer = new byte[8096];
int length = -1;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
setBytesSoFar(bytesSoFar + length);
}
in.close();
return out.toByteArray();
}
}