diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a542bfd5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" # Location of package manifests + schedule: + interval: "daily" + labels: + - "dependencies" + - package-ecosystem: "github-actions" + directory: "/" # Location of package manifests + schedule: + interval: "weekly" \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5cb2900a..619fd3be 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - kotlinVersion = '1.4.20' - moshiVersion = "1.9.2" + kotlinVersion = '1.4.31' + moshiVersion = "1.11.0" } repositories { @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef..e708b1c0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 391434d7..28ff446a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Sep 08 09:13:16 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/gradlew b/gradlew index 91a7e269..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,47 +22,6 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,12 +36,53 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -90,7 +106,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -110,11 +126,13 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" @@ -138,27 +156,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,20 +24,23 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,34 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/owncloudComLibrary/build.gradle b/owncloudComLibrary/build.gradle index 0a003a08..90ffbcd6 100644 --- a/owncloudComLibrary/build.gradle +++ b/owncloudComLibrary/build.gradle @@ -9,12 +9,13 @@ dependencies { api 'com.github.AppDevNext.Logcat:LogcatCore:2.2.2' // Moshi - implementation ("com.squareup.moshi:moshi-kotlin:$moshiVersion") { + implementation("com.squareup.moshi:moshi-kotlin:$moshiVersion") { exclude module: "kotlin-reflect" } kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion" - testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.3.1' } android { @@ -24,8 +25,8 @@ android { minSdkVersion 21 targetSdkVersion 29 - versionCode = 10000900 - versionName = "1.0.9" + versionCode = 10001000 + versionName = "1.0.10" } lintOptions { @@ -37,4 +38,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + testOptions { + unitTests { + includeAndroidResources = true + } + } } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java index 331ad7a0..8d27c34a 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClient.java @@ -48,15 +48,15 @@ import java.io.IOException; import java.io.InputStream; import java.util.List; +import static com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER; import static com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID; public class OwnCloudClient extends HttpClient { 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"; - private static final String WEBDAV_UPLOADS_PATH_4_0 = "/remote.php/dav/uploads/"; public static final String STATUS_PATH = "/status.php"; - + 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_REPEAT_COUNT_WITH_FRESH_CREDENTIALS = 1; @@ -104,8 +104,8 @@ public class OwnCloudClient extends HttpClient { method.setRequestHeader(HttpConstants.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(HttpConstants.AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); + if (mCredentials.getHeaderAuth() != null && method.getRequestHeader(AUTHORIZATION_HEADER) == null) { + method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); } status = method.execute(); @@ -136,7 +136,7 @@ public class OwnCloudClient extends HttpClient { method.setRequestHeader(HttpConstants.USER_AGENT_HEADER, SingleSessionManager.getUserAgent()); method.setRequestHeader(HttpConstants.ACCEPT_ENCODING_HEADER, HttpConstants.ACCEPT_ENCODING_IDENTITY); if (mCredentials.getHeaderAuth() != null) { - method.setRequestHeader(HttpConstants.AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); + method.setRequestHeader(AUTHORIZATION_HEADER, mCredentials.getHeaderAuth()); } status = method.execute(); diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java index d78e4d6a..948e8a07 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/OwnCloudClientFactory.java @@ -27,6 +27,9 @@ 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 { /** @@ -42,8 +45,14 @@ public class OwnCloudClientFactory { client.setFollowRedirects(followRedirects); - client.setContext(context); + HttpClient.setContext(context); + retrieveCookiesFromMiddleware(client); return client; } + + private static void retrieveCookiesFromMiddleware(OwnCloudClient client) { + final GetRemoteStatusOperation statusOperation = new GetRemoteStatusOperation(); + statusOperation.run(client); + } } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/SingleSessionManager.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/SingleSessionManager.java index e011a7fa..c410cc90 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/SingleSessionManager.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/SingleSessionManager.java @@ -24,8 +24,6 @@ package com.owncloud.android.lib.common; -import android.accounts.Account; -import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; @@ -37,7 +35,6 @@ import com.owncloud.android.lib.common.http.HttpClient; import timber.log.Timber; import java.io.IOException; -import java.util.Iterator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -111,6 +108,12 @@ public class SingleSessionManager { account.getBaseUri(), context.getApplicationContext(), true); // TODO remove dependency on OwnCloudClientFactory + + //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 + client.clearCookies(); + client.clearCredentials(); + client.setAccount(account); HttpClient.setContext(context); @@ -130,7 +133,6 @@ public class SingleSessionManager { Timber.v("reusing client for session %s", sessionName); } - keepCookiesUpdated(context, account, client); keepUriUpdated(account, client); } Timber.d("getClientFor finishing "); @@ -161,32 +163,6 @@ public class SingleSessionManager { Timber.d("removeClientFor finishing "); } - public void saveAllClients(Context context, String accountType) { - Timber.d("Saving sessions... "); - - Iterator accountNames = mClientsWithKnownUsername.keySet().iterator(); - String accountName; - Account account; - while (accountNames.hasNext()) { - accountName = accountNames.next(); - account = new Account(accountName, accountType); - AccountUtils.saveClient(mClientsWithKnownUsername.get(accountName), account, context); - } - - Timber.d("All sessions saved"); - } - - private void keepCookiesUpdated(Context context, OwnCloudAccount account, OwnCloudClient reusedClient) { - AccountManager am = AccountManager.get(context.getApplicationContext()); - if (am != null && account.getSavedAccount() != null) { - String recentCookies = am.getUserData(account.getSavedAccount(), AccountUtils.Constants.KEY_COOKIES); - String previousCookies = reusedClient.getCookiesString(); - if (recentCookies != null && !previousCookies.equals("") && !recentCookies.equals(previousCookies)) { - AccountUtils.restoreCookies(account.getSavedAccount(), reusedClient, context); - } - } - } - public void refreshCredentialsForAccount(String accountName, OwnCloudCredentials credentials) { OwnCloudClient ownCloudClient = mClientsWithKnownUsername.get(accountName); if (ownCloudClient == null) { diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java index d8983ed9..32e062ae 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/accounts/AccountUtils.java @@ -36,15 +36,10 @@ import android.net.Uri; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.authentication.OwnCloudCredentials; import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory; -import com.owncloud.android.lib.resources.files.FileUtils; import com.owncloud.android.lib.resources.status.OwnCloudVersion; -import okhttp3.Cookie; import timber.log.Timber; -import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; public class AccountUtils { /** @@ -202,64 +197,6 @@ public class AccountUtils { return username + "@" + url; } - public static void saveClient(OwnCloudClient client, Account savedAccount, Context context) { - // Account Manager - AccountManager ac = AccountManager.get(context.getApplicationContext()); - - if (client != null) { - String cookiesString = client.getCookiesString(); - if (!"".equals(cookiesString)) { - ac.setUserData(savedAccount, Constants.KEY_COOKIES, cookiesString); - Timber.d("Saving Cookies: %s", cookiesString); - } - } - } - - /** - * Restore the client cookies persisted in an account stored in the system AccountManager. - * - * @param account Stored account. - * @param client Client to restore cookies in. - * @param context Android context used to access the system AccountManager. - */ - public static void restoreCookies(Account account, OwnCloudClient client, Context context) { - if (account == null) { - Timber.d("Cannot restore cookie for null account"); - - } else { - Timber.d("Restoring cookies for %s", account.name); - - // Account Manager - AccountManager am = AccountManager.get(context.getApplicationContext()); - - Uri serverUri = (client.getBaseUri() != null) ? client.getBaseUri() : client.getUserFilesWebDavUri(); - - String cookiesString = am.getUserData(account, Constants.KEY_COOKIES); - if (cookiesString != null) { - String[] rawCookies = cookiesString.split(";"); - List cookieList = new ArrayList<>(rawCookies.length); - for (String rawCookie : rawCookies) { - rawCookie = rawCookie.replace(" ", ""); - final int equalPos = rawCookie.indexOf('='); - if (equalPos == -1) { - continue; - } - cookieList.add(new Cookie.Builder() - .name(rawCookie.substring(0, equalPos)) - .value(rawCookie.substring(equalPos + 1)) - .domain(serverUri.getHost()) - .path( - serverUri.getPath().equals("") - ? File.separator - : serverUri.getPath() - ) - .build()); - } - client.setCookiesForCurrentAccount(cookieList); - } - } - } - public static class AccountNotFoundException extends AccountsException { /** @@ -299,11 +236,6 @@ public class AccountUtils { public static final String OAUTH_SUPPORTED_TRUE = "TRUE"; - /** - * OC account cookies - */ - public static final String KEY_COOKIES = "oc_account_cookies"; - /** * OC account version */ diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/authentication/OwnCloudCredentialsFactory.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/authentication/OwnCloudCredentialsFactory.java index 3d5dae91..621a5c75 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/authentication/OwnCloudCredentialsFactory.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/authentication/OwnCloudCredentialsFactory.java @@ -24,10 +24,6 @@ package com.owncloud.android.lib.common.authentication; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.http.HttpClient; -import com.owncloud.android.lib.common.http.HttpConstants; - public class OwnCloudCredentialsFactory { public static final String CREDENTIAL_CHARSET = "UTF-8"; diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/CookieJarImpl.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/CookieJarImpl.kt new file mode 100644 index 00000000..ae76424e --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/CookieJarImpl.kt @@ -0,0 +1,63 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2021 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.http + +import okhttp3.Cookie +import okhttp3.CookieJar +import okhttp3.HttpUrl + +class CookieJarImpl( + private val sCookieStore: HashMap> +) : CookieJar { + + fun containsCookieWithName(cookies: List, name: String): Boolean { + for (cookie: Cookie in cookies) { + if (cookie.name == name) { + return true + } + } + return false + } + + fun getUpdatedCookies(oldCookies: List, newCookies: List): List { + val updatedList = ArrayList(newCookies) + for (oldCookie: Cookie in oldCookies) { + if (!containsCookieWithName(updatedList, oldCookie.name)) { + updatedList.add(oldCookie) + } + } + return updatedList + } + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + // Avoid duplicated cookies but update + val currentCookies: List = sCookieStore[url.host] ?: ArrayList() + val updatedCookies: List = getUpdatedCookies(currentCookies, cookies) + sCookieStore[url.host] = updatedCookies + } + + override fun loadForRequest(url: HttpUrl) = + sCookieStore[url.host] ?: ArrayList() + +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java index 06e8056d..046dc711 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java @@ -33,19 +33,18 @@ import okhttp3.CookieJar; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Protocol; +import okhttp3.TlsVersion; import timber.log.Timber; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -53,6 +52,7 @@ import java.util.concurrent.TimeUnit; * * @author David González Verdugo */ + public class HttpClient { private static OkHttpClient sOkHttpClient; private static Context sContext; @@ -64,66 +64,13 @@ public class HttpClient { try { final X509TrustManager trustManager = new AdvancedX509TrustManager( NetworkUtils.getKnownServersStore(sContext)); - - SSLContext sslContext; - - try { - sslContext = SSLContext.getInstance("TLSv1.3"); - } catch (NoSuchAlgorithmException tlsv13Exception) { - try { - Timber.w("TLSv1.3 is not supported in this device; falling through TLSv1.2"); - sslContext = SSLContext.getInstance("TLSv1.2"); - } catch (NoSuchAlgorithmException tlsv12Exception) { - try { - Timber.w("TLSv1.2 is not supported in this device; falling through TLSv1.1"); - sslContext = SSLContext.getInstance("TLSv1.1"); - } catch (NoSuchAlgorithmException tlsv11Exception) { - Timber.w("TLSv1.1 is not supported in this device; falling through TLSv1.0"); - sslContext = SSLContext.getInstance("TLSv1"); - // should be available in any device; see reference of supported protocols in - // http://developer.android.com/reference/javax/net/ssl/SSLSocket.html - } - } - } - - sslContext.init(null, new TrustManager[]{trustManager}, null); - - SSLSocketFactory sslSocketFactory; - - sslSocketFactory = sslContext.getSocketFactory(); - + final SSLSocketFactory sslSocketFactory = getNewSslSocketFactory(trustManager); // Automatic cookie handling, NOT PERSISTENT - CookieJar cookieJar = new CookieJar() { - @Override - public void saveFromResponse(HttpUrl url, List cookies) { - // Avoid duplicated cookies - Set nonDuplicatedCookiesSet = new HashSet<>(cookies); - List nonDuplicatedCookiesList = new ArrayList<>(nonDuplicatedCookiesSet); + final CookieJar cookieJar = new CookieJarImpl(sCookieStore); - sCookieStore.put(url.host(), nonDuplicatedCookiesList); - } - - @Override - public List loadForRequest(HttpUrl url) { - List cookies = sCookieStore.get(url.host()); - return cookies != null ? cookies : new ArrayList<>(); - } - }; - - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() - .addNetworkInterceptor(getLogInterceptor()) - .protocols(Arrays.asList(Protocol.HTTP_1_1)) - .readTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) - .writeTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) - .connectTimeout(HttpConstants.DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS) - .followRedirects(false) - .sslSocketFactory(sslSocketFactory, trustManager) - .hostnameVerifier((asdf, usdf) -> true) - .cookieJar(cookieJar); // TODO: Not verifying the hostname against certificate. ask owncloud security human if this is ok. //.hostnameVerifier(new BrowserCompatHostnameVerifier()); - - sOkHttpClient = clientBuilder.build(); + sOkHttpClient = buildNewOkHttpClient(sslSocketFactory, trustManager, cookieJar); } catch (Exception e) { Timber.e(e, "Could not setup SSL system."); @@ -132,6 +79,60 @@ public class HttpClient { return sOkHttpClient; } + private static SSLContext getSslContext() throws NoSuchAlgorithmException { + try { + return SSLContext.getInstance(TlsVersion.TLS_1_3.javaName()); + } catch (NoSuchAlgorithmException tlsv13Exception) { + try { + Timber.w("TLSv1.3 is not supported in this device; falling through TLSv1.2"); + return SSLContext.getInstance(TlsVersion.TLS_1_2.javaName()); + } catch (NoSuchAlgorithmException tlsv12Exception) { + try { + Timber.w("TLSv1.2 is not supported in this device; falling through TLSv1.1"); + return SSLContext.getInstance(TlsVersion.TLS_1_1.javaName()); + } catch (NoSuchAlgorithmException tlsv11Exception) { + Timber.w("TLSv1.1 is not supported in this device; falling through TLSv1.0"); + return SSLContext.getInstance(TlsVersion.TLS_1_0.javaName()); + // should be available in any device; see reference of supported protocols in + // http://developer.android.com/reference/javax/net/ssl/SSLSocket.html + } + } + } + } + + private static SSLSocketFactory getNewSslSocketFactory(X509TrustManager trustManager) + throws NoSuchAlgorithmException, KeyManagementException { + final SSLContext sslContext = getSslContext(); + sslContext.init(null, new TrustManager[]{trustManager}, null); + return sslContext.getSocketFactory(); + } + + private static OkHttpClient buildNewOkHttpClient(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager, + CookieJar cookieJar) { + return new OkHttpClient.Builder() + .addNetworkInterceptor(getLogInterceptor()) + .protocols(Collections.singletonList(Protocol.HTTP_1_1)) + .readTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) + .writeTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) + .connectTimeout(HttpConstants.DEFAULT_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS) + .followRedirects(false) + .sslSocketFactory(sslSocketFactory, trustManager) + .hostnameVerifier((asdf, usdf) -> true) + .cookieJar(cookieJar) + .build(); + } + + public static LogInterceptor getLogInterceptor() { + if (sLogInterceptor == null) { + sLogInterceptor = new LogInterceptor(); + } + return sLogInterceptor; + } + + public static List getCookiesFromUrl(HttpUrl httpUrl) { + return sCookieStore.get(httpUrl.host()); + } + public Context getContext() { return sContext; } @@ -140,17 +141,6 @@ public class HttpClient { sContext = context; } - public static LogInterceptor getLogInterceptor() { - if (sLogInterceptor == null) { - sLogInterceptor = new LogInterceptor(); - } - return sLogInterceptor; - } - - public List getCookiesFromUrl(HttpUrl httpUrl) { - return sCookieStore.get(httpUrl.host()); - } - public void clearCookies() { sCookieStore.clear(); } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java index b86534a3..ad8c6d1c 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java @@ -51,6 +51,12 @@ public class HttpConstants { public static final String ACCEPT_ENCODING_IDENTITY = "identity"; public static final String OC_FILE_REMOTE_ID = "OC-FileId"; + // OAuth + public static final String OAUTH_HEADER_AUTHORIZATION_CODE = "code"; + public static final String OAUTH_HEADER_GRANT_TYPE = "grant_type"; + public static final String OAUTH_HEADER_REDIRECT_URI = "redirect_uri"; + public static final String OAUTH_HEADER_REFRESH_TOKEN = "refresh_token"; + /*********************************************************************************************************** ************************************************ CONTENT TYPES ******************************************** ***********************************************************************************************************/ diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java index 321907b6..ef2a8321 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/operations/RemoteOperation.java @@ -258,11 +258,6 @@ public abstract class RemoteOperation implements Runnable { final RemoteOperationResult resultToSend = runOperation(); - if (mAccount != null && mContext != null) { - // Save Client Cookies - AccountUtils.saveClient(mClient, mAccount, mContext); - } - if (mListenerHandler != null && mListener != null) { mListenerHandler.post(() -> mListener.onRemoteOperationFinish(RemoteOperation.this, resultToSend)); diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/GetOIDCDiscoveryRemoteOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/GetOIDCDiscoveryRemoteOperation.kt new file mode 100644 index 00000000..9cebcf44 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/GetOIDCDiscoveryRemoteOperation.kt @@ -0,0 +1,93 @@ +/* ownCloud Android Library is available under MIT license + * + * @author Abel García de Prada + * + * 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.resources.oauth + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.oauth.responses.OIDCDiscoveryResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import timber.log.Timber +import java.net.URL + +/** + * Get OIDC Discovery + * + * @author Abel García de Prada + */ +class GetOIDCDiscoveryRemoteOperation : RemoteOperation() { + + override fun run(client: OwnCloudClient): RemoteOperationResult { + try { + val uriBuilder = client.baseUri.buildUpon().apply { + appendPath(WELL_KNOWN_PATH) // avoid starting "/" in this method + appendPath(OPENID_CONFIGURATION_RESOURCE) + }.build() + + val getMethod = GetMethod(URL(uriBuilder.toString())).apply { + addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE) + } + + val status = client.executeHttpMethod(getMethod) + + val responseBody = getMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_OK && responseBody != null) { + Timber.d("Successful response $responseBody") + + // Parse the response + val moshi: Moshi = Moshi.Builder().build() + val jsonAdapter: JsonAdapter = moshi.adapter(OIDCDiscoveryResponse::class.java) + val oidcDiscoveryResponse: OIDCDiscoveryResponse? = jsonAdapter.fromJson(responseBody) + Timber.d("Get OIDC Discovery completed and parsed to [$oidcDiscoveryResponse]") + + return RemoteOperationResult(RemoteOperationResult.ResultCode.OK).apply { + data = oidcDiscoveryResponse + } + + } else { + Timber.e("Failed response while getting OIDC server discovery from the server status code: $status; response message: $responseBody") + + return RemoteOperationResult(getMethod) + } + + } catch (e: Exception) { + Timber.e(e, "Exception while getting OIDC server discovery") + + return RemoteOperationResult(e) + } + } + + companion object { + private const val WELL_KNOWN_PATH = ".well-known" + private const val OPENID_CONFIGURATION_RESOURCE = "openid-configuration" + + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/RegisterClientRemoteOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/RegisterClientRemoteOperation.kt new file mode 100644 index 00000000..5d6482da --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/RegisterClientRemoteOperation.kt @@ -0,0 +1,85 @@ +/* ownCloud Android Library is available under MIT license + * + * @author Abel García de Prada + * + * Copyright (C) 2021 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.oauth + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.nonwebdav.PostMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.oauth.params.ClientRegistrationParams +import com.owncloud.android.lib.resources.oauth.responses.ClientRegistrationResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import timber.log.Timber +import java.net.URL + +class RegisterClientRemoteOperation( + private val clientRegistrationParams: ClientRegistrationParams +) : RemoteOperation() { + + override fun run(client: OwnCloudClient): RemoteOperationResult { + try { + val requestBody = clientRegistrationParams.toRequestBody() + + val postMethod = PostMethod( + url = URL(clientRegistrationParams.registrationEndpoint), + postRequestBody = requestBody + ) + + val status = client.executeHttpMethod(postMethod) + + val responseBody = postMethod.getResponseBodyAsString() + + if (status == HttpConstants.HTTP_CREATED && responseBody != null) { + Timber.d("Successful response $responseBody") + + // Parse the response + val moshi: Moshi = Moshi.Builder().build() + val jsonAdapter: JsonAdapter = + moshi.adapter(ClientRegistrationResponse::class.java) + val clientRegistrationResponse: ClientRegistrationResponse? = jsonAdapter.fromJson(responseBody) + Timber.d("Client registered and parsed to $clientRegistrationResponse") + + return RemoteOperationResult(RemoteOperationResult.ResultCode.OK).apply { + data = clientRegistrationResponse + } + + } else { + Timber.e("Failed response while registering a new client. Status code: $status; response message: $responseBody") + return RemoteOperationResult(postMethod) + } + + } catch (e: Exception) { + Timber.e(e, "Exception while registering a new client.") + return RemoteOperationResult(e) + + } + + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt new file mode 100644 index 00000000..67253c4c --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/TokenRequestRemoteOperation.kt @@ -0,0 +1,87 @@ +/* ownCloud Android Library is available under MIT license + * + * @author Abel García de Prada + * + * 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.resources.oauth + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER +import com.owncloud.android.lib.common.http.HttpConstants.HTTP_OK +import com.owncloud.android.lib.common.http.methods.nonwebdav.PostMethod +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.oauth.params.TokenRequestParams +import com.owncloud.android.lib.resources.oauth.responses.TokenResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import timber.log.Timber +import java.net.URL + +/** + * Perform token request + * + * @author Abel García de Prada + */ +class TokenRequestRemoteOperation( + private val tokenRequestParams: TokenRequestParams +) : RemoteOperation() { + + override fun run(client: OwnCloudClient): RemoteOperationResult { + try { + val requestBody = tokenRequestParams.toRequestBody() + + val postMethod = PostMethod(URL(tokenRequestParams.tokenEndpoint), requestBody) + + postMethod.addRequestHeader(AUTHORIZATION_HEADER, tokenRequestParams.clientAuth) + + val status = client.executeHttpMethod(postMethod) + + val responseBody = postMethod.getResponseBodyAsString() + + if (status == HTTP_OK && responseBody != null) { + Timber.d("Successful response $responseBody") + + // Parse the response + val moshi: Moshi = Moshi.Builder().build() + val jsonAdapter: JsonAdapter = moshi.adapter(TokenResponse::class.java) + val tokenResponse: TokenResponse? = jsonAdapter.fromJson(responseBody) + Timber.d("Get tokens completed and parsed to $tokenResponse") + + return RemoteOperationResult(RemoteOperationResult.ResultCode.OK).apply { + data = tokenResponse + } + + } else { + Timber.e("Failed response while getting tokens from the server status code: $status; response message: $responseBody") + return RemoteOperationResult(postMethod) + } + + } catch (e: Exception) { + Timber.e(e, "Exception while getting tokens") + return RemoteOperationResult(e) + + } + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/params/ClientRegistrationParams.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/params/ClientRegistrationParams.kt new file mode 100644 index 00000000..e26604a0 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/params/ClientRegistrationParams.kt @@ -0,0 +1,57 @@ +/* ownCloud Android Library is available under MIT license + * + * @author Abel García de Prada + * + * Copyright (C) 2021 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.oauth.params + +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONObject + +data class ClientRegistrationParams( + val registrationEndpoint: String, + val clientName: String, + val redirectUris: List, + val tokenEndpointAuthMethod: String, + val applicationType: String +) { + fun toRequestBody(): RequestBody = + JSONObject().apply { + put(PARAM_APPLICATION_TYPE, applicationType) + put(PARAM_CLIENT_NAME, clientName) + put(PARAM_REDIRECT_URIS, JSONArray(redirectUris)) + put(PARAM_TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthMethod) + }.toString().toRequestBody(CONTENT_TYPE_JSON.toMediaType()) + + companion object { + private const val PARAM_APPLICATION_TYPE = "application_type" + private const val PARAM_CLIENT_NAME = "client_name" + private const val PARAM_TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method" + private const val PARAM_REDIRECT_URIS = "redirect_uris" + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/params/TokenRequestParams.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/params/TokenRequestParams.kt new file mode 100644 index 00000000..367af463 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/params/TokenRequestParams.kt @@ -0,0 +1,69 @@ +/* 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.resources.oauth.params + +import com.owncloud.android.lib.common.http.HttpConstants +import okhttp3.FormBody +import okhttp3.RequestBody + +sealed class TokenRequestParams( + val tokenEndpoint: String, + val clientAuth: String, + val grantType: String +) { + abstract fun toRequestBody(): RequestBody + + class Authorization( + tokenEndpoint: String, + clientAuth: String, + grantType: String, + val authorizationCode: String, + val redirectUri: String + ) : TokenRequestParams(tokenEndpoint, clientAuth, grantType) { + + override fun toRequestBody(): RequestBody = + FormBody.Builder() + .add(HttpConstants.OAUTH_HEADER_AUTHORIZATION_CODE, authorizationCode) + .add(HttpConstants.OAUTH_HEADER_GRANT_TYPE, grantType) + .add(HttpConstants.OAUTH_HEADER_REDIRECT_URI, redirectUri) + .build() + } + + class RefreshToken( + tokenEndpoint: String, + clientAuth: String, + grantType: String, + val refreshToken: String? = null + ) : TokenRequestParams(tokenEndpoint, clientAuth, grantType) { + + override fun toRequestBody(): RequestBody = + FormBody.Builder().apply { + add(HttpConstants.OAUTH_HEADER_GRANT_TYPE, grantType) + if (!refreshToken.isNullOrBlank()) { + add(HttpConstants.OAUTH_HEADER_REFRESH_TOKEN, refreshToken) + } + }.build() + + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/ClientRegistrationResponse.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/ClientRegistrationResponse.kt new file mode 100644 index 00000000..2a1f3c05 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/ClientRegistrationResponse.kt @@ -0,0 +1,42 @@ +/* ownCloud Android Library is available under MIT license + * + * @author Abel García de Prada + * + * Copyright (C) 2021 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.oauth.responses + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ClientRegistrationResponse( + @Json(name = "client_id") + val clientId: String, + @Json(name = "client_secret") + val clientSecret: String?, + @Json(name = "client_id_issued_at") + val clientIdIssuedAt: Int?, + @Json(name = "client_secret_expires_at") + val clientSecretExpiration: Int, +) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/OIDCDiscoveryResponse.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/OIDCDiscoveryResponse.kt new file mode 100644 index 00000000..7072bc1f --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/OIDCDiscoveryResponse.kt @@ -0,0 +1,43 @@ +/* ownCloud Android Library is available under MIT license + * + * @author Abel García de Prada + * + * 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.resources.oauth.responses + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class OIDCDiscoveryResponse( + val authorization_endpoint: String, + val check_session_iframe: String, + val end_session_endpoint: String, + val issuer: String, + val registration_endpoint: String, + val response_types_supported: List, + val scopes_supported: List, + val token_endpoint: String, + val token_endpoint_auth_methods_supported: List, + val userinfo_endpoint: String, +) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/TokenResponse.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/TokenResponse.kt new file mode 100644 index 00000000..f7fdc87a --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/responses/TokenResponse.kt @@ -0,0 +1,45 @@ +/* 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.resources.oauth.responses + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TokenResponse( + @Json(name = "access_token") + val accessToken: String, + @Json(name = "expires_in") + val expiresIn: Int, + @Json(name = "refresh_token") + val refreshToken: String?, + @Json(name = "token_type") + val tokenType: String, + @Json(name = "user_id") + val userId: String?, + val scope: String?, + @Json(name = "additional_parameters") + val additionalParameters: Map? +) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/services/OIDCService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/services/OIDCService.kt new file mode 100644 index 00000000..d61fdd09 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/services/OIDCService.kt @@ -0,0 +1,47 @@ +/* ownCloud Android Library is available under MIT license + * + * Copyright (C) 2021 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.oauth.services + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.oauth.params.ClientRegistrationParams +import com.owncloud.android.lib.resources.oauth.params.TokenRequestParams +import com.owncloud.android.lib.resources.oauth.responses.ClientRegistrationResponse +import com.owncloud.android.lib.resources.oauth.responses.OIDCDiscoveryResponse +import com.owncloud.android.lib.resources.oauth.responses.TokenResponse + +interface OIDCService { + + fun getOIDCServerDiscovery(ownCloudClient: OwnCloudClient): RemoteOperationResult + + fun performTokenRequest( + ownCloudClient: OwnCloudClient, + tokenRequest: TokenRequestParams + ): RemoteOperationResult + + fun registerClientWithRegistrationEndpoint( + ownCloudClient: OwnCloudClient, + clientRegistrationParams: ClientRegistrationParams + ): RemoteOperationResult +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/services/implementation/OCOIDCService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/services/implementation/OCOIDCService.kt new file mode 100644 index 00000000..fe6b8fc9 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/oauth/services/implementation/OCOIDCService.kt @@ -0,0 +1,57 @@ +/* ownCloud Android Library is available under MIT license + * + * Copyright (C) 2021 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.oauth.services.implementation + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.oauth.GetOIDCDiscoveryRemoteOperation +import com.owncloud.android.lib.resources.oauth.RegisterClientRemoteOperation +import com.owncloud.android.lib.resources.oauth.TokenRequestRemoteOperation +import com.owncloud.android.lib.resources.oauth.params.ClientRegistrationParams +import com.owncloud.android.lib.resources.oauth.params.TokenRequestParams +import com.owncloud.android.lib.resources.oauth.responses.ClientRegistrationResponse +import com.owncloud.android.lib.resources.oauth.responses.OIDCDiscoveryResponse +import com.owncloud.android.lib.resources.oauth.responses.TokenResponse +import com.owncloud.android.lib.resources.oauth.services.OIDCService + +class OCOIDCService : OIDCService { + + override fun getOIDCServerDiscovery( + ownCloudClient: OwnCloudClient + ): RemoteOperationResult = + GetOIDCDiscoveryRemoteOperation().execute(ownCloudClient) + + override fun performTokenRequest( + ownCloudClient: OwnCloudClient, + tokenRequest: TokenRequestParams + ): RemoteOperationResult = + TokenRequestRemoteOperation(tokenRequest).execute(ownCloudClient) + + override fun registerClientWithRegistrationEndpoint( + ownCloudClient: OwnCloudClient, + clientRegistrationParams: ClientRegistrationParams + ): RemoteOperationResult = + RegisterClientRemoteOperation(clientRegistrationParams).execute(ownCloudClient) + +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/GetRemoteStatusOperation.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/GetRemoteStatusOperation.kt index f47544b1..f4e1d452 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/GetRemoteStatusOperation.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/GetRemoteStatusOperation.kt @@ -25,17 +25,15 @@ package com.owncloud.android.lib.resources.status import android.net.Uri import com.owncloud.android.lib.common.OwnCloudClient -import com.owncloud.android.lib.common.http.HttpConstants -import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult 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.HTTP_PREFIX +import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_SCHEME +import okhttp3.HttpUrl.Companion.toHttpUrl import org.json.JSONException -import org.json.JSONObject import timber.log.Timber -import java.net.URL -import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLException /** * Checks if the server is valid @@ -45,116 +43,46 @@ import javax.net.ssl.SSLException * @author David González Verdugo * @author Abel García de Prada */ -class GetRemoteStatusOperation : RemoteOperation() { - private lateinit var latestResult: RemoteOperationResult +class GetRemoteStatusOperation : RemoteOperation() { - override fun run(client: OwnCloudClient): RemoteOperationResult { + public override fun run(client: OwnCloudClient): RemoteOperationResult { + client.baseUri = buildFullHttpsUrl(client.baseUri) - val baseUriStr = client.baseUri.toString() - if (baseUriStr.startsWith(HTTP_PREFIX) || baseUriStr.startsWith( - HTTPS_PREFIX - )) { - tryConnection(client) - } else { - client.baseUri = Uri.parse(HTTPS_PREFIX + baseUriStr) - val httpsSuccess = tryConnection(client) - if (!httpsSuccess && !latestResult.isSslRecoverableException) { - Timber.d("Establishing secure connection failed, trying non secure connection") - client.baseUri = Uri.parse(HTTP_PREFIX + baseUriStr) - tryConnection(client) - } + var result = 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 latestResult + + return result } - private fun tryConnection(client: OwnCloudClient): Boolean { - var successfulConnection = false - val baseUrlSt = client.baseUri.toString() - try { - var getMethod = GetMethod(URL(baseUrlSt + OwnCloudClient.STATUS_PATH)).apply { - setReadTimeout(TRY_CONNECTION_TIMEOUT, TimeUnit.SECONDS) - setConnectionTimeout(TRY_CONNECTION_TIMEOUT, TimeUnit.SECONDS) - } - client.setFollowRedirects(false) - var isRedirectToNonSecureConnection = false - var status: Int - try { - status = client.executeHttpMethod(getMethod) - latestResult = - if (isSuccess(status)) RemoteOperationResult(ResultCode.OK) - else RemoteOperationResult(getMethod) - - } catch (sslE: SSLException) { - latestResult = RemoteOperationResult(sslE) - return successfulConnection - } - - var redirectedLocation = latestResult.redirectedLocation - while (!redirectedLocation.isNullOrEmpty() && !latestResult.isSuccess) { - isRedirectToNonSecureConnection = - isRedirectToNonSecureConnection || - (baseUrlSt.startsWith(HTTPS_PREFIX) && redirectedLocation.startsWith( - HTTP_PREFIX - )) - - getMethod = GetMethod(URL(redirectedLocation)).apply { - setReadTimeout(TRY_CONNECTION_TIMEOUT, TimeUnit.SECONDS) - setConnectionTimeout(TRY_CONNECTION_TIMEOUT, TimeUnit.SECONDS) - } - - status = client.executeHttpMethod(getMethod) - latestResult = RemoteOperationResult(getMethod) - redirectedLocation = latestResult.redirectedLocation - } - - if (isSuccess(status)) { - val respJSON = JSONObject(getMethod.getResponseBodyAsString()) - if (!respJSON.getBoolean(NODE_INSTALLED)) { - latestResult = RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED) - } else { - val version = respJSON.getString(NODE_VERSION) - val ocVersion = OwnCloudVersion(version) - // 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) - latestResult = if (isRedirectToNonSecureConnection) { - RemoteOperationResult(ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION) - } else { - if (baseUrlSt.startsWith(HTTPS_PREFIX)) RemoteOperationResult(ResultCode.OK_SSL) - else RemoteOperationResult(ResultCode.OK_NO_SSL) - } - latestResult.data = ocVersion - successfulConnection = true - } - } else { - latestResult = RemoteOperationResult(getMethod) + private fun tryToConnect(client: OwnCloudClient): RemoteOperationResult { + val baseUrl = client.baseUri.toString() + client.setFollowRedirects(false) + return try { + val requester = StatusRequester() + val requestResult = requester.requestAndFollowRedirects(baseUrl, client) + requester.handleRequestResult(requestResult, baseUrl).also { + client.baseUri = Uri.parse(it.data.baseUrl) } } catch (e: JSONException) { - latestResult = RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED) + RemoteOperationResult(ResultCode.INSTANCE_NOT_CONFIGURED) } catch (e: Exception) { - latestResult = RemoteOperationResult(e) + RemoteOperationResult(e) } - when { - latestResult.isSuccess -> Timber.i("Connection check at $baseUrlSt successful: ${latestResult.logMessage}") - - latestResult.isException -> - Timber.e(latestResult.exception, "Connection check at $baseUrlSt: ${latestResult.logMessage}") - - else -> Timber.e("Connection check at $baseUrlSt failed: ${latestResult.logMessage}") - } - return successfulConnection } - private fun isSuccess(status: Int): Boolean = status == HttpConstants.HTTP_OK - companion object { - /** - * Maximum time to wait for a response from the server when the connection is being tested, - * in MILLISECONDs. - */ - private const val TRY_CONNECTION_TIMEOUT: Long = 5000 - private const val NODE_INSTALLED = "installed" - private const val NODE_VERSION = "version" - private const val HTTPS_PREFIX = "https://" - private const val HTTP_PREFIX = "http://" + fun usesHttpOrHttps(uri: Uri) = + uri.toString().startsWith(HTTPS_PREFIX) || uri.toString().startsWith(HTTP_PREFIX) + + fun buildFullHttpsUrl(baseUri: Uri): Uri { + if (usesHttpOrHttps(baseUri)) { + return baseUri + } + return Uri.parse("$HTTPS_PREFIX$baseUri") + } } } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/HttpScheme.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/HttpScheme.kt new file mode 100644 index 00000000..ff3e3662 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/HttpScheme.kt @@ -0,0 +1,32 @@ +/* ownCloud Android Library is available under MIT license +* Copyright (C) 2021 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.status + +object HttpScheme { + const val HTTP_SCHEME = "http" + const val HTTPS_SCHEME = "https" + const val HTTP_PREFIX = "$HTTP_SCHEME://" + const val HTTPS_PREFIX = "$HTTPS_SCHEME://" +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/RemoteServerInfo.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/RemoteServerInfo.kt new file mode 100644 index 00000000..7163fd4a --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/RemoteServerInfo.kt @@ -0,0 +1,30 @@ +/* ownCloud Android Library is available under MIT license +* Copyright (C) 2021 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.status + +data class RemoteServerInfo( + val ownCloudVersion: OwnCloudVersion, + val baseUrl: String, + val isSecureConnection: Boolean +) diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/StatusRequester.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/StatusRequester.kt new file mode 100644 index 00000000..8706a185 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/StatusRequester.kt @@ -0,0 +1,156 @@ +/* ownCloud Android Library is available under MIT license +* Copyright (C) 2021 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.status + +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.http.HttpConstants +import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.resources.status.HttpScheme.HTTPS_SCHEME +import org.json.JSONObject +import java.net.URL +import java.util.concurrent.TimeUnit + +internal class StatusRequester { + + /** + * This function is ment to detect if a redirect from a secure to an unsecure connection + * was made. If only connections from unsecure connections to unsecure connections were made + * this function should not return true, because if the whole redirect chain was unsecure + * we assume it was a debug setup. + */ + fun isRedirectedToNonSecureConnection( + redirectedToNonSecureLocationBefore: Boolean, + baseUrl: String, + redirectedUrl: String + ) = redirectedToNonSecureLocationBefore + || (baseUrl.startsWith(HTTPS_SCHEME) + && !redirectedUrl.startsWith(HTTPS_SCHEME)) + + fun updateLocationWithRedirectPath(oldLocation: String, redirectedLocation: String): String { + /** Redirection with different endpoint. + * When asking for server.com/status.php and redirected to different.one/, we need to ask different.one/status.php + */ + if (redirectedLocation.endsWith('/')) { + return redirectedLocation.trimEnd('/') + OwnCloudClient.STATUS_PATH + } + + if (!redirectedLocation.startsWith("/")) + return redirectedLocation + val oldLocationURL = URL(oldLocation) + return URL(oldLocationURL.protocol, oldLocationURL.host, oldLocationURL.port, redirectedLocation).toString() + } + + private fun getGetMethod(url: String): GetMethod { + return GetMethod(URL(url)).apply { + setReadTimeout(TRY_CONNECTION_TIMEOUT, TimeUnit.SECONDS) + setConnectionTimeout(TRY_CONNECTION_TIMEOUT, TimeUnit.SECONDS) + } + } + + data class RequestResult( + val getMethod: GetMethod, + val status: Int, + val redirectedToUnsecureLocation: Boolean, + val lastLocation: String + ) + + fun requestAndFollowRedirects(baseLocation: String, client: OwnCloudClient): RequestResult { + var currentLocation = baseLocation + OwnCloudClient.STATUS_PATH + var redirectedToUnsecureLocation = false + var status: Int + + while (true) { + val getMethod = getGetMethod(currentLocation) + + status = client.executeHttpMethod(getMethod) + val result = + if (status.isSuccess()) RemoteOperationResult(RemoteOperationResult.ResultCode.OK) + else RemoteOperationResult(getMethod) + + if (result.redirectedLocation.isNullOrEmpty() || result.isSuccess) { + 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 + + fun handleRequestResult( + requestResult: RequestResult, + baseUrl: String + ): RemoteOperationResult { + if (!requestResult.status.isSuccess()) + return RemoteOperationResult(requestResult.getMethod) + + val respJSON = JSONObject(requestResult.getMethod.getResponseBodyAsString() ?: "") + if (!respJSON.getBoolean(NODE_INSTALLED)) + return RemoteOperationResult(RemoteOperationResult.ResultCode.INSTANCE_NOT_CONFIGURED) + + val ocVersion = OwnCloudVersion(respJSON.getString(NODE_VERSION)) + // 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) + val result: RemoteOperationResult = + if (requestResult.redirectedToUnsecureLocation) { + RemoteOperationResult(RemoteOperationResult.ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION) + } else { + if (baseUrl.startsWith(HTTPS_SCHEME)) RemoteOperationResult(RemoteOperationResult.ResultCode.OK_SSL) + else RemoteOperationResult(RemoteOperationResult.ResultCode.OK_NO_SSL) + } + val finalUrl = URL(requestResult.lastLocation) + val finalBaseUrl = URL( + finalUrl.protocol, + finalUrl.host, + finalUrl.port, + finalUrl.file.dropLastWhile { it != '/' }.trimEnd('/') + ) + + result.data = RemoteServerInfo( + ownCloudVersion = ocVersion, + baseUrl = finalBaseUrl.toString(), + isSecureConnection = finalBaseUrl.protocol.startsWith(HTTPS_SCHEME) + ) + return result + } + + companion object { + /** + * Maximum time to wait for a response from the server when the connection is being tested, + * in milliseconds. + */ + private const val TRY_CONNECTION_TIMEOUT = 5_000L + private const val NODE_INSTALLED = "installed" + private const val NODE_VERSION = "version" + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/ServerInfoService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/ServerInfoService.kt index 1efa7f1b..911b2b80 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/ServerInfoService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/ServerInfoService.kt @@ -23,11 +23,12 @@ */ package com.owncloud.android.lib.resources.status.services +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.resources.status.OwnCloudVersion +import com.owncloud.android.lib.resources.status.RemoteServerInfo interface ServerInfoService { - fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult + fun checkPathExistence(path: String, isUserLogged: Boolean, client: OwnCloudClient): RemoteOperationResult - fun getRemoteStatus(path: String): RemoteOperationResult + fun getRemoteStatus(path: String, client: OwnCloudClient): RemoteOperationResult } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/implementation/OCServerInfoService.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/implementation/OCServerInfoService.kt index 76f82a46..0dbae093 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/implementation/OCServerInfoService.kt +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/resources/status/services/implementation/OCServerInfoService.kt @@ -19,26 +19,28 @@ package com.owncloud.android.lib.resources.status.services.implementation -import android.net.Uri import com.owncloud.android.lib.common.OwnCloudClient -import com.owncloud.android.lib.common.authentication.OwnCloudCredentialsFactory.getAnonymousCredentials import com.owncloud.android.lib.common.operations.RemoteOperationResult -import com.owncloud.android.lib.resources.status.services.ServerInfoService import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation -import com.owncloud.android.lib.resources.status.OwnCloudVersion +import com.owncloud.android.lib.resources.status.RemoteServerInfo +import com.owncloud.android.lib.resources.status.services.ServerInfoService class OCServerInfoService : ServerInfoService { - override fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult = + + override fun checkPathExistence( + path: String, + isUserLogged: Boolean, + client: OwnCloudClient + ): RemoteOperationResult = CheckPathExistenceRemoteOperation( remotePath = path, isUserLogged = true - ).execute(createClientFromPath(path)) + ).execute(client) - override fun getRemoteStatus(path: String): RemoteOperationResult = - GetRemoteStatusOperation().execute(createClientFromPath(path)) - - private fun createClientFromPath(path: String): OwnCloudClient { - return OwnCloudClient(Uri.parse(path)).apply { credentials = getAnonymousCredentials() } - } + override fun getRemoteStatus( + path: String, + client: OwnCloudClient + ): RemoteOperationResult = + GetRemoteStatusOperation().execute(client) } diff --git a/owncloudComLibrary/src/test/java/com/owncloud/android/lib/CookieJarImplTest.kt b/owncloudComLibrary/src/test/java/com/owncloud/android/lib/CookieJarImplTest.kt new file mode 100644 index 00000000..7e1d7691 --- /dev/null +++ b/owncloudComLibrary/src/test/java/com/owncloud/android/lib/CookieJarImplTest.kt @@ -0,0 +1,84 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2021 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 + +import com.owncloud.android.lib.common.http.CookieJarImpl +import okhttp3.Cookie +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class CookieJarImplTest { + + private val oldCookies = listOf(COOKIE_A, COOKIE_B_OLD) + private val newCookies = listOf(COOKIE_B_NEW) + private val updatedCookies = listOf(COOKIE_A, COOKIE_B_NEW) + private val cookieStore = hashMapOf(SOME_HOST to oldCookies) + + private val cookieJarImpl = CookieJarImpl(cookieStore) + + @Test + fun `contains cookie with name - ok - true`() { + assertTrue(cookieJarImpl.containsCookieWithName(oldCookies, COOKIE_B_OLD.name)) + } + + @Test + fun `contains cookie with name - ok - false`() { + assertFalse(cookieJarImpl.containsCookieWithName(newCookies, COOKIE_A.name)) + } + + @Test + fun `get updated cookies - ok`() { + val generatedUpdatedCookies = cookieJarImpl.getUpdatedCookies(oldCookies, newCookies) + assertEquals(2, generatedUpdatedCookies.size) + assertEquals(updatedCookies[0], generatedUpdatedCookies[1]) + assertEquals(updatedCookies[1], generatedUpdatedCookies[0]) + } + + @Test + fun `store cookie via saveFromResponse - ok`() { + cookieJarImpl.saveFromResponse(SOME_URL, newCookies) + val generatedUpdatedCookies = cookieStore[SOME_HOST] + assertEquals(2, generatedUpdatedCookies?.size) + assertEquals(updatedCookies[0], generatedUpdatedCookies?.get(1)) + assertEquals(updatedCookies[1], generatedUpdatedCookies?.get(0)) + } + + @Test + fun `load for request - ok`() { + val cookies = cookieJarImpl.loadForRequest(SOME_URL) + assertEquals(oldCookies[0], cookies[0]) + assertEquals(oldCookies[1], cookies[1]) + } + + companion object { + const val SOME_HOST = "some.host.com" + val SOME_URL = "https://$SOME_HOST".toHttpUrl() + val COOKIE_A = Cookie.parse(SOME_URL, "CookieA=CookieValueA")!! + val COOKIE_B_OLD = Cookie.parse(SOME_URL, "CookieB=CookieOldValueB")!! + val COOKIE_B_NEW = Cookie.parse(SOME_URL, "CookieB=CookieNewValueB")!! + } +} diff --git a/owncloudComLibrary/src/test/java/com/owncloud/android/lib/GetRemoteStatusOperationTest.kt b/owncloudComLibrary/src/test/java/com/owncloud/android/lib/GetRemoteStatusOperationTest.kt new file mode 100644 index 00000000..375e9aa5 --- /dev/null +++ b/owncloudComLibrary/src/test/java/com/owncloud/android/lib/GetRemoteStatusOperationTest.kt @@ -0,0 +1,145 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2021 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 + +import android.net.Uri +import android.os.Build +import com.owncloud.android.lib.resources.status.GetRemoteStatusOperation +import com.owncloud.android.lib.resources.status.HttpScheme.HTTPS_PREFIX +import com.owncloud.android.lib.resources.status.HttpScheme.HTTP_PREFIX +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.O], manifest = Config.NONE) +class GetRemoteStatusOperationTest { + + @Test + fun `uses http or https - ok - http`() { + assertTrue(GetRemoteStatusOperation.usesHttpOrHttps(Uri.parse(HTTP_SOME_OWNCLOUD))) + } + + @Test + fun `uses http or https - ok - https`() { + assertTrue(GetRemoteStatusOperation.usesHttpOrHttps(Uri.parse(HTTPS_SOME_OWNCLOUD))) + } + + @Test + fun `uses http or https - ok - no http or https`() { + assertFalse(GetRemoteStatusOperation.usesHttpOrHttps(Uri.parse(SOME_OWNCLOUD))) + } + + @Test + fun `build full https url - ok - http`() { + assertEquals( + Uri.parse(HTTP_SOME_OWNCLOUD), + GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(HTTP_SOME_OWNCLOUD)) + ) + } + + @Test + fun `build full https url - ok - https`() { + assertEquals( + Uri.parse(HTTPS_SOME_OWNCLOUD), + GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(HTTPS_SOME_OWNCLOUD)) + ) + } + + @Test + fun `build full https url - ok - no prefix`() { + assertEquals( + Uri.parse(HTTPS_SOME_OWNCLOUD), + GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(SOME_OWNCLOUD)) + ) + } + + @Test + fun `build full https url - ok - no https with subdir`() { + assertEquals( + Uri.parse(HTTPS_SOME_OWNCLOUD_WITH_SUBDIR), + GetRemoteStatusOperation.buildFullHttpsUrl( + Uri.parse(HTTPS_SOME_OWNCLOUD_WITH_SUBDIR) + ) + ) + } + + @Test + fun `build full https url - ok - no prefix with subdir`() { + assertEquals( + Uri.parse(HTTPS_SOME_OWNCLOUD_WITH_SUBDIR), + GetRemoteStatusOperation.buildFullHttpsUrl( + Uri.parse(SOME_OWNCLOUD_WITH_SUBDIR) + ) + ) + } + + @Test + fun `build full https url - ok - ip`() { + assertEquals(Uri.parse(HTTPS_SOME_IP), GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(SOME_IP))) + } + + @Test + fun `build full https url - ok - http ip`() { + assertEquals(Uri.parse(HTTP_SOME_IP), GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(HTTP_SOME_IP))) + } + + @Test + fun `build full https url - ok - ip with port`() { + assertEquals( + Uri.parse(HTTPS_SOME_IP_WITH_PORT), + GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(SOME_IP_WITH_PORT)) + ) + } + + @Test + fun `build full https url - ok - ip with http and port`() { + assertEquals( + Uri.parse(HTTP_SOME_IP_WITH_PORT), + GetRemoteStatusOperation.buildFullHttpsUrl(Uri.parse(HTTP_SOME_IP_WITH_PORT)) + ) + } + + companion object { + const val SOME_OWNCLOUD = "some_owncloud.com" + const val HTTP_SOME_OWNCLOUD = "$HTTP_PREFIX$SOME_OWNCLOUD" + const val HTTPS_SOME_OWNCLOUD = "$HTTPS_PREFIX$SOME_OWNCLOUD" + + const val SOME_OWNCLOUD_WITH_SUBDIR = "some_owncloud.com/subdir" + const val HTTP_SOME_OWNCLOUD_WITH_SUBDIR = "$HTTP_PREFIX$SOME_OWNCLOUD_WITH_SUBDIR" + const val HTTPS_SOME_OWNCLOUD_WITH_SUBDIR = "$HTTPS_PREFIX$SOME_OWNCLOUD_WITH_SUBDIR" + + const val SOME_IP = "184.123.185.12" + const val HTTP_SOME_IP = "$HTTP_PREFIX$SOME_IP" + const val HTTPS_SOME_IP = "$HTTPS_PREFIX$SOME_IP" + + const val SOME_IP_WITH_PORT = "184.123.185.12:5678" + const val HTTP_SOME_IP_WITH_PORT = "$HTTP_PREFIX$SOME_IP_WITH_PORT" + const val HTTPS_SOME_IP_WITH_PORT = "$HTTPS_PREFIX$SOME_IP_WITH_PORT" + } +} diff --git a/owncloudComLibrary/src/test/java/com/owncloud/android/lib/StatusRequesterTest.kt b/owncloudComLibrary/src/test/java/com/owncloud/android/lib/StatusRequesterTest.kt new file mode 100644 index 00000000..20b1c668 --- /dev/null +++ b/owncloudComLibrary/src/test/java/com/owncloud/android/lib/StatusRequesterTest.kt @@ -0,0 +1,104 @@ +/* ownCloud Android Library is available under MIT license +* Copyright (C) 2021 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 + +import com.owncloud.android.lib.resources.status.StatusRequester +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class StatusRequesterTest { + private val requester = StatusRequester() + + @Test + fun `update location - ok - absolute path`() { + val newLocation = requester.updateLocationWithRedirectPath(TEST_DOMAIN, "$TEST_DOMAIN$SUB_PATH") + assertEquals("$TEST_DOMAIN$SUB_PATH", newLocation) + } + + @Test + fun `update location - ok - smaller absolute path`() { + val newLocation = requester.updateLocationWithRedirectPath("$TEST_DOMAIN$SUB_PATH", TEST_DOMAIN) + assertEquals(TEST_DOMAIN, newLocation) + } + + @Test + fun `update location - ok - relative path`() { + val newLocation = requester.updateLocationWithRedirectPath(TEST_DOMAIN, SUB_PATH) + assertEquals("$TEST_DOMAIN$SUB_PATH", newLocation) + } + + @Test + fun `update location - ok - replace relative path`() { + val newLocation = requester.updateLocationWithRedirectPath("$TEST_DOMAIN/some/other/subdir", SUB_PATH) + assertEquals("$TEST_DOMAIN$SUB_PATH", newLocation) + } + + @Test + fun `check redirect to unsecure connection - ok - redirect to http`() { + assertTrue( + requester.isRedirectedToNonSecureConnection(false, SECURE_DOMAIN, UNSECURE_DOMAIN + ) + ) + } + + @Test + fun `check redirect to unsecure connection - ko - redirect to https from http`() { + assertFalse( + requester.isRedirectedToNonSecureConnection(false, UNSECURE_DOMAIN, SECURE_DOMAIN + ) + ) + } + + @Test + fun `check redirect to unsecure connection - ko - from https to https`() { + assertFalse( + requester.isRedirectedToNonSecureConnection(false, SECURE_DOMAIN, SECURE_DOMAIN) + ) + } + + @Test + fun `check redirect to unsecure connection - ok - from https to https with previous http`() { + assertTrue( + requester.isRedirectedToNonSecureConnection(true, SECURE_DOMAIN, SECURE_DOMAIN) + ) + } + + @Test + fun `check redirect to unsecure connection - ok - from http to http`() { + assertFalse( + requester.isRedirectedToNonSecureConnection(false, UNSECURE_DOMAIN, UNSECURE_DOMAIN) + ) + } + + companion object { + const val TEST_DOMAIN = "https://cloud.somewhere.com" + const val SUB_PATH = "/subdir" + + const val SECURE_DOMAIN = "https://cloud.somewhere.com" + const val UNSECURE_DOMAIN = "http://somewhereelse.org" + } +}