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

Merge pull request #450 from owncloud/connection_validator

Connection validator
This commit is contained in:
Abel García de Prada 2022-03-31 14:55:31 +02:00 committed by GitHub
commit d8ac22d57e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 714 additions and 356 deletions

View File

@ -12,6 +12,7 @@ dependencies {
implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") { implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") {
exclude module: "kotlin-reflect" exclude module: "kotlin-reflect"
} }
implementation 'org.apache.commons:commons-lang3:3.12.0'
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'

View File

@ -0,0 +1,186 @@
package com.owncloud.android.lib.common
import android.accounts.AccountManager
import android.accounts.AccountsException
import android.content.Context
import com.owncloud.android.lib.common.authentication.OwnCloudCredentials
import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials
import com.owncloud.android.lib.common.http.HttpConstants
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation
import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation
import com.owncloud.android.lib.resources.status.RemoteServerInfo
import org.apache.commons.lang3.exception.ExceptionUtils
import timber.log.Timber
import java.io.IOException
import java.lang.Exception
class ConnectionValidator(
val context: Context,
val clearCookiesOnValidation: Boolean
) {
fun validate(baseClient: OwnCloudClient, singleSessionManager: SingleSessionManager): Boolean {
try {
var validationRetryCount = 0
val client = OwnCloudClient(baseClient.baseUri, null, false, singleSessionManager)
if (clearCookiesOnValidation) {
client.clearCookies()
} else {
client.cookiesForBaseUri = baseClient.cookiesForBaseUri
}
client.account = baseClient.account
client.credentials = baseClient.credentials
while (validationRetryCount < VALIDATION_RETRY_COUNT) {
Timber.d("validationRetryCout %d", validationRetryCount)
var successCounter = 0
var failCounter = 0
client.setFollowRedirects(true)
if (isOnwCloudStatusOk(client)) {
successCounter++
} else {
failCounter++
}
// Skip the part where we try to check if we can access the parts where we have to be logged in... if we are not logged in
if (baseClient.credentials !is OwnCloudAnonymousCredentials) {
client.setFollowRedirects(false)
val contentReply = canAccessRootFolder(client)
if (contentReply.httpCode == HttpConstants.HTTP_OK) {
if (contentReply.data == true) { //if data is true it means that the content reply was ok
successCounter++
} else {
failCounter++
}
} else {
failCounter++
if (contentReply.httpCode == HttpConstants.HTTP_UNAUTHORIZED) {
checkUnauthorizedAccess(client, singleSessionManager, contentReply.httpCode)
}
}
}
if (successCounter >= failCounter) {
baseClient.credentials = client.credentials
baseClient.cookiesForBaseUri = client.cookiesForBaseUri
return true
}
validationRetryCount++
}
Timber.d("Could not authenticate or get valid data from owncloud")
} catch (e: Exception) {
Timber.d(ExceptionUtils.getStackTrace(e))
}
return false
}
private fun isOnwCloudStatusOk(client: OwnCloudClient): Boolean {
val reply = getOwnCloudStatus(client)
// dont check status code. It currently relais on the broken redirect code of the owncloud client
// TODO: Use okhttp redirect and add this check again
// return reply.httpCode == HttpConstants.HTTP_OK &&
return !reply.isException &&
reply.data != null
}
private fun getOwnCloudStatus(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> {
val remoteStatusOperation = GetRemoteStatusOperation()
return remoteStatusOperation.execute(client)
}
private fun canAccessRootFolder(client: OwnCloudClient): RemoteOperationResult<Boolean> {
val checkPathExistenceRemoteOperation = CheckPathExistenceRemoteOperation("/", true)
return checkPathExistenceRemoteOperation.execute(client)
}
/**
* 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 fun shouldInvalidateAccountCredentials(credentials: OwnCloudCredentials, account: OwnCloudAccount, httpStatusCode: Int): Boolean {
var shouldInvalidateAccountCredentials = httpStatusCode == HttpConstants.HTTP_UNAUTHORIZED
shouldInvalidateAccountCredentials = shouldInvalidateAccountCredentials and // real credentials
(credentials !is OwnCloudAnonymousCredentials)
// test if have all the needed to effectively invalidate ...
shouldInvalidateAccountCredentials =
shouldInvalidateAccountCredentials and (account.savedAccount != null)
return shouldInvalidateAccountCredentials
}
/**
* Invalidates credentials stored for the given account in the system [AccountManager] and in
* current [SingleSessionManager.getDefaultSingleton] instance.
*
*
* [.shouldInvalidateAccountCredentials] should be called first.
*
*/
private fun invalidateAccountCredentials(account: OwnCloudAccount, credentials: OwnCloudCredentials) {
val am = AccountManager.get(context)
am.invalidateAuthToken(
account.savedAccount.type,
credentials.authToken
)
am.clearPassword(account.savedAccount) // being strict, only needed for Basic Auth credentials
}
/**
* 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 fun checkUnauthorizedAccess(client: OwnCloudClient, singleSessionManager: SingleSessionManager, status: Int): Boolean {
var credentialsWereRefreshed = false
val account = client.account
val credentials = account.credentials
if (shouldInvalidateAccountCredentials(credentials, account, status)) {
invalidateAccountCredentials(account, credentials)
if (credentials.authTokenCanBeRefreshed()) {
try {
// This command does the actual refresh
account.loadCredentials(context)
// if mAccount.getCredentials().length() == 0 --> refresh failed
client.credentials = account.credentials
credentialsWereRefreshed = true
} catch (e: AccountsException) {
Timber.e(
e, "Error while trying to refresh auth token for %s\ntrace: %s",
account.savedAccount.name,
ExceptionUtils.getStackTrace(e)
)
} catch (e: IOException) {
Timber.e(
e, "Error while trying to refresh auth token for %s\ntrace: %s",
account.savedAccount.name,
ExceptionUtils.getStackTrace(e)
)
}
if (!credentialsWereRefreshed) {
// if credentials are not refreshed, client must be removed
// from the OwnCloudClientManager to prevent it is reused once and again
singleSessionManager.removeClientFor(account)
}
}
// else: onExecute will finish with status 401
}
return credentialsWereRefreshed
}
companion object {
private val VALIDATION_RETRY_COUNT = 3
}
}

View File

@ -25,11 +25,8 @@
package com.owncloud.android.lib.common; package com.owncloud.android.lib.common;
import android.accounts.AccountManager;
import android.accounts.AccountsException;
import android.net.Uri; import android.net.Uri;
import at.bitfire.dav4jvm.exception.HttpException;
import com.owncloud.android.lib.common.accounts.AccountUtils; import com.owncloud.android.lib.common.accounts.AccountUtils;
import com.owncloud.android.lib.common.authentication.OwnCloudCredentials; import com.owncloud.android.lib.common.authentication.OwnCloudCredentials;
import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory; import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory;
@ -37,7 +34,6 @@ import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory
import com.owncloud.android.lib.common.http.HttpClient; import com.owncloud.android.lib.common.http.HttpClient;
import com.owncloud.android.lib.common.http.HttpConstants; import com.owncloud.android.lib.common.http.HttpConstants;
import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; import com.owncloud.android.lib.common.http.methods.HttpBaseMethod;
import com.owncloud.android.lib.common.network.RedirectionPath;
import com.owncloud.android.lib.common.utils.RandomUtils; import com.owncloud.android.lib.common.utils.RandomUtils;
import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.lib.resources.status.OwnCloudVersion;
import okhttp3.Cookie; import okhttp3.Cookie;
@ -49,40 +45,51 @@ import java.io.InputStream;
import java.util.List; import java.util.List;
import static com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER; import static com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER;
import static com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID; import static com.owncloud.android.lib.common.http.HttpConstants.HTTP_MOVED_PERMANENTLY;
public class OwnCloudClient extends HttpClient { public class OwnCloudClient extends HttpClient {
public static final String WEBDAV_FILES_PATH_4_0 = "/remote.php/dav/files/"; public static final String WEBDAV_FILES_PATH_4_0 = "/remote.php/dav/files/";
public static final String WEBDAV_PATH_4_0_AND_LATER = "/remote.php/dav";
public static final String STATUS_PATH = "/status.php"; public static final String STATUS_PATH = "/status.php";
private static final String WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/"; private static final String WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/";
private static final int MAX_REDIRECTIONS_COUNT = 3; private static final int MAX_RETRY_COUNT = 2;
private static final int MAX_REPEAT_COUNT_WITH_FRESH_CREDENTIALS = 1;
private static byte[] sExhaustBuffer = new byte[1024];
private static int sIntanceCounter = 0; private static int sIntanceCounter = 0;
private OwnCloudCredentials mCredentials = null; private OwnCloudCredentials mCredentials = null;
private int mInstanceNumber; private int mInstanceNumber;
private Uri mBaseUri; private Uri mBaseUri;
private OwnCloudVersion mVersion = null; private OwnCloudVersion mVersion = null;
private OwnCloudAccount mAccount; private OwnCloudAccount mAccount;
private final ConnectionValidator mConnectionValidator;
private Object mRequestMutex = new Object();
// If set to true a mutex will be used to prevent parallel execution of the execute() method
// if false the execute() method can be called even though the mutex is already aquired.
// This is used for the ConnectionValidator, which has to be able to execute OperationsWhile all "normal" operations net
// to be set on hold.
private final Boolean mSynchronizeRequests;
private SingleSessionManager mSingleSessionManager = null; private SingleSessionManager mSingleSessionManager = null;
private boolean mFollowRedirects; private boolean mFollowRedirects = false;
public OwnCloudClient(Uri baseUri) { public OwnCloudClient(Uri baseUri,
ConnectionValidator connectionValidator,
boolean synchronizeRequests,
SingleSessionManager singleSessionManager) {
if (baseUri == null) { if (baseUri == null) {
throw new IllegalArgumentException("Parameter 'baseUri' cannot be NULL"); throw new IllegalArgumentException("Parameter 'baseUri' cannot be NULL");
} }
mBaseUri = baseUri; mBaseUri = baseUri;
mSynchronizeRequests = synchronizeRequests;
mSingleSessionManager = singleSessionManager;
mInstanceNumber = sIntanceCounter++; mInstanceNumber = sIntanceCounter++;
Timber.d("#" + mInstanceNumber + "Creating OwnCloudClient"); Timber.d("#" + mInstanceNumber + "Creating OwnCloudClient");
clearCredentials(); clearCredentials();
clearCookies(); clearCookies();
mConnectionValidator = connectionValidator;
} }
public void clearCredentials() { public void clearCredentials() {
@ -92,11 +99,27 @@ public class OwnCloudClient extends HttpClient {
} }
public int executeHttpMethod(HttpBaseMethod method) throws Exception { public int executeHttpMethod(HttpBaseMethod method) throws Exception {
boolean repeatWithFreshCredentials; if(mSynchronizeRequests) {
synchronized (mRequestMutex) {
return saveExecuteHttpMethod(method);
}
} else {
return saveExecuteHttpMethod(method);
}
}
private int saveExecuteHttpMethod(HttpBaseMethod method) throws Exception {
int repeatCounter = 0; int repeatCounter = 0;
int status; int status;
if(mFollowRedirects) {
method.setFollowRedirects(true);
}
boolean retry;
do { do {
repeatCounter++;
retry = false;
String requestId = RandomUtils.generateRandomUUID(); String requestId = RandomUtils.generateRandomUUID();
// Header to allow tracing requests in apache and ownCloud logs // Header to allow tracing requests in apache and ownCloud logs
@ -104,105 +127,31 @@ public class OwnCloudClient extends HttpClient {
method.setRequestHeader(HttpConstants.OC_X_REQUEST_ID, requestId); method.setRequestHeader(HttpConstants.OC_X_REQUEST_ID, requestId);
method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent()); method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent());
method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY); method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY);
if (mCredentials.getHeaderAuth() != null && method.getRequestHeader(AUTHORIZATION_HEADER) == null) { if (mCredentials.getHeaderAuth() != null && !mCredentials.getHeaderAuth().isEmpty()) {
method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth());
} }
status = method.execute(); status = method.execute();
if (mFollowRedirects) { if (shouldConnectionValidatorBeCalled(method, status)) {
status = followRedirection(method).getLastStatus(); retry = mConnectionValidator.validate(this, mSingleSessionManager); // retry on success fail on no success
} else if(method.getFollowPermanentRedirects() && status == HTTP_MOVED_PERMANENTLY) {
retry = true;
method.setFollowRedirects(true);
} }
repeatWithFreshCredentials = checkUnauthorizedAccess(status, repeatCounter); } while (retry && repeatCounter < MAX_RETRY_COUNT);
if (repeatWithFreshCredentials) {
repeatCounter++;
}
} while (repeatWithFreshCredentials);
return status; return status;
} }
private int executeRedirectedHttpMethod(HttpBaseMethod method) throws Exception { private boolean shouldConnectionValidatorBeCalled(HttpBaseMethod method, int status) {
boolean repeatWithFreshCredentials; return !mFollowRedirects &&
int repeatCounter = 0; !method.getFollowRedirects() &&
int status; mConnectionValidator != null &&
(status == HttpConstants.HTTP_MOVED_TEMPORARILY ||
do { (!(mCredentials instanceof OwnCloudAnonymousCredentials) &&
String requestId = RandomUtils.generateRandomUUID(); status == HttpConstants.HTTP_UNAUTHORIZED));
// Header to allow tracing requests in apache and ownCloud logs
Timber.d("Executing in request with id %s", requestId);
method.setRequestHeader(OC_X_REQUEST_ID, requestId);
method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent());
method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY);
if (mCredentials.getHeaderAuth() != null) {
method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth());
}
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 redirectionPath = 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) {
Timber.d("#" + mInstanceNumber + "Location to redirect: " + location);
redirectionPath.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(getUserFilesWebDavUri().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;
}
}
redirectionPath.addStatus(status);
redirectionsCount++;
} else {
Timber.d(" #" + mInstanceNumber + "No location to redirect!");
status = HttpConstants.HTTP_NOT_FOUND;
}
}
return redirectionPath;
} }
/** /**
@ -273,26 +222,18 @@ public class OwnCloudClient extends HttpClient {
} }
} }
public String getCookiesString() { public void setCookiesForBaseUri(List<Cookie> cookies) {
StringBuilder cookiesString = new StringBuilder();
List<Cookie> cookieList = getCookiesFromUrl(HttpUrl.parse(mBaseUri.toString()));
if (cookieList != null) {
for (Cookie cookie : cookieList) {
cookiesString.append(cookie.toString()).append(";");
}
}
return cookiesString.toString();
}
public void setCookiesForCurrentAccount(List<Cookie> cookies) {
getOkHttpClient().cookieJar().saveFromResponse( getOkHttpClient().cookieJar().saveFromResponse(
HttpUrl.parse(getAccount().getBaseUri().toString()), HttpUrl.parse(mBaseUri.toString()),
cookies cookies
); );
} }
public List<Cookie> getCookiesForBaseUri() {
return getOkHttpClient().cookieJar().loadForRequest(
HttpUrl.parse(mBaseUri.toString()));
}
public OwnCloudVersion getOwnCloudVersion() { public OwnCloudVersion getOwnCloudVersion() {
return mVersion; return mVersion;
} }
@ -309,94 +250,6 @@ public class OwnCloudClient extends HttpClient {
this.mAccount = account; this.mAccount = account;
} }
/**
* Checks the status code of an execution and decides if should be repeated with fresh credentials.
* <p>
* Invalidates current credentials if the request failed as anauthorized.
* <p>
* 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(getContext());
// if mAccount.getCredentials().length() == 0 --> refresh failed
setCredentials(mAccount.getCredentials());
credentialsWereRefreshed = true;
} catch (AccountsException | IOException e) {
Timber.e(e, "Error while trying to refresh auth token for %s",
mAccount.getSavedAccount().name
);
}
}
if (!credentialsWereRefreshed && mSingleSessionManager != null) {
// if credentials are not refreshed, client must be removed
// from the OwnCloudClientManager to prevent it is reused once and again
mSingleSessionManager.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 shouldInvalidateAccountCredentials =
(httpStatusCode == HttpConstants.HTTP_UNAUTHORIZED);
shouldInvalidateAccountCredentials &= (mCredentials != null && // real credentials
!(mCredentials instanceof OwnCloudCredentialsFactory.OwnCloudAnonymousCredentials));
// test if have all the needed to effectively invalidate ...
shouldInvalidateAccountCredentials &= (mAccount != null && mAccount.getSavedAccount() != null && getContext() != null);
return shouldInvalidateAccountCredentials;
}
/**
* Invalidates credentials stored for the given account in the system {@link AccountManager} and in
* current {@link SingleSessionManager#getDefaultSingleton()} instance.
* <p>
* {@link #shouldInvalidateAccountCredentials(int)} should be called first.
*
* @return 'True' if invalidation was successful, 'false' otherwise.
*/
private boolean invalidateAccountCredentials() {
AccountManager am = AccountManager.get(getContext());
am.invalidateAuthToken(
mAccount.getSavedAccount().type,
mCredentials.getAuthToken()
);
am.clearPassword(mAccount.getSavedAccount()); // being strict, only needed for Basic Auth credentials
return true;
}
public boolean followRedirects() {
return mFollowRedirects;
}
public void setFollowRedirects(boolean followRedirects) { public void setFollowRedirects(boolean followRedirects) {
this.mFollowRedirects = followRedirects; this.mFollowRedirects = followRedirects;
} }

View File

@ -1,58 +0,0 @@
/* ownCloud Android Library is available under MIT license
* Copyright (C) 2020 ownCloud GmbH.
*
* 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.content.Context;
import android.net.Uri;
import com.owncloud.android.lib.common.http.HttpClient;
import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation;
public class OwnCloudClientFactory {
/**
* Creates a OwnCloudClient to access a URL and sets the desired parameters for ownCloud
* client connections.
*
* @param uri URL to the ownCloud server; BASE ENTRY POINT, not WebDavPATH
* @param context Android context where the OwnCloudClient is being created.
* @return A OwnCloudClient object ready to be used
*/
public static OwnCloudClient createOwnCloudClient(Uri uri, Context context, boolean followRedirects) {
OwnCloudClient client = new OwnCloudClient(uri);
client.setFollowRedirects(followRedirects);
HttpClient.setContext(context);
retrieveCookiesFromMiddleware(client);
return client;
}
private static void retrieveCookiesFromMiddleware(OwnCloudClient client) {
final GetRemoteStatusOperation statusOperation = new GetRemoteStatusOperation();
statusOperation.run(client);
}
}

View File

@ -49,6 +49,7 @@ public class SingleSessionManager {
private static SingleSessionManager sDefaultSingleton; private static SingleSessionManager sDefaultSingleton;
private static String sUserAgent; private static String sUserAgent;
private static ConnectionValidator sConnectionValidator;
private ConcurrentMap<String, OwnCloudClient> mClientsWithKnownUsername = new ConcurrentHashMap<>(); private ConcurrentMap<String, OwnCloudClient> mClientsWithKnownUsername = new ConcurrentHashMap<>();
private ConcurrentMap<String, OwnCloudClient> mClientsWithUnknownUsername = new ConcurrentHashMap<>(); private ConcurrentMap<String, OwnCloudClient> mClientsWithUnknownUsername = new ConcurrentHashMap<>();
@ -60,6 +61,14 @@ public class SingleSessionManager {
return sDefaultSingleton; return sDefaultSingleton;
} }
public static void setConnectionValidator(ConnectionValidator connectionValidator) {
sConnectionValidator = connectionValidator;
}
public static ConnectionValidator getConnectionValidator() {
return sConnectionValidator;
}
public static String getUserAgent() { public static String getUserAgent() {
return sUserAgent; return sUserAgent;
} }
@ -68,7 +77,22 @@ public class SingleSessionManager {
sUserAgent = userAgent; sUserAgent = userAgent;
} }
public OwnCloudClient getClientFor(OwnCloudAccount account, Context context) throws OperationCanceledException, private static OwnCloudClient createOwnCloudClient(Uri uri, Context context, ConnectionValidator connectionValidator, SingleSessionManager singleSessionManager) {
OwnCloudClient client = new OwnCloudClient(uri, connectionValidator, true, singleSessionManager);
HttpClient.setContext(context);
return client;
}
public OwnCloudClient getClientFor(OwnCloudAccount account,
Context context) throws OperationCanceledException,
AuthenticatorException, IOException {
return getClientFor(account, context, getConnectionValidator());
}
public OwnCloudClient getClientFor(OwnCloudAccount account,
Context context,
ConnectionValidator connectionValidator) throws OperationCanceledException,
AuthenticatorException, IOException { AuthenticatorException, IOException {
Timber.d("getClientFor starting "); Timber.d("getClientFor starting ");
@ -104,10 +128,11 @@ public class SingleSessionManager {
if (client == null) { if (client == null) {
// no client to reuse - create a new one // no client to reuse - create a new one
client = OwnCloudClientFactory.createOwnCloudClient( client = createOwnCloudClient(
account.getBaseUri(), account.getBaseUri(),
context.getApplicationContext(), context.getApplicationContext(),
true); // TODO remove dependency on OwnCloudClientFactory connectionValidator,
this);
//the next two lines are a hack because okHttpclient is used as a singleton instead of being an //the next two lines are a hack because okHttpclient is used as a singleton instead of being an
//injected instance that can be deleted when required //injected instance that can be deleted when required

View File

@ -28,7 +28,7 @@ import okhttp3.CookieJar
import okhttp3.HttpUrl import okhttp3.HttpUrl
class CookieJarImpl( class CookieJarImpl(
private val sCookieStore: HashMap<String, List<Cookie>> private val cookieStore: HashMap<String, List<Cookie>>
) : CookieJar { ) : CookieJar {
fun containsCookieWithName(cookies: List<Cookie>, name: String): Boolean { fun containsCookieWithName(cookies: List<Cookie>, name: String): Boolean {
@ -52,12 +52,11 @@ class CookieJarImpl(
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
// Avoid duplicated cookies but update // Avoid duplicated cookies but update
val currentCookies: List<Cookie> = sCookieStore[url.host] ?: ArrayList() val currentCookies: List<Cookie> = cookieStore[url.host] ?: ArrayList()
val updatedCookies: List<Cookie> = getUpdatedCookies(currentCookies, cookies) val updatedCookies: List<Cookie> = getUpdatedCookies(currentCookies, cookies)
sCookieStore[url.host] = updatedCookies cookieStore[url.host] = updatedCookies
} }
override fun loadForRequest(url: HttpUrl) = override fun loadForRequest(url: HttpUrl) =
sCookieStore[url.host] ?: ArrayList() cookieStore[url.host] ?: ArrayList()
} }

View File

@ -132,10 +132,6 @@ public class HttpClient {
return sLogInterceptor; return sLogInterceptor;
} }
public static List<Cookie> getCookiesFromUrl(HttpUrl httpUrl) {
return sCookieStore.get(httpUrl.host());
}
public Context getContext() { public Context getContext() {
return sContext; return sContext;
} }

View File

@ -41,6 +41,7 @@ abstract class HttpBaseMethod constructor(url: URL) {
var okHttpClient: OkHttpClient var okHttpClient: OkHttpClient
var httpUrl: HttpUrl = url.toHttpUrlOrNull() ?: throw MalformedURLException() var httpUrl: HttpUrl = url.toHttpUrlOrNull() ?: throw MalformedURLException()
var request: Request var request: Request
private var _followPermanentRedirects = false
abstract var response: Response abstract var response: Response
var call: Call? = null var call: Call? = null
@ -123,6 +124,11 @@ abstract class HttpBaseMethod constructor(url: URL) {
return response.body?.byteStream() return response.body?.byteStream()
} }
/**
* returns the final url after following the last redirect.
*/
open fun getFinalUrl() = response.request.url
/************************* /*************************
*** Connection Params *** *** Connection Params ***
*************************/ *************************/
@ -158,6 +164,15 @@ abstract class HttpBaseMethod constructor(url: URL) {
.build() .build()
} }
open fun getFollowRedirects() = okHttpClient.followRedirects
open fun setFollowPermanentRedirects(followRedirects: Boolean) {
_followPermanentRedirects = followRedirects
}
open fun getFollowPermanentRedirects() = _followPermanentRedirects
/************ /************
*** Call *** *** Call ***
************/ ************/

View File

@ -50,7 +50,7 @@ class PropfindMethod(
public override fun onExecute(): Int { public override fun onExecute(): Int {
davResource.propfind( davResource.propfind(
depth = depth, depth = depth,
reqProp = *propertiesToRequest, reqProp = propertiesToRequest,
listOfHeaders = super.getRequestHeadersAsHashMap(), listOfHeaders = super.getRequestHeadersAsHashMap(),
callback = { response: Response, hrefRelation: HrefRelation? -> callback = { response: Response, hrefRelation: HrefRelation? ->
when (hrefRelation) { when (hrefRelation) {

View File

@ -159,7 +159,7 @@ public abstract class RemoteOperation<T> implements Runnable {
if (mAccount != null && mContext != null) { if (mAccount != null && mContext != null) {
OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, mContext); OwnCloudAccount ocAccount = new OwnCloudAccount(mAccount, mContext);
mClient = SingleSessionManager.getDefaultSingleton(). mClient = SingleSessionManager.getDefaultSingleton().
getClientFor(ocAccount, mContext); getClientFor(ocAccount, mContext, SingleSessionManager.getConnectionValidator());
} else { } else {
throw new IllegalStateException("Trying to run a remote operation " + throw new IllegalStateException("Trying to run a remote operation " +
"asynchronously with no client and no chance to create one (no account)"); "asynchronously with no client and no chance to create one (no account)");

View File

@ -34,6 +34,7 @@ import com.owncloud.android.lib.common.http.HttpConstants;
import com.owncloud.android.lib.common.http.methods.HttpBaseMethod; import com.owncloud.android.lib.common.http.methods.HttpBaseMethod;
import com.owncloud.android.lib.common.network.CertificateCombinedException; import com.owncloud.android.lib.common.network.CertificateCombinedException;
import okhttp3.Headers; import okhttp3.Headers;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.json.JSONException; import org.json.JSONException;
import timber.log.Timber; import timber.log.Timber;
@ -59,13 +60,15 @@ public class RemoteOperationResult<T>
* Generated - should be refreshed every time the class changes!! * Generated - should be refreshed every time the class changes!!
*/ */
private static final long serialVersionUID = 4968939884332372230L; private static final long serialVersionUID = 4968939884332372230L;
private static final String LOCATION = "location";
private static final String WWW_AUTHENTICATE = "www-authenticate";
private boolean mSuccess = false; private boolean mSuccess = false;
private int mHttpCode = -1; private int mHttpCode = -1;
private String mHttpPhrase = null; private String mHttpPhrase = null;
private Exception mException = null; private Exception mException = null;
private ResultCode mCode = ResultCode.UNKNOWN_ERROR; private ResultCode mCode = ResultCode.UNKNOWN_ERROR;
private String mRedirectedLocation; private String mRedirectedLocation = "";
private List<String> mAuthenticate = new ArrayList<>(); private List<String> mAuthenticate = new ArrayList<>();
private String mLastPermanentLocation = null; private String mLastPermanentLocation = null;
private T mData = null; private T mData = null;
@ -112,6 +115,14 @@ public class RemoteOperationResult<T>
*/ */
public RemoteOperationResult(Exception e) { public RemoteOperationResult(Exception e) {
mException = e; mException = e;
//TODO: Do propper exception handling and remove this
Timber.e("---------------------------------" +
"\nCreate RemoteOperationResult from exception." +
"\n Message: %s" +
"\n Stacktrace: %s" +
"\n---------------------------------",
ExceptionUtils.getMessage(e),
ExceptionUtils.getStackTrace(e));
if (e instanceof OperationCancelledException) { if (e instanceof OperationCancelledException) {
mCode = ResultCode.CANCELLED; mCode = ResultCode.CANCELLED;
@ -248,11 +259,11 @@ public class RemoteOperationResult<T>
this(httpCode, httpPhrase); this(httpCode, httpPhrase);
if (headers != null) { if (headers != null) {
for (Map.Entry<String, List<String>> header : headers.toMultimap().entrySet()) { for (Map.Entry<String, List<String>> header : headers.toMultimap().entrySet()) {
if ("location".equals(header.getKey().toLowerCase())) { if (LOCATION.equalsIgnoreCase(header.getKey())) {
mRedirectedLocation = header.getValue().get(0); mRedirectedLocation = header.getValue().get(0);
continue; continue;
} }
if ("www-authenticate".equals(header.getKey().toLowerCase())) { if (WWW_AUTHENTICATE.equalsIgnoreCase(header.getKey())) {
for (String value: header.getValue()) { for (String value: header.getValue()) {
mAuthenticate.add(value.toLowerCase()); mAuthenticate.add(value.toLowerCase());
} }
@ -321,7 +332,7 @@ public class RemoteOperationResult<T>
mHttpPhrase = errorMessage; mHttpPhrase = errorMessage;
} }
} catch (Exception e) { } catch (Exception e) {
Timber.w("Error reading exception from server: %s", e.getMessage()); Timber.w("Error reading exception from server: %s\nTrace: %s", e.getMessage(), ExceptionUtils.getStackTrace(e));
// mCode stays as set in this(success, httpCode, headers) // mCode stays as set in this(success, httpCode, headers)
} }
} }

View File

@ -27,7 +27,6 @@ import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.http.HttpConstants import com.owncloud.android.lib.common.http.HttpConstants
import com.owncloud.android.lib.common.http.methods.webdav.DavUtils.allPropset import com.owncloud.android.lib.common.http.methods.webdav.DavUtils.allPropset
import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod
import com.owncloud.android.lib.common.network.RedirectionPath
import com.owncloud.android.lib.common.network.WebdavUtils import com.owncloud.android.lib.common.network.WebdavUtils
import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperation
import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult
@ -44,26 +43,18 @@ import java.util.concurrent.TimeUnit
* @author Abel García de Prada * @author Abel García de Prada
* *
* @param remotePath Path to append to the URL owned by the client instance. * @param remotePath Path to append to the URL owned by the client instance.
* @param isUserLogged When `true`, the username won't be added at the end of the PROPFIND url since is not * @param isUserLoggedIn When `true`, the username won't be added at the end of the PROPFIND url since is not
* needed to check user credentials * needed to check user credentials
*/ */
class CheckPathExistenceRemoteOperation( class CheckPathExistenceRemoteOperation(
val remotePath: String? = "", val remotePath: String? = "",
val isUserLogged: Boolean val isUserLoggedIn: Boolean
) : RemoteOperation<Boolean>() { ) : RemoteOperation<Boolean>() {
/**
* Gets the sequence of redirections followed during the execution of the operation.
*
* @return Sequence of redirections followed, if any, or NULL if the operation was not executed.
*/
var redirectionPath: RedirectionPath? = null
private set
override fun run(client: OwnCloudClient): RemoteOperationResult<Boolean> { override fun run(client: OwnCloudClient): RemoteOperationResult<Boolean> {
val previousFollowRedirects = client.followRedirects()
return try { return try {
val stringUrl = val stringUrl =
if (isUserLogged) client.baseFilesWebDavUri.toString() if (isUserLoggedIn) client.baseFilesWebDavUri.toString()
else client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath) else client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath)
val propFindMethod = PropfindMethod(URL(stringUrl), 0, allPropset).apply { val propFindMethod = PropfindMethod(URL(stringUrl), 0, allPropset).apply {
@ -71,12 +62,7 @@ class CheckPathExistenceRemoteOperation(
setConnectionTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS) setConnectionTimeout(TIMEOUT.toLong(), TimeUnit.SECONDS)
} }
client.setFollowRedirects(false)
var status = client.executeHttpMethod(propFindMethod) var status = client.executeHttpMethod(propFindMethod)
if (previousFollowRedirects) {
redirectionPath = client.followRedirection(propFindMethod)
status = redirectionPath?.lastStatus!!
}
/* PROPFIND method /* PROPFIND method
* 404 NOT FOUND: path doesn't exist, * 404 NOT FOUND: path doesn't exist,
* 207 MULTI_STATUS: path exists. * 207 MULTI_STATUS: path exists.
@ -94,16 +80,9 @@ class CheckPathExistenceRemoteOperation(
"Existence check for ${client.userFilesWebDavUri}${WebdavUtils.encodePath(remotePath)} : ${result.logMessage}" "Existence check for ${client.userFilesWebDavUri}${WebdavUtils.encodePath(remotePath)} : ${result.logMessage}"
) )
result result
} finally {
client.setFollowRedirects(previousFollowRedirects)
} }
} }
/**
* @return 'True' if the operation was executed and at least one redirection was followed.
*/
fun wasRedirected() = redirectionPath?.redirectionsCount ?: 0 > 0
private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS
companion object { companion object {

View File

@ -0,0 +1,77 @@
/* ownCloud Android Library is available under MIT license
* Copyright (C) 2022 ownCloud GmbH.
*
* 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.resources.files
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.lib.common.http.HttpConstants
import com.owncloud.android.lib.common.http.methods.webdav.DavUtils
import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod
import com.owncloud.android.lib.common.operations.RemoteOperation
import com.owncloud.android.lib.common.operations.RemoteOperationResult
import timber.log.Timber
import java.net.URL
import java.util.concurrent.TimeUnit
/**
* Operation to get the base url, which might differ in case of a redirect.
*
* @author Christian Schabesberger
*/
class GetBaseUrlRemoteOperation : RemoteOperation<String?>() {
override fun run(client: OwnCloudClient): RemoteOperationResult<String?> {
return try {
val stringUrl = client.baseFilesWebDavUri.toString()
val propFindMethod = PropfindMethod(URL(stringUrl), 0, DavUtils.allPropset).apply {
setReadTimeout(TIMEOUT, TimeUnit.SECONDS)
setConnectionTimeout(TIMEOUT, TimeUnit.SECONDS)
}
val status = client.executeHttpMethod(propFindMethod)
if (isSuccess(status)) {
RemoteOperationResult<String?>(RemoteOperationResult.ResultCode.OK).apply {
data = propFindMethod.getFinalUrl().toString()
}
} else {
RemoteOperationResult<String?>(propFindMethod).apply {
data = null
}
}
} catch (e: Exception) {
Timber.e(e, "Could not get actuall (or redirected) base URL from base url (/).")
RemoteOperationResult<String?>(e)
}
}
private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_MULTI_STATUS
companion object {
/**
* Maximum time to wait for a response from the server in milliseconds.
*/
private const val TIMEOUT = 10_000L
}
}

View File

@ -77,8 +77,6 @@ public class ReadRemoteFolderOperation extends RemoteOperation<ArrayList<RemoteF
DavConstants.DEPTH_1, DavConstants.DEPTH_1,
DavUtils.getAllPropset()); DavUtils.getAllPropset());
client.setFollowRedirects(true);
int status = client.executeHttpMethod(propfindMethod); int status = client.executeHttpMethod(propfindMethod);
if (isSuccess(status)) { if (isSuccess(status)) {

View File

@ -33,6 +33,6 @@ class OCFileService(override val client: OwnCloudClient) :
override fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult<Boolean> = override fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult<Boolean> =
CheckPathExistenceRemoteOperation( CheckPathExistenceRemoteOperation(
remotePath = path, remotePath = path,
isUserLogged = isUserLogged isUserLoggedIn = isUserLogged
).execute(client) ).execute(client)
} }

View File

@ -55,6 +55,7 @@ class GetOIDCDiscoveryRemoteOperation : RemoteOperation<OIDCDiscoveryResponse>()
addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE) addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE)
} }
getMethod.setFollowRedirects(true)
val status = client.executeHttpMethod(getMethod) val status = client.executeHttpMethod(getMethod)
val responseBody = getMethod.getResponseBodyAsString() val responseBody = getMethod.getResponseBodyAsString()

View File

@ -30,9 +30,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import com.owncloud.android.lib.resources.status.HttpScheme.HTTPS_PREFIX import com.owncloud.android.lib.resources.status.HttpScheme.HTTPS_PREFIX
import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_PREFIX import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_PREFIX
import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_SCHEME
import org.json.JSONException import org.json.JSONException
import timber.log.Timber
/** /**
* Checks if the server is valid * Checks if the server is valid
@ -45,27 +43,24 @@ import timber.log.Timber
class GetRemoteStatusOperation : RemoteOperation<RemoteServerInfo>() { class GetRemoteStatusOperation : RemoteOperation<RemoteServerInfo>() {
public override fun run(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { public override fun run(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> {
if (!usesHttpOrHttps(client.baseUri)) {
client.baseUri = buildFullHttpsUrl(client.baseUri) client.baseUri = buildFullHttpsUrl(client.baseUri)
}
var result = tryToConnect(client) return tryToConnect(client)
if (!(result.code == ResultCode.OK || result.code == ResultCode.OK_SSL) && !result.isSslRecoverableException) {
Timber.d("Establishing secure connection failed, trying non secure connection")
client.baseUri = client.baseUri.buildUpon().scheme(HTTP_SCHEME).build()
result = tryToConnect(client)
} }
return result private fun updateClientBaseUrl(client: OwnCloudClient, newBaseUrl: String) {
client.baseUri = Uri.parse(newBaseUrl)
} }
private fun tryToConnect(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> { private fun tryToConnect(client: OwnCloudClient): RemoteOperationResult<RemoteServerInfo> {
val baseUrl = client.baseUri.toString() val baseUrl = client.baseUri.toString()
client.setFollowRedirects(false)
return try { return try {
val requester = StatusRequester() val requester = StatusRequester()
val requestResult = requester.requestAndFollowRedirects(baseUrl, client) val requestResult = requester.request(baseUrl, client)
requester.handleRequestResult(requestResult, baseUrl).also { val result = requester.handleRequestResult(requestResult, baseUrl)
client.baseUri = Uri.parse(it.data.baseUrl) updateClientBaseUrl(client, result.data.baseUrl)
} return result
} catch (e: JSONException) { } catch (e: JSONException) {
RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED) RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -73,36 +73,18 @@ internal class StatusRequester {
data class RequestResult( data class RequestResult(
val getMethod: GetMethod, val getMethod: GetMethod,
val status: Int, val status: Int,
val redirectedToUnsecureLocation: Boolean,
val lastLocation: String val lastLocation: String
) )
fun requestAndFollowRedirects(baseLocation: String, client: OwnCloudClient): RequestResult { fun request(baseLocation: String, client: OwnCloudClient): RequestResult {
var currentLocation = baseLocation + OwnCloudClient.STATUS_PATH val currentLocation = baseLocation + OwnCloudClient.STATUS_PATH
var redirectedToUnsecureLocation = false
var status: Int var status: Int
while (true) {
val getMethod = getGetMethod(currentLocation) val getMethod = getGetMethod(currentLocation)
getMethod.setFollowPermanentRedirects(true)
status = client.executeHttpMethod(getMethod) status = client.executeHttpMethod(getMethod)
val result =
if (status.isSuccess()) RemoteOperationResult<OwnCloudVersion>(RemoteOperationResult.ResultCode.OK)
else RemoteOperationResult(getMethod)
if (result.redirectedLocation.isNullOrEmpty() || result.isSuccess) { return RequestResult(getMethod, status, getMethod.getFinalUrl().toString())
return RequestResult(getMethod, status, redirectedToUnsecureLocation, currentLocation)
} else {
val nextLocation = updateLocationWithRedirectPath(currentLocation, result.redirectedLocation)
redirectedToUnsecureLocation =
isRedirectedToNonSecureConnection(
redirectedToUnsecureLocation,
currentLocation,
nextLocation
)
currentLocation = nextLocation
}
}
} }
private fun Int.isSuccess() = this == HttpConstants.HTTP_OK private fun Int.isSuccess() = this == HttpConstants.HTTP_OK
@ -122,12 +104,8 @@ internal class StatusRequester {
// the version object will be returned even if the version is invalid, no error code; // the version object will be returned even if the version is invalid, no error code;
// every app will decide how to act if (ocVersion.isVersionValid() == false) // every app will decide how to act if (ocVersion.isVersionValid() == false)
val result: RemoteOperationResult<RemoteServerInfo> = val result: RemoteOperationResult<RemoteServerInfo> =
if (requestResult.redirectedToUnsecureLocation) {
RemoteOperationResult(RemoteOperationResult.ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION)
} else {
if (baseUrl.startsWith(HTTPS_SCHEME)) RemoteOperationResult(RemoteOperationResult.ResultCode.OK_SSL) if (baseUrl.startsWith(HTTPS_SCHEME)) RemoteOperationResult(RemoteOperationResult.ResultCode.OK_SSL)
else RemoteOperationResult(RemoteOperationResult.ResultCode.OK_NO_SSL) else RemoteOperationResult(RemoteOperationResult.ResultCode.OK_NO_SSL)
}
val finalUrl = URL(requestResult.lastLocation) val finalUrl = URL(requestResult.lastLocation)
val finalBaseUrl = URL( val finalBaseUrl = URL(
finalUrl.protocol, finalUrl.protocol,

View File

@ -42,7 +42,7 @@ class OCServerInfoService : ServerInfoService {
): RemoteOperationResult<Boolean> = ): RemoteOperationResult<Boolean> =
CheckPathExistenceRemoteOperation( CheckPathExistenceRemoteOperation(
remotePath = path, remotePath = path,
isUserLogged = true isUserLoggedIn = true
).execute(client) ).execute(client)
override fun getRemoteStatus( override fun getRemoteStatus(

View File

@ -0,0 +1,302 @@
/* ownCloud Android Library is available under MIT license
* Copyright (C) 2020 ownCloud GmbH.
*
* 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.sampleclient;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.owncloud.android.lib.common.ConnectionValidator;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.SingleSessionManager;
import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory;
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
import com.owncloud.android.lib.common.operations.RemoteOperation;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.resources.files.DownloadRemoteFileOperation;
import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation;
import com.owncloud.android.lib.resources.files.RemoteFile;
import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation;
import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;
import timber.log.Timber;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends Activity implements OnRemoteOperationListener, OnDatatransferProgressListener {
private Handler mHandler;
private OwnCloudClient mClient;
private FilesArrayAdapter mFilesAdapter;
private View mFrame;
/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Timber.plant();
mHandler = new Handler();
final Uri serverUri = Uri.parse(getString(R.string.server_base_url));
SingleSessionManager.setUserAgent(getUserAgent());
SingleSessionManager.setConnectionValidator(new ConnectionValidator(this, false));
mClient = new OwnCloudClient(serverUri,
SingleSessionManager.getConnectionValidator(),
true,
SingleSessionManager.getDefaultSingleton());
mClient.setCredentials(
OwnCloudCredentialsFactory.newBasicCredentials(
getString(R.string.username),
getString(R.string.password)
)
);
mFilesAdapter = new FilesArrayAdapter(this, R.layout.file_in_list);
((ListView) findViewById(R.id.list_view)).setAdapter(mFilesAdapter);
// TODO move to background thread or task
AssetManager assets = getAssets();
try {
String sampleFileName = getString(R.string.sample_file_name);
File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path));
upFolder.mkdir();
File upFile = new File(upFolder, sampleFileName);
FileOutputStream fos = new FileOutputStream(upFile);
InputStream is = assets.open(sampleFileName);
int count;
byte[] buffer = new byte[1024];
while ((count = is.read(buffer, 0, buffer.length)) >= 0) {
fos.write(buffer, 0, count);
}
is.close();
fos.close();
} catch (IOException e) {
Toast.makeText(this, R.string.error_copying_sample_file, Toast.LENGTH_SHORT).show();
Timber.e(e, getString(R.string.error_copying_sample_file));
}
mFrame = findViewById(R.id.frame);
}
@Override
public void onDestroy() {
File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path));
File upFile = upFolder.listFiles()[0];
upFile.delete();
upFolder.delete();
super.onDestroy();
}
public void onClickHandler(View button) {
switch (button.getId()) {
case R.id.button_refresh:
startRefresh();
break;
case R.id.button_upload:
startUpload();
break;
case R.id.button_delete_remote:
startRemoteDeletion();
break;
case R.id.button_download:
startDownload();
break;
case R.id.button_delete_local:
startLocalDeletion();
break;
default:
Toast.makeText(this, R.string.youre_doing_it_wrong, Toast.LENGTH_SHORT).show();
}
}
private void startRefresh() {
ReadRemoteFolderOperation refreshOperation = new ReadRemoteFolderOperation(File.separator);
refreshOperation.execute(mClient, this, mHandler);
}
private void startUpload() {
File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path));
File fileToUpload = upFolder.listFiles()[0];
String remotePath = File.separator + fileToUpload.getName();
String mimeType = getString(R.string.sample_file_mimetype);
// Get the last modification date of the file from the file system
long timeStampLong = fileToUpload.lastModified() / 1000;
String timeStamp = Long.toString(timeStampLong);
UploadRemoteFileOperation uploadOperation = new UploadRemoteFileOperation(fileToUpload.getAbsolutePath(),
remotePath, mimeType, timeStamp);
uploadOperation.addDatatransferProgressListener(this);
uploadOperation.execute(mClient, this, mHandler);
}
private void startRemoteDeletion() {
File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path));
File fileToUpload = upFolder.listFiles()[0];
String remotePath = File.separator + fileToUpload.getName();
RemoveRemoteFileOperation removeOperation = new RemoveRemoteFileOperation(remotePath);
removeOperation.execute(mClient, this, mHandler);
}
private void startDownload() {
File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path));
downFolder.mkdir();
File upFolder = new File(getCacheDir(), getString(R.string.upload_folder_path));
File fileToUpload = upFolder.listFiles()[0];
String remotePath = File.separator + fileToUpload.getName();
DownloadRemoteFileOperation downloadOperation = new DownloadRemoteFileOperation(remotePath,
downFolder.getAbsolutePath());
downloadOperation.addDatatransferProgressListener(this);
downloadOperation.execute(mClient, this, mHandler);
}
private void startLocalDeletion() {
File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path));
File downloadedFile = downFolder.listFiles()[0];
if (!downloadedFile.delete() && downloadedFile.exists()) {
Toast.makeText(this, R.string.error_deleting_local_file, Toast.LENGTH_SHORT).show();
} else {
((TextView) findViewById(R.id.download_progress)).setText("0%");
findViewById(R.id.frame).setBackgroundDrawable(null);
}
}
@Override
public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
if (!result.isSuccess()) {
Toast.makeText(this, R.string.todo_operation_finished_in_fail, Toast.LENGTH_SHORT).show();
Timber.e(result.getException(), result.getLogMessage());
} else if (operation instanceof ReadRemoteFolderOperation) {
onSuccessfulRefresh(result);
} else if (operation instanceof com.owncloud.android.lib.resources.files.UploadRemoteFileOperation) {
onSuccessfulUpload();
} else if (operation instanceof RemoveRemoteFileOperation) {
onSuccessfulRemoteDeletion();
} else if (operation instanceof DownloadRemoteFileOperation) {
onSuccessfulDownload();
} else {
Toast.makeText(this, R.string.todo_operation_finished_in_success, Toast.LENGTH_SHORT).show();
}
}
private void onSuccessfulRefresh(RemoteOperationResult result) {
mFilesAdapter.clear();
List<RemoteFile> files = new ArrayList<>();
for (RemoteFile remoteFile : (List<RemoteFile>) result.getData()) {
files.add(remoteFile);
}
for (RemoteFile file : files) {
mFilesAdapter.add(file);
}
mFilesAdapter.remove(mFilesAdapter.getItem(0));
mFilesAdapter.notifyDataSetChanged();
}
private void onSuccessfulUpload() {
startRefresh();
}
private void onSuccessfulRemoteDeletion() {
startRefresh();
TextView progressView = findViewById(R.id.upload_progress);
if (progressView != null) {
progressView.setText("0%");
}
}
private void onSuccessfulDownload() {
File downFolder = new File(getCacheDir(), getString(R.string.download_folder_path));
File downloadedFile = downFolder.listFiles()[0];
BitmapDrawable bDraw = new BitmapDrawable(getResources(), downloadedFile.getAbsolutePath());
mFrame.setBackgroundDrawable(bDraw);
}
@SuppressLint("SetTextI18n")
@Override
public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName) {
final long percentage = (totalToTransfer > 0 ? totalTransferredSoFar * 100 / totalToTransfer : 0);
final boolean upload = fileName.contains(getString(R.string.upload_folder_path));
Timber.d("progressRate %s", percentage);
mHandler.post(() -> {
TextView progressView;
if (upload) {
progressView = findViewById(R.id.upload_progress);
} else {
progressView = findViewById(R.id.download_progress);
}
if (progressView != null) {
progressView.setText(percentage + "%");
}
});
}
// user agent
@SuppressLint("StringFormatInvalid")
private String getUserAgent() {
String appString = getResources().getString(R.string.user_agent);
String packageName = getPackageName();
String version = "";
PackageInfo pInfo;
try {
pInfo = getPackageManager().getPackageInfo(packageName, 0);
if (pInfo != null) {
version = pInfo.versionName;
}
} catch (PackageManager.NameNotFoundException e) {
Timber.e(e);
}
// Mozilla/5.0 (Android) ownCloud-android/1.7.0
return String.format(appString, version);
}
}