1
0
mirror of https://github.com/owncloud/android-library.git synced 2025-06-08 00:16:09 +00:00

468 lines
18 KiB
Java

/* ownCloud Android Library is available under MIT license
* Copyright (C) 2018 ownCloud GmbH.
* Copyright (C) 2012 Bartek Przybylski
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package com.owncloud.android.lib.common;
import android.accounts.AccountManager;
import android.accounts.AccountsException;
import android.content.Context;
import android.net.Uri;
import com.owncloud.android.lib.common.authentication.OwnCloudCredentials;
import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory;
import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials;
import com.owncloud.android.lib.common.http.HttpClient;
import com.owncloud.android.lib.common.http.HttpConstants;
import com.owncloud.android.lib.common.http.methods.HttpBaseMethod;
import com.owncloud.android.lib.common.network.RedirectionPath;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.common.utils.RandomUtils;
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import at.bitfire.dav4android.exception.HttpException;
import okhttp3.Cookie;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import static com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID;
public class OwnCloudClient extends HttpClient {
public static final String NEW_WEBDAV_FILES_PATH_4_0 = "/remote.php/dav/files/";
public static final String NEW_WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/";
public static final String STATUS_PATH = "/status.php";
public static final String FILES_WEB_PATH = "/index.php/apps/files";
private static final String TAG = OwnCloudClient.class.getSimpleName();
private static final int MAX_REDIRECTIONS_COUNT = 3;
private static final int MAX_REPEAT_COUNT_WITH_FRESH_CREDENTIALS = 1;
private static final String PARAM_PROTOCOL_VERSION = "http.protocol.version";
private static byte[] sExhaustBuffer = new byte[1024];
private static int sIntanceCounter = 0;
private OwnCloudCredentials mCredentials = null;
private int mInstanceNumber = 0;
private Uri mBaseUri;
private OwnCloudVersion mVersion = null;
/// next too attributes are a very ugly dependency, added to grant silent retry of OAuth token when needed ;
/// see #shouldInvalidateCredentials and #invalidateCredentials for more details
private Context mContext;
private OwnCloudAccount mAccount;
/**
* {@link @OwnCloudClientManager} holding a reference to this object and delivering it to callers; might be
* NULL
*/
private OwnCloudClientManager mOwnCloudClientManager = null;
private String mRedirectedLocation;
private boolean mFollowRedirects;
public OwnCloudClient(Uri baseUri) {
if (baseUri == null) {
throw new IllegalArgumentException("Parameter 'baseUri' cannot be NULL");
}
mBaseUri = baseUri;
mInstanceNumber = sIntanceCounter++;
Log_OC.d(TAG + " #" + mInstanceNumber, "Creating OwnCloudClient");
clearCredentials();
}
public void setCredentials(OwnCloudCredentials credentials) {
if (credentials != null) {
mCredentials = credentials;
mCredentials.applyTo(this);
} else {
clearCredentials();
}
}
public void clearCredentials() {
if (!(mCredentials instanceof OwnCloudAnonymousCredentials)) {
mCredentials = OwnCloudCredentialsFactory.getAnonymousCredentials();
}
mCredentials.applyTo(this);
}
public int executeHttpMethod (HttpBaseMethod method) throws Exception {
boolean repeatWithFreshCredentials;
int repeatCounter = 0;
int status;
do {
// Clean previous request id. This is a bit hacky but is the only way to add request headers in WebDAV
// methods by using Dav4Android
deleteHeaderForAllRequests(OC_X_REQUEST_ID);
// Header to allow tracing requests in apache and ownCloud logs
addHeaderForAllRequests(OC_X_REQUEST_ID,
RandomUtils.generateRandomString(RandomUtils.generateRandomInteger(20, 200)));
status = method.execute();
checkFirstRedirection(method);
if(mFollowRedirects && !isIdPRedirection()) {
status = followRedirection(method).getLastStatus();
}
repeatWithFreshCredentials = checkUnauthorizedAccess(status, repeatCounter);
if (repeatWithFreshCredentials) {
repeatCounter++;
}
} while (repeatWithFreshCredentials);
return status;
}
private void checkFirstRedirection(HttpBaseMethod method) {
final String location = method.getResponseHeader(HttpConstants.LOCATION_HEADER_LOWER);
if(location != null && !location.isEmpty()) {
mRedirectedLocation = location;
}
}
private int executeRedirectedHttpMethod (HttpBaseMethod method) throws Exception {
boolean repeatWithFreshCredentials;
int repeatCounter = 0;
int status;
do {
status = method.execute();
repeatWithFreshCredentials = checkUnauthorizedAccess(status, repeatCounter);
if (repeatWithFreshCredentials) {
repeatCounter++;
}
} while (repeatWithFreshCredentials);
return status;
}
public RedirectionPath followRedirection(HttpBaseMethod method) throws Exception {
int redirectionsCount = 0;
int status = method.getStatusCode();
RedirectionPath result = new RedirectionPath(status, MAX_REDIRECTIONS_COUNT);
while (redirectionsCount < MAX_REDIRECTIONS_COUNT &&
(status == HttpConstants.HTTP_MOVED_PERMANENTLY ||
status == HttpConstants.HTTP_MOVED_TEMPORARILY ||
status == HttpConstants.HTTP_TEMPORARY_REDIRECT)
) {
final String location = method.getResponseHeader(HttpConstants.LOCATION_HEADER) != null
? method.getResponseHeader(HttpConstants.LOCATION_HEADER)
: method.getResponseHeader(HttpConstants.LOCATION_HEADER_LOWER);
if (location != null) {
Log_OC.d(TAG + " #" + mInstanceNumber,
"Location to redirect: " + location);
result.addLocation(location);
// Release the connection to avoid reach the max number of connections per host
// due to it will be set a different url
exhaustResponse(method.getResponseBodyAsStream());
method.setUrl(HttpUrl.parse(location));
final String destination = method.getRequestHeader("Destination") != null
? method.getRequestHeader("Destination")
: method.getRequestHeader("destination");
if (destination != null) {
final int suffixIndex = location.lastIndexOf(getNewFilesWebDavUri().toString());
final String redirectionBase = location.substring(0, suffixIndex);
final String destinationPath = destination.substring(mBaseUri.toString().length());
method.setRequestHeader("destination", redirectionBase + destinationPath);
}
try {
status = executeRedirectedHttpMethod(method);
} catch (HttpException e) {
if(e.getMessage().contains(Integer.toString(HttpConstants.HTTP_MOVED_TEMPORARILY))) {
status = HttpConstants.HTTP_MOVED_TEMPORARILY;
} else {
throw e;
}
}
result.addStatus(status);
redirectionsCount++;
} else {
Log_OC.d(TAG + " #" + mInstanceNumber, "No location to redirect!");
status = HttpConstants.HTTP_NOT_FOUND;
}
}
return result;
}
/**
* Exhausts a not interesting HTTP response. Encouraged by HttpClient documentation.
*
* @param responseBodyAsStream InputStream with the HTTP response to exhaust.
*/
public void exhaustResponse(InputStream responseBodyAsStream) {
if (responseBodyAsStream != null) {
try {
while (responseBodyAsStream.read(sExhaustBuffer) >= 0) ;
responseBodyAsStream.close();
} catch (IOException io) {
Log_OC.e(TAG, "Unexpected exception while exhausting not interesting HTTP response;" +
" will be IGNORED", io);
}
}
}
public Uri getNewFilesWebDavUri() {
return mCredentials instanceof OwnCloudAnonymousCredentials
? Uri.parse(mBaseUri + NEW_WEBDAV_FILES_PATH_4_0)
: Uri.parse(mBaseUri + NEW_WEBDAV_FILES_PATH_4_0 + mCredentials.getUsername());
}
public Uri getNewUploadsWebDavUri() {
return mCredentials instanceof OwnCloudAnonymousCredentials
? Uri.parse(mBaseUri + NEW_WEBDAV_UPLOADS_PATH_4_0)
: Uri.parse(mBaseUri + NEW_WEBDAV_UPLOADS_PATH_4_0 + mCredentials.getUsername());
}
/**
* Sets the root URI to the ownCloud server.
*
* Use with care.
*
* @param uri
*/
public void setBaseUri(Uri uri) {
if (uri == null) {
throw new IllegalArgumentException("URI cannot be NULL");
}
mBaseUri = uri;
}
public Uri getBaseUri() {
return mBaseUri;
}
public final OwnCloudCredentials getCredentials() {
return mCredentials;
}
private void logCookie(Cookie cookie) {
Log_OC.d(TAG, "Cookie name: " + cookie.name());
Log_OC.d(TAG, " value: " + cookie.value());
Log_OC.d(TAG, " domain: " + cookie.domain());
Log_OC.d(TAG, " path: " + cookie.path());
Log_OC.d(TAG, " expiryDate: " + cookie.expiresAt());
Log_OC.d(TAG, " secure: " + cookie.secure());
}
private void logCookiesAtRequest(Headers headers, String when) {
int counter = 0;
for (final String cookieHeader : headers.toMultimap().get("cookie")) {
Log_OC.d(TAG + " #" + mInstanceNumber,
"Cookies at request (" + when + ") (" + counter++ + "): "
+ cookieHeader);
}
if (counter == 0) {
Log_OC.d(TAG + " #" + mInstanceNumber, "No cookie at request before");
}
}
private void logSetCookiesAtResponse(Headers headers) {
int counter = 0;
for (final String cookieHeader : headers.toMultimap().get("set-cookie")) {
Log_OC.d(TAG + " #" + mInstanceNumber,
"Set-Cookie (" + counter++ + "): " + cookieHeader);
}
if (counter == 0) {
Log_OC.d(TAG + " #" + mInstanceNumber, "No set-cookie");
}
}
public List<Cookie> getCookiesFromCurrentAccount() {
return getOkHttpClient().cookieJar().loadForRequest(HttpUrl.parse(
getAccount().getBaseUri().toString()));
}
public String getCookiesString() {
String cookiesString = "";
for (Cookie cookie : getCookiesFromCurrentAccount()) {
cookiesString += cookie.toString() + ";";
}
return cookiesString;
}
public void setCookiesForCurrentAccount(List<Cookie> cookies) {
getOkHttpClient().cookieJar().saveFromResponse(HttpUrl.parse(
getAccount().getBaseUri().toString()), cookies);
}
public void setOwnCloudVersion(OwnCloudVersion version) {
mVersion = version;
}
public OwnCloudVersion getOwnCloudVersion() {
return mVersion;
}
public Context getContext() {
return mContext;
}
public void setAccount(OwnCloudAccount account) {
this.mAccount = account;
}
public OwnCloudAccount getAccount() {
return mAccount;
}
/**
* Checks the status code of an execution and decides if should be repeated with fresh credentials.
*
* Invalidates current credentials if the request failed as anauthorized.
*
* Refresh current credentials if possible, and marks a retry.
*
* @param status
* @param repeatCounter
* @return
*/
private boolean checkUnauthorizedAccess(int status, int repeatCounter) {
boolean credentialsWereRefreshed = false;
if (shouldInvalidateAccountCredentials(status)) {
boolean invalidated = invalidateAccountCredentials();
if (invalidated) {
if (getCredentials().authTokenCanBeRefreshed() &&
repeatCounter < MAX_REPEAT_COUNT_WITH_FRESH_CREDENTIALS) {
try {
mAccount.loadCredentials(mContext);
// if mAccount.getCredentials().length() == 0 --> refresh failed
setCredentials(mAccount.getCredentials());
credentialsWereRefreshed = true;
} catch (AccountsException | IOException e) {
Log_OC.e(
TAG,
"Error while trying to refresh auth token for " + mAccount.getSavedAccount().name,
e
);
}
}
if (!credentialsWereRefreshed && mOwnCloudClientManager != null) {
// if credentials are not refreshed, client must be removed
// from the OwnCloudClientManager to prevent it is reused once and again
mOwnCloudClientManager.removeClientFor(mAccount);
}
}
// else: onExecute will finish with status 401
}
return credentialsWereRefreshed;
}
/**
* Determines if credentials should be invalidated according the to the HTTPS status
* of a network request just performed.
*
* @param httpStatusCode Result of the last request ran with the 'credentials' belows.
* @return 'True' if credentials should and might be invalidated, 'false' if shouldn't or
* cannot be invalidated with the given arguments.
*/
private boolean shouldInvalidateAccountCredentials(int httpStatusCode) {
boolean should = (httpStatusCode == HttpConstants.HTTP_UNAUTHORIZED || isIdPRedirection()); // invalid credentials
should &= (mCredentials != null && // real credentials
!(mCredentials instanceof OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials));
// test if have all the needed to effectively invalidate ...
should &= (mAccount != null && mAccount.getSavedAccount() != null && mContext != null);
return should;
}
/**
* Invalidates credentials stored for the given account in the system {@link AccountManager} and in
* current {@link OwnCloudClientManagerFactory#getDefaultSingleton()} instance.
*
* {@link #shouldInvalidateAccountCredentials(int)} should be called first.
*
* @return 'True' if invalidation was successful, 'false' otherwise.
*/
private boolean invalidateAccountCredentials() {
AccountManager am = AccountManager.get(mContext);
am.invalidateAuthToken(
mAccount.getSavedAccount().type,
mCredentials.getAuthToken()
);
am.clearPassword(mAccount.getSavedAccount()); // being strict, only needed for Basic Auth credentials
return true;
}
public OwnCloudClientManager getOwnCloudClientManager() {
return mOwnCloudClientManager;
}
void setOwnCloudClientManager(OwnCloudClientManager clientManager) {
mOwnCloudClientManager = clientManager;
}
/**
* Check if the redirection is to an identity provider such as SAML or wayf
* @return true if the redirection location includes SAML or wayf, false otherwise
*/
private boolean isIdPRedirection() {
return (mRedirectedLocation != null &&
(mRedirectedLocation.toUpperCase().contains("SAML") ||
mRedirectedLocation.toLowerCase().contains("wayf")));
}
public boolean followRedirects() {
return mFollowRedirects;
}
public void setFollowRedirects(boolean followRedirects) {
this.mFollowRedirects = followRedirects;
}
}