blob: 3d9d83193471abbfd6f94db181a22d7cfd717202 [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.IStorageService;
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.Credentials;
import org.eclipse.userstorage.spi.ICredentialsProvider;
import org.eclipse.userstorage.util.ConflictException;
import org.eclipse.userstorage.util.NotFoundException;
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.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
/**
* @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";
public static final String NOT_FOUND_ETAG = "not_found";
private static final int AUTHENTICATION_ATTEMPTS = 3;
private static final boolean DEBUG = Boolean.getBoolean("org.eclipse.userstorage.session.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 Executor executor = Executor.newInstance().cookieStore(cookieStore);
private final StorageService service;
private String sessionID;
private String csrfToken;
public Session(StorageService service)
{
this.service = service;
}
public IStorageService getService()
{
return service;
}
public void reset()
{
sessionID = null;
csrfToken = null;
// Make sure no old session cookies are sent.
// Otherwise the server would reply with "401: CSRF Validation Failed".
cookieStore.clear();
}
public Map<String, Map<String, Object>> retrieveProperties(final String applicationToken, ICredentialsProvider credentialsProvider, int pageSize, int page)
throws IOException
{
if (pageSize < 1 || pageSize > 100)
{
throw new IllegalArgumentException("pageSize=" + pageSize);
}
if (page < 1)
{
throw new IllegalArgumentException("page=" + page);
}
URI uri = StringUtil.newURI(service.getServiceURI(), "api/blob/" + applicationToken + "?pagesize=" + pageSize + "&page=" + page);
return new RequestTemplate<Map<String, Map<String, Object>>>(uri)
{
@Override
protected Request prepareRequest() throws IOException
{
return configureRequest(Request.Get(uri), uri);
}
@Override
protected Map<String, Map<String, Object>> handleResponse(HttpResponse response, HttpEntity responseEntity) throws IOException
{
getStatusCode("GET", uri, response, OK);
List<Object> array = JSONUtil.parse(responseEntity.getContent(), null);
Map<String, Map<String, Object>> result = new HashMap<String, Map<String, Object>>();
for (Object element : array)
{
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>)element;
Object appToken = map.remove("application_token");
if (!applicationToken.equals(appToken))
{
StatusLine statusLine = response.getStatusLine();
String protocolVersion = statusLine == null ? "HTTP" : getProtocolVersion(statusLine);
throw new ProtocolException("GET", uri, protocolVersion, BAD_RESPONSE, "Bad Response : Wrong application token: " + appToken);
}
map.remove("url");
String key = (String)map.remove("key");
result.put(key, map);
}
return result;
}
}.send(credentialsProvider);
}
public InputStream retrieveBlob(String applicationToken, String key, final Map<String, String> properties, final boolean useETag,
ICredentialsProvider credentialsProvider) throws IOException
{
URI uri = StringUtil.newURI(service.getServiceURI(), "api/blob/" + applicationToken + "/" + 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_NONE_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)
{
Map<String, Object> object = JSONUtil.parse(responseEntity.getContent(), "value");
InputStream stream = (InputStream)object.remove("value");
for (Map.Entry<String, Object> entry : object.entrySet())
{
Object value = entry.getValue();
properties.put(entry.getKey(), String.valueOf(value));
}
return stream;
}
if (statusCode == NOT_MODIFIED)
{
return Blob.NOT_MODIFIED;
}
// Blob wasn't found.
properties.clear();
StatusLine statusLine = response.getStatusLine();
throw new NotFoundException("GET", uri, getProtocolVersion(statusLine), statusLine.getReasonPhrase());
}
}.send(credentialsProvider);
}
public boolean updateBlob(String applicationToken, String key, final Map<String, String> properties, final InputStream in,
ICredentialsProvider credentialsProvider) throws IOException, ConflictException
{
URI uri = StringUtil.newURI(service.getServiceURI(), "api/blob/" + applicationToken + "/" + 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.build(Collections.singletonMap("value", in));
request.bodyStream(body);
return request;
}
@Override
protected Boolean handleResponse(HttpResponse response, HttpEntity responseEntity) throws IOException
{
String eTag = getETag(response);
try
{
int statusCode = getStatusCode("PUT", uri, response, OK, CREATED, CONFLICT);
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;
}
catch (ProtocolException ex)
{
int xxx;
if (ex.getStatusCode() == BAD_REQUEST && ex.getReasonPhrase().contains("The resource already exist"))
{
StatusLine statusLine = response.getStatusLine();
throw new ConflictException("PUT", uri, getProtocolVersion(statusLine), "The resource already exists.", eTag);
}
throw ex;
}
}
}.send(credentialsProvider);
}
public boolean deleteBlob(String applicationToken, String key, final Map<String, String> properties, ICredentialsProvider credentialsProvider)
throws IOException, ConflictException
{
URI uri = StringUtil.newURI(service.getServiceURI(), "api/blob/" + applicationToken + "/" + key);
boolean deleted = new RequestTemplate<Boolean>(uri)
{
@Override
protected Request prepareRequest() throws IOException
{
Request request = configureRequest(Request.Delete(uri), uri);
String eTag = properties.get(Blob.ETAG);
if (!StringUtil.isEmpty(eTag))
{
request.setHeader(IF_MATCH, "\"" + eTag + "\"");
}
return request;
}
@Override
protected Boolean handleResponse(HttpResponse response, HttpEntity responseEntity) throws IOException
{
int statusCode = getStatusCode("DELETE", uri, response, NO_CONTENT, CONFLICT, NOT_FOUND);
String eTag = getETag(response);
if (statusCode == CONFLICT)
{
StatusLine statusLine = response.getStatusLine();
throw new ConflictException("DELETE", uri, getProtocolVersion(statusLine), statusLine.getReasonPhrase(), eTag);
}
properties.put(Blob.ETAG, "<deleted_etag>");
return statusCode == NO_CONTENT;
}
}.send(credentialsProvider);
return deleted;
}
private void debugResponseEntity(HttpEntity responseEntity) throws IOException
{
if (DEBUG && responseEntity != null)
{
responseEntity.writeTo(System.out);
System.out.println();
System.out.println();
}
}
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 authenticationAttempts = AUTHENTICATION_ATTEMPTS;
boolean reauthentication = false;
Credentials credentials = service.getCredentials();
if (credentials != null)
{
if (StringUtil.isEmpty(credentials.getUsername()) || StringUtil.isEmpty(credentials.getPassword()))
{
credentials = null;
}
else
{
// The first attempt will be made with the stored credentials, so increase authenticationAttempts to 4 to prompt 3 times.
++authenticationAttempts;
}
}
boolean authenticated = false;
for (;;)
{
body = null;
HttpEntity responseEntity = null;
try
{
authenticated = false;
authenticate(credentials, credentialsProvider, reauthentication);
authenticated = true;
Request request = prepareRequest();
HttpResponse response = sendRequest(request, uri);
IOUtil.closeSilent(body);
body = null;
responseEntity = response.getEntity();
return handleResponse(response, responseEntity);
}
catch (IOException ex)
{
debugResponseEntity(responseEntity);
if (ex instanceof ProtocolException)
{
ProtocolException protocolException = (ProtocolException)ex;
if (protocolException.getStatusCode() == AUTHORIZATION_REQUIRED)
{
if (authenticated)
{
// This means that the initial authenticate() call was skipped because we already have a session,
// but this session is no longer valid on the server.
// So call reset() to force a full reauthentication with initial credentials.
reset();
continue;
}
if (--authenticationAttempts > 0)
{
reauthentication = true;
credentials = null;
continue;
}
}
}
throw ex;
}
finally
{
IOUtil.closeSilent(body);
body = null;
}
}
}
protected final void authenticate(Credentials credentials, ICredentialsProvider credentialsProvider, boolean reauthentication) throws IOException
{
if (sessionID == null)
{
reset();
InputStream body = null;
HttpEntity responseEntity = null;
try
{
credentials = getCredentials(credentials, credentialsProvider, reauthentication);
Map<String, Object> arguments = new LinkedHashMap<String, Object>();
arguments.put("username", credentials.getUsername());
arguments.put("password", credentials.getPassword());
URI uri = StringUtil.newURI(service.getServiceURI(), "api/user/login");
Request request = configureRequest(Request.Post(uri), uri);
body = JSONUtil.build(arguments);
request.bodyStream(body);
HttpResponse response = sendRequest(request, uri);
responseEntity = response.getEntity();
getStatusCode("POST", uri, response, OK);
Map<String, Object> object = JSONUtil.parse(responseEntity.getContent(), null);
sessionID = (String)object.get("sessid");
if (sessionID == null)
{
throw new IOException("No session ID");
}
csrfToken = (String)object.get("token");
}
catch (IOException ex)
{
sessionID = null;
csrfToken = null;
debugResponseEntity(responseEntity);
throw ex;
}
finally
{
IOUtil.closeSilent(body);
}
}
acquireCSRFToken();
}
protected final void acquireCSRFToken() throws IOException
{
if (csrfToken == null)
{
HttpEntity responseEntity = null;
try
{
URI uri = StringUtil.newURI(service.getServiceURI(), "api/user/token");
Request request = configureRequest(Request.Post(uri), uri);
HttpResponse response = sendRequest(request, uri);
responseEntity = response.getEntity();
Map<String, Object> object = JSONUtil.parse(responseEntity.getContent(), null);
csrfToken = (String)object.get("token");
if (csrfToken == null)
{
throw new IOException("No CSRF token");
}
}
catch (IOException ex)
{
csrfToken = null;
debugResponseEntity(responseEntity);
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(Credentials credentials, ICredentialsProvider credentialsProvider, boolean reauthentication)
throws OperationCanceledException
{
if (credentials == null)
{
if (credentialsProvider != null)
{
Semaphore semaphore = service.getAuthenticationSemaphore();
try
{
semaphore.acquire();
credentials = credentialsProvider.provideCredentials(service, reauthentication);
}
catch (InterruptedException ex)
{
//$FALL-THROUGH$
}
finally
{
semaphore.release();
}
if (credentials != null)
{
service.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_NONE_MATCH = "If-None-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;
}