blob: c72884fc4744d46e644baef5aff8a5587acab973 [file] [log] [blame]
/*
* Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eike Stepper - initial API and implementation
*/
package org.eclipse.userstorage.internal;
import org.eclipse.userstorage.IStorage;
import org.eclipse.userstorage.internal.util.IOUtil;
import org.eclipse.userstorage.internal.util.JSONUtil;
import org.eclipse.userstorage.internal.util.ProxyUtil;
import org.eclipse.userstorage.internal.util.StringUtil;
import org.eclipse.userstorage.spi.ICredentialsProvider;
import org.eclipse.userstorage.util.ConflictException;
import org.eclipse.userstorage.util.ProtocolException;
import org.eclipse.core.runtime.OperationCanceledException;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.CookieStore;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author Eike Stepper
*/
public class Session implements Headers, Codes
{
public static final String APPLICATION_JSON = "application/json";
public static final String USER_AGENT_ID = "uss/1.0.0"; // "uss/1.0.0" or use bundle version if running in OSGi...
public static final String USER_AGENT_PROPERTY = Session.class.getName() + ".userAgent";
private static final int AUTHORIZATION_ATTEMPTS = 3;
private static final boolean DEBUG = Boolean.getBoolean(Session.class.getName() + ".debug");
/**
* It's important that the cookie store is <b>not</b> declared as a static field!
* Otherwise session cookies could be left over even if the sessionID is set to null and
* re-authentication would re-send old session cookies, which would make the server reply with "401: CSRF Validation Failed".
*/
@SuppressWarnings("restriction")
private final CookieStore cookieStore = new org.apache.http.impl.client.BasicCookieStore();
private final Storage storage;
private final Executor executor;
private String sessionID;
private String csrfToken;
public Session(Storage storage)
{
this.storage = storage;
executor = Executor.newInstance().cookieStore(cookieStore);
}
public IStorage getStorage()
{
return storage;
}
public InputStream retrieveBlob(String appToken, String key, final Map<String, String> properties, final boolean useETag,
ICredentialsProvider credentialsProvider) throws IOException
{
URI uri = StringUtil.newURI(storage.getServiceURI(), "api/blob/" + appToken + "/" + key);
return new RequestTemplate<InputStream>(uri)
{
@Override
protected Request prepareRequest() throws IOException
{
Request request = configureRequest(Request.Get(uri), uri);
if (useETag)
{
String eTag = properties.get(Blob.ETAG);
if (!StringUtil.isEmpty(eTag))
{
request.setHeader(IF_NON_MATCH, "\"" + eTag + "\"");
}
}
return request;
}
@Override
protected InputStream handleResponse(HttpResponse response, HttpEntity responseEntity) throws IOException
{
int statusCode = getStatusCode("GET", uri, response, OK, NOT_MODIFIED, NOT_FOUND);
String eTag = getETag(response);
if (eTag != null)
{
properties.put(Blob.ETAG, eTag);
}
if (statusCode == OK)
{
return JSONUtil.decode(responseEntity.getContent(), properties, "value");
}
if (statusCode == NOT_MODIFIED)
{
return Blob.NOT_MODIFIED;
}
// NOT_FOUND
return null;
}
}.send(credentialsProvider);
}
public boolean updateBlob(String appToken, String key, final Map<String, String> properties, final InputStream in, ICredentialsProvider credentialsProvider)
throws IOException, ConflictException
{
URI uri = StringUtil.newURI(storage.getServiceURI(), "api/blob/" + appToken + "/" + key);
return new RequestTemplate<Boolean>(uri)
{
@Override
protected Request prepareRequest() throws IOException
{
Request request = configureRequest(Request.Put(uri), uri);
String eTag = properties.get(Blob.ETAG);
if (!StringUtil.isEmpty(eTag))
{
request.setHeader(IF_MATCH, "\"" + eTag + "\"");
}
body = JSONUtil.encode(Blob.NO_PROPERTIES, "value", in);
request.bodyStream(body);
return request;
}
@Override
protected Boolean handleResponse(HttpResponse response, HttpEntity responseEntity) throws IOException
{
int statusCode = getStatusCode("PUT", uri, response, OK, CREATED, CONFLICT);
String eTag = getETag(response);
if (statusCode == CONFLICT)
{
StatusLine statusLine = response.getStatusLine();
throw new ConflictException("PUT", uri, getProtocolVersion(statusLine), statusLine.getReasonPhrase(), eTag);
}
if (eTag == null)
{
throw new ProtocolException("PUT", uri, getProtocolVersion(response.getStatusLine()), BAD_RESPONSE, "Bad Response : No ETag");
}
properties.put(Blob.ETAG, eTag);
return statusCode == CREATED;
}
}.send(credentialsProvider);
}
private static String getETag(HttpResponse response)
{
Header[] headers = response.getHeaders(Headers.ETAG);
if (headers != null && headers.length != 0)
{
String eTag = headers[0].getValue();
// Remove the quotes.
eTag = eTag.substring(1, eTag.length() - 1);
return eTag;
}
return null;
}
/**
* @author Eike Stepper
*/
private abstract class RequestTemplate<T>
{
protected final URI uri;
protected InputStream body;
public RequestTemplate(URI uri)
{
this.uri = uri;
}
public final synchronized T send(ICredentialsProvider credentialsProvider) throws IOException
{
int authorizationAttempts = AUTHORIZATION_ATTEMPTS;
for (;;)
{
body = null;
HttpEntity responseEntity = null;
try
{
authenticate(credentialsProvider);
Request request = prepareRequest();
HttpResponse response = sendRequest(request, uri);
IOUtil.closeSilent(body);
body = null;
responseEntity = response.getEntity();
return handleResponse(response, responseEntity);
}
catch (IOException ex)
{
if (DEBUG && responseEntity != null)
{
responseEntity.writeTo(System.out);
}
if (ex instanceof ProtocolException)
{
ProtocolException protocolException = (ProtocolException)ex;
if (protocolException.getStatusCode() == AUTHORIZATION_REQUIRED && --authorizationAttempts > 0)
{
continue;
}
}
throw ex;
}
finally
{
IOUtil.closeSilent(body);
body = null;
}
}
}
protected final void authenticate(ICredentialsProvider credentialsProvider) throws IOException
{
if (sessionID == null)
{
// Make sure no old session session cookies are sent.
// Otherwise the server would reply with "401: CSRF Validation Failed".
cookieStore.clear();
InputStream body = null;
HttpEntity responseEntity = null;
try
{
Credentials credentials = getCredentials(credentialsProvider);
Map<String, String> arguments = new LinkedHashMap<String, String>();
arguments.put("username", credentials.getUsername());
arguments.put("password", credentials.getPassword());
URI uri = StringUtil.newURI(storage.getServiceURI(), "api/user/login");
Request request = configureRequest(Request.Post(uri), uri);
body = JSONUtil.encode(arguments, null, null);
request.bodyStream(body);
HttpResponse response = sendRequest(request, uri);
responseEntity = response.getEntity();
getStatusCode("POST", uri, response, OK);
Map<String, String> properties = new LinkedHashMap<String, String>();
IOUtil.closeSilent(JSONUtil.decode(responseEntity.getContent(), properties, null));
sessionID = properties.get("sessid");
if (sessionID == null)
{
throw new IOException("No session ID");
}
csrfToken = properties.get("token");
}
catch (IOException ex)
{
sessionID = null;
csrfToken = null;
if (DEBUG && responseEntity != null)
{
responseEntity.writeTo(System.out);
}
throw ex;
}
finally
{
IOUtil.closeSilent(body);
}
}
acquireCSRFToken();
}
protected final void acquireCSRFToken() throws IOException
{
if (csrfToken == null)
{
HttpEntity responseEntity = null;
try
{
URI uri = StringUtil.newURI(storage.getServiceURI(), "api/user/token");
Request request = configureRequest(Request.Post(uri), uri);
HttpResponse response = sendRequest(request, uri);
responseEntity = response.getEntity();
Map<String, String> properties = new LinkedHashMap<String, String>();
IOUtil.closeSilent(JSONUtil.decode(responseEntity.getContent(), properties, null));
csrfToken = properties.get("token");
if (csrfToken == null)
{
throw new IOException("No CSRF token");
}
}
catch (IOException ex)
{
csrfToken = null;
if (DEBUG && responseEntity != null)
{
responseEntity.writeTo(System.out);
}
throw ex;
}
}
}
protected final Request configureRequest(Request request, URI uri)
{
if (csrfToken != null)
{
request.setHeader(CSRF_TOKEN, csrfToken);
}
String userAgent = System.getProperty(USER_AGENT_PROPERTY, USER_AGENT_ID);
return request //
.viaProxy(ProxyUtil.getProxyHost(uri)) //
.staleConnectionCheck(true) //
.connectTimeout(StorageProperties.getProperty(StorageProperties.CONNECT_TIMEOUT, 3000)) //
.socketTimeout(StorageProperties.getProperty(StorageProperties.SOCKET_TIMEOUT, 10000)) //
.addHeader(USER_AGENT, userAgent) //
.addHeader(CONTENT_TYPE, APPLICATION_JSON) //
.addHeader(ACCEPT, APPLICATION_JSON);
}
protected final HttpResponse sendRequest(Request request, URI uri) throws IOException
{
long start = 0;
if (DEBUG)
{
try
{
start = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
builder.append(request);
builder.append('\n');
Field f1 = Request.class.getDeclaredField("request");
f1.setAccessible(true);
Object o1 = f1.get(request);
Field f2 = Class.forName("org.apache.http.message.AbstractHttpMessage").getDeclaredField("headergroup");
f2.setAccessible(true);
Object o2 = f2.get(o1);
Field f3 = o2.getClass().getDeclaredField("headers");
f3.setAccessible(true);
@SuppressWarnings("unchecked")
List<Header> o3 = (List<Header>)f3.get(o2);
for (Header header : o3)
{
builder.append(" ");
builder.append(header);
builder.append('\n');
}
System.out.print(builder);
}
catch (Throwable ex)
{
ex.printStackTrace();
}
}
Response result = ProxyUtil.proxyAuthentication(executor, uri).execute(request);
HttpResponse response = result.returnResponse();
if (DEBUG)
{
try
{
StringBuilder builder = new StringBuilder();
builder.append(response.getStatusLine());
builder.append('\n');
for (Header header : response.getAllHeaders())
{
builder.append(" ");
builder.append(header);
builder.append('\n');
}
if (start != 0)
{
long millis = System.currentTimeMillis() - start;
builder.append("Took: ");
builder.append(millis);
builder.append(" millis");
builder.append('\n');
}
builder.append('\n');
System.out.print(builder);
}
catch (Throwable ex)
{
ex.printStackTrace();
}
}
return response;
}
protected final int getStatusCode(String method, URI uri, HttpResponse response, int... expectedStatusCodes) throws ProtocolException
{
StatusLine statusLine = response.getStatusLine();
if (statusLine == null)
{
throw new ProtocolException(method, uri, getProtocolVersion(statusLine), BAD_RESPONSE, "Bad Response : No status line returned");
}
int statusCode = statusLine.getStatusCode();
if (statusCode == AUTHORIZATION_REQUIRED)
{
sessionID = null;
csrfToken = null;
}
for (int i = 0; i < expectedStatusCodes.length; i++)
{
int expectedStatusCode = expectedStatusCodes[i];
if (statusCode == expectedStatusCode)
{
return statusCode;
}
}
throw new ProtocolException(method, uri, getProtocolVersion(statusLine), statusCode, statusLine.getReasonPhrase());
}
protected final String getProtocolVersion(StatusLine statusLine)
{
if (statusLine != null)
{
ProtocolVersion protocolVersion = statusLine.getProtocolVersion();
if (protocolVersion != null)
{
return protocolVersion.toString();
}
}
return "HTTP";
}
protected final Credentials getCredentials(ICredentialsProvider credentialsProvider) throws OperationCanceledException
{
Credentials credentials = storage.getCredentials();
if (credentials == null)
{
if (credentialsProvider != null)
{
credentials = credentialsProvider.provideCredentials(storage);
if (credentials != null)
{
storage.setCredentials(credentials);
}
}
}
if (credentials == null)
{
throw new OperationCanceledException("No credentials provided");
}
return credentials;
}
protected abstract Request prepareRequest() throws IOException;
protected abstract T handleResponse(HttpResponse response, HttpEntity responseEntity) throws IOException;
}
}
/**
* @author Eike Stepper
*/
interface Headers
{
public static final String USER_AGENT = "User-Agent";
public static final String CONTENT_TYPE = "Content-Type";
public static final String ACCEPT = "Accept";
public static final String CSRF_TOKEN = "X-CSRF-Token";
public static final String ETAG = "ETag";
public static final String IF_MATCH = "If-Match";
public static final String IF_NON_MATCH = "If-Non-Match";
}
/**
* @author Eike Stepper
*/
interface Codes
{
public static final int OK = 200;
public static final int CREATED = 201;
public static final int NO_CONTENT = 204;
public static final int NOT_MODIFIED = 304;
public static final int BAD_REQUEST = 400;
public static final int AUTHORIZATION_REQUIRED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int NOT_ACCEPTABLE = 406;
public static final int CONFLICT = 409;
public static final int BAD_RESPONSE = 444;
}