From: masensio Date: Thu, 7 Nov 2013 17:31:54 +0000 (+0100) Subject: OC-1868: Create library project. Add classes to Create FolderOperation X-Git-Tag: oc-android-1.5.5~123^2~20 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/f297a376699635eb071b2d0d5a170d205c2aa2a5 OC-1868: Create library project. Add classes to Create FolderOperation --- diff --git a/oc_framework/.classpath b/oc_framework/.classpath new file mode 100644 index 00000000..7bc01d9a --- /dev/null +++ b/oc_framework/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/oc_framework/.project b/oc_framework/.project new file mode 100644 index 00000000..18812a0d --- /dev/null +++ b/oc_framework/.project @@ -0,0 +1,33 @@ + + + oc_framework + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/oc_framework/.settings/org.eclipse.jdt.core.prefs b/oc_framework/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b080d2dd --- /dev/null +++ b/oc_framework/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/oc_framework/AndroidManifest.xml b/oc_framework/AndroidManifest.xml new file mode 100644 index 00000000..bf727ad8 --- /dev/null +++ b/oc_framework/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/oc_framework/libs/android-support-v4.jar b/oc_framework/libs/android-support-v4.jar new file mode 100644 index 00000000..9056828a Binary files /dev/null and b/oc_framework/libs/android-support-v4.jar differ diff --git a/oc_framework/libs/jackrabbit-webdav-2.2.5-jar-with-dependencies.jar b/oc_framework/libs/jackrabbit-webdav-2.2.5-jar-with-dependencies.jar new file mode 100644 index 00000000..2dd374f9 Binary files /dev/null and b/oc_framework/libs/jackrabbit-webdav-2.2.5-jar-with-dependencies.jar differ diff --git a/oc_framework/project.properties b/oc_framework/project.properties new file mode 100644 index 00000000..484dab07 --- /dev/null +++ b/oc_framework/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/oc_framework/res/drawable-hdpi/ic_launcher.png b/oc_framework/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..96a442e5 Binary files /dev/null and b/oc_framework/res/drawable-hdpi/ic_launcher.png differ diff --git a/oc_framework/res/drawable-mdpi/ic_launcher.png b/oc_framework/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..359047df Binary files /dev/null and b/oc_framework/res/drawable-mdpi/ic_launcher.png differ diff --git a/oc_framework/res/drawable-xhdpi/ic_launcher.png b/oc_framework/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..71c6d760 Binary files /dev/null and b/oc_framework/res/drawable-xhdpi/ic_launcher.png differ diff --git a/oc_framework/res/values-v11/styles.xml b/oc_framework/res/values-v11/styles.xml new file mode 100644 index 00000000..3c02242a --- /dev/null +++ b/oc_framework/res/values-v11/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/oc_framework/res/values-v14/styles.xml b/oc_framework/res/values-v14/styles.xml new file mode 100644 index 00000000..a91fd037 --- /dev/null +++ b/oc_framework/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/oc_framework/res/values/setup.xml b/oc_framework/res/values/setup.xml new file mode 100644 index 00000000..02fdf08d --- /dev/null +++ b/oc_framework/res/values/setup.xml @@ -0,0 +1,6 @@ + + + owncloud + org.owncloud + + diff --git a/oc_framework/res/values/strings.xml b/oc_framework/res/values/strings.xml new file mode 100644 index 00000000..da5efb93 --- /dev/null +++ b/oc_framework/res/values/strings.xml @@ -0,0 +1,5 @@ + + + oc_framework + + diff --git a/oc_framework/res/values/styles.xml b/oc_framework/res/values/styles.xml new file mode 100644 index 00000000..6ce89c7b --- /dev/null +++ b/oc_framework/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/oc_framework/src/com/owncloud/android/oc_framework/MainApp.java b/oc_framework/src/com/owncloud/android/oc_framework/MainApp.java new file mode 100644 index 00000000..8783daff --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/MainApp.java @@ -0,0 +1,62 @@ +package com.owncloud.android.oc_framework; + +import android.app.Application; +import android.content.Context; + +public class MainApp extends Application { + + private static Context mContext; + + public void onCreate(){ + super.onCreate(); + MainApp.mContext = getApplicationContext(); + } + + public static Context getAppContext() { + return MainApp.mContext; + } + + // Methods to obtain Strings referring app_name + // From AccountAuthenticator + // public static final String ACCOUNT_TYPE = "owncloud"; + public static String getAccountType() { + return getAppContext().getResources().getString(R.string.account_type); + } + + // From AccountAuthenticator + // public static final String AUTHORITY = "org.owncloud"; + public static String getAuthority() { + return getAppContext().getResources().getString(R.string.authority); + } + + // From AccountAuthenticator + // public static final String AUTH_TOKEN_TYPE = "org.owncloud"; + public static String getAuthTokenType() { + return getAppContext().getResources().getString(R.string.authority); + } + + // From AccountAuthenticator + // public static final String AUTH_TOKEN_TYPE_PASSWORD = "owncloud.password"; + public static String getAuthTokenTypePass() { + return getAppContext().getResources().getString(R.string.account_type) + ".password"; + } + + // From AccountAuthenticator + // public static final String AUTH_TOKEN_TYPE_ACCESS_TOKEN = "owncloud.oauth2.access_token"; + public static String getAuthTokenTypeAccessToken() { + return getAppContext().getResources().getString(R.string.account_type) + ".oauth2.access_token"; + } + + // From AccountAuthenticator + // public static final String AUTH_TOKEN_TYPE_REFRESH_TOKEN = "owncloud.oauth2.refresh_token"; + public static String getAuthTokenTypeRefreshToken() { + return getAppContext().getResources().getString(R.string.account_type) + ".oauth2.refresh_token"; + } + + // From AccountAuthenticator + // public static final String AUTH_TOKEN_TYPE_SAML_WEB_SSO_SESSION_COOKIE = "owncloud.saml.web_sso.session_cookie"; + public static String getAuthTokenTypeSamlSessionCookie() { + return getAppContext().getResources().getString(R.string.account_type) + ".saml.web_sso.session_cookie"; + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/OwnCloudVersion.java b/oc_framework/src/com/owncloud/android/oc_framework/OwnCloudVersion.java new file mode 100644 index 00000000..5686e383 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/OwnCloudVersion.java @@ -0,0 +1,85 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework; + +public class OwnCloudVersion implements Comparable { + public static final OwnCloudVersion owncloud_v1 = new OwnCloudVersion( + 0x010000); + public static final OwnCloudVersion owncloud_v2 = new OwnCloudVersion( + 0x020000); + public static final OwnCloudVersion owncloud_v3 = new OwnCloudVersion( + 0x030000); + public static final OwnCloudVersion owncloud_v4 = new OwnCloudVersion( + 0x040000); + public static final OwnCloudVersion owncloud_v4_5 = new OwnCloudVersion( + 0x040500); + + // format is in version + // 0xAABBCC + // for version AA.BB.CC + // ie version 2.0.3 will be stored as 0x030003 + private int mVersion; + private boolean mIsValid; + + public OwnCloudVersion(int version) { + mVersion = version; + mIsValid = true; + } + + public OwnCloudVersion(String version) { + mVersion = 0; + mIsValid = false; + parseVersionString(version); + } + + public String toString() { + return ((mVersion >> 16) % 256) + "." + ((mVersion >> 8) % 256) + "." + + ((mVersion) % 256); + } + + public boolean isVersionValid() { + return mIsValid; + } + + @Override + public int compareTo(OwnCloudVersion another) { + return another.mVersion == mVersion ? 0 + : another.mVersion < mVersion ? 1 : -1; + } + + private void parseVersionString(String version) { + try { + String[] nums = version.split("\\."); + if (nums.length > 0) { + mVersion += Integer.parseInt(nums[0]); + } + mVersion = mVersion << 8; + if (nums.length > 1) { + mVersion += Integer.parseInt(nums[1]); + } + mVersion = mVersion << 8; + if (nums.length > 2) { + mVersion += Integer.parseInt(nums[2]); + } + mIsValid = true; + } catch (Exception e) { + mIsValid = false; + } + } +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/authentication/AccountAuthenticatorConstants.java b/oc_framework/src/com/owncloud/android/oc_framework/authentication/AccountAuthenticatorConstants.java new file mode 100644 index 00000000..bcd4f5cf --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/authentication/AccountAuthenticatorConstants.java @@ -0,0 +1,38 @@ +package com.owncloud.android.oc_framework.authentication; + +public class AccountAuthenticatorConstants { + + public static final String KEY_AUTH_TOKEN_TYPE = "authTokenType"; + public static final String KEY_REQUIRED_FEATURES = "requiredFeatures"; + public static final String KEY_LOGIN_OPTIONS = "loginOptions"; + public static final String KEY_ACCOUNT = "account"; + + /** + * Value under this key should handle path to webdav php script. Will be + * removed and usage should be replaced by combining + * {@link com.owncloud.android.authentication.AuthenticatorActivity.KEY_OC_BASE_URL} and + * {@link com.owncloud.android.utils.OwnCloudVersion} + * + * @deprecated + */ + public static final String KEY_OC_URL = "oc_url"; + /** + * Version should be 3 numbers separated by dot so it can be parsed by + * {@link com.owncloud.android.utils.OwnCloudVersion} + */ + public static final String KEY_OC_VERSION = "oc_version"; + /** + * Base url should point to owncloud installation without trailing / ie: + * http://server/path or https://owncloud.server + */ + public static final String KEY_OC_BASE_URL = "oc_base_url"; + /** + * Flag signaling if the ownCloud server can be accessed with OAuth2 access tokens. + */ + public static final String KEY_SUPPORTS_OAUTH2 = "oc_supports_oauth2"; + /** + * Flag signaling if the ownCloud server can be accessed with session cookies from SAML-based web single-sign-on. + */ + public static final String KEY_SUPPORTS_SAML_WEB_SSO = "oc_supports_saml_web_sso"; + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/authentication/AccountUtils.java b/oc_framework/src/com/owncloud/android/oc_framework/authentication/AccountUtils.java new file mode 100644 index 00000000..5fb6505f --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/authentication/AccountUtils.java @@ -0,0 +1,221 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.authentication; + + +import com.owncloud.android.oc_framework.MainApp; +import com.owncloud.android.oc_framework.OwnCloudVersion; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountsException; +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +public class AccountUtils { + public static final String WEBDAV_PATH_1_2 = "/webdav/owncloud.php"; + public static final String WEBDAV_PATH_2_0 = "/files/webdav.php"; + public static final String WEBDAV_PATH_4_0 = "/remote.php/webdav"; + private static final String ODAV_PATH = "/remote.php/odav"; + private static final String SAML_SSO_PATH = "/remote.php/webdav"; + public static final String CARDDAV_PATH_2_0 = "/apps/contacts/carddav.php"; + public static final String CARDDAV_PATH_4_0 = "/remote/carddav.php"; + public static final String STATUS_PATH = "/status.php"; + + /** + * Can be used to get the currently selected ownCloud {@link Account} in the + * application preferences. + * + * @param context The current application {@link Context} + * @return The ownCloud {@link Account} currently saved in preferences, or the first + * {@link Account} available, if valid (still registered in the system as ownCloud + * account). If none is available and valid, returns null. + */ + public static Account getCurrentOwnCloudAccount(Context context) { + Account[] ocAccounts = AccountManager.get(context).getAccountsByType( + MainApp.getAccountType()); + Account defaultAccount = null; + + SharedPreferences appPreferences = PreferenceManager + .getDefaultSharedPreferences(context); + String accountName = appPreferences + .getString("select_oc_account", null); + + // account validation: the saved account MUST be in the list of ownCloud Accounts known by the AccountManager + if (accountName != null) { + for (Account account : ocAccounts) { + if (account.name.equals(accountName)) { + defaultAccount = account; + break; + } + } + } + + if (defaultAccount == null && ocAccounts.length != 0) { + // take first account as fallback + defaultAccount = ocAccounts[0]; + } + + return defaultAccount; + } + + + public static boolean exists(Account account, Context context) { + Account[] ocAccounts = AccountManager.get(context).getAccountsByType( + MainApp.getAccountType()); + + if (account != null && account.name != null) { + for (Account ac : ocAccounts) { + if (ac.name.equals(account.name)) { + return true; + } + } + } + return false; + } + + + /** + * Checks, whether or not there are any ownCloud accounts setup. + * + * @return true, if there is at least one account. + */ + public static boolean accountsAreSetup(Context context) { + AccountManager accMan = AccountManager.get(context); + Account[] accounts = accMan + .getAccountsByType(MainApp.getAccountType()); + return accounts.length > 0; + } + + + public static boolean setCurrentOwnCloudAccount(Context context, String accountName) { + boolean result = false; + if (accountName != null) { + Account[] ocAccounts = AccountManager.get(context).getAccountsByType( + MainApp.getAccountType()); + boolean found = false; + for (Account account : ocAccounts) { + found = (account.name.equals(accountName)); + if (found) { + SharedPreferences.Editor appPrefs = PreferenceManager + .getDefaultSharedPreferences(context).edit(); + appPrefs.putString("select_oc_account", accountName); + + appPrefs.commit(); + result = true; + break; + } + } + } + return result; + } + + /** + * + * @param version version of owncloud + * @return webdav path for given OC version, null if OC version unknown + */ + public static String getWebdavPath(OwnCloudVersion version, boolean supportsOAuth, boolean supportsSamlSso) { + if (version != null) { + if (supportsOAuth) { + return ODAV_PATH; + } + if (supportsSamlSso) { + return SAML_SSO_PATH; + } + if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0) + return WEBDAV_PATH_4_0; + if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0 + || version.compareTo(OwnCloudVersion.owncloud_v2) >= 0) + return WEBDAV_PATH_2_0; + if (version.compareTo(OwnCloudVersion.owncloud_v1) >= 0) + return WEBDAV_PATH_1_2; + } + return null; + } + + /** + * Returns the proper URL path to access the WebDAV interface of an ownCloud server, + * according to its version and the authorization method used. + * + * @param version Version of ownCloud server. + * @param authTokenType Authorization token type, matching some of the AUTH_TOKEN_TYPE_* constants in {@link AccountAuthenticator}. + * @return WebDAV path for given OC version and authorization method, null if OC version is unknown. + */ + public static String getWebdavPath(OwnCloudVersion version, String authTokenType) { + if (version != null) { + if (MainApp.getAuthTokenTypeAccessToken().equals(authTokenType)) { + return ODAV_PATH; + } + if (MainApp.getAuthTokenTypeSamlSessionCookie().equals(authTokenType)) { + return SAML_SSO_PATH; + } + if (version.compareTo(OwnCloudVersion.owncloud_v4) >= 0) + return WEBDAV_PATH_4_0; + if (version.compareTo(OwnCloudVersion.owncloud_v3) >= 0 + || version.compareTo(OwnCloudVersion.owncloud_v2) >= 0) + return WEBDAV_PATH_2_0; + if (version.compareTo(OwnCloudVersion.owncloud_v1) >= 0) + return WEBDAV_PATH_1_2; + } + return null; + } + + /** + * Constructs full url to host and webdav resource basing on host version + * @param context + * @param account + * @return url or null on failure + * @throws AccountNotFoundException When 'account' is unknown for the AccountManager + */ + public static String constructFullURLForAccount(Context context, Account account) throws AccountNotFoundException { + AccountManager ama = AccountManager.get(context); + String baseurl = ama.getUserData(account, AccountAuthenticatorConstants.KEY_OC_BASE_URL); + String strver = ama.getUserData(account, AccountAuthenticatorConstants.KEY_OC_VERSION); + boolean supportsOAuth = (ama.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_OAUTH2) != null); + boolean supportsSamlSso = (ama.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_SAML_WEB_SSO) != null); + OwnCloudVersion ver = new OwnCloudVersion(strver); + String webdavpath = getWebdavPath(ver, supportsOAuth, supportsSamlSso); + + if (baseurl == null || webdavpath == null) + throw new AccountNotFoundException(account, "Account not found", null); + + return baseurl + webdavpath; + } + + + public static class AccountNotFoundException extends AccountsException { + + /** Generated - should be refreshed every time the class changes!! */ + private static final long serialVersionUID = -9013287181793186830L; + + private Account mFailedAccount; + + public AccountNotFoundException(Account failedAccount, String message, Throwable cause) { + super(message, cause); + mFailedAccount = failedAccount; + } + + public Account getFailedAccount() { + return mFailedAccount; + } + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/AdvancedSslSocketFactory.java b/oc_framework/src/com/owncloud/android/oc_framework/network/AdvancedSslSocketFactory.java new file mode 100644 index 00000000..d79c29cb --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/AdvancedSslSocketFactory.java @@ -0,0 +1,242 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.security.cert.X509Certificate; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +import org.apache.commons.httpclient.ConnectTimeoutException; +import org.apache.commons.httpclient.params.HttpConnectionParams; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; + +import android.util.Log; + + +/** + * AdvancedSSLProtocolSocketFactory allows to create SSL {@link Socket}s with + * a custom SSLContext and an optional Hostname Verifier. + * + * @author David A. Velasco + */ + +public class AdvancedSslSocketFactory implements ProtocolSocketFactory { + + private static final String TAG = AdvancedSslSocketFactory.class.getSimpleName(); + + private SSLContext mSslContext = null; + private AdvancedX509TrustManager mTrustManager = null; + private X509HostnameVerifier mHostnameVerifier = null; + + public SSLContext getSslContext() { + return mSslContext; + } + + /** + * Constructor for AdvancedSSLProtocolSocketFactory. + */ + public AdvancedSslSocketFactory(SSLContext sslContext, AdvancedX509TrustManager trustManager, X509HostnameVerifier hostnameVerifier) { + if (sslContext == null) + throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null SSLContext"); + if (trustManager == null) + throw new IllegalArgumentException("AdvancedSslSocketFactory can not be created with a null Trust Manager"); + mSslContext = sslContext; + mTrustManager = trustManager; + mHostnameVerifier = hostnameVerifier; + } + + /** + * @see ProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int) + */ + public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException, UnknownHostException { + Socket socket = mSslContext.getSocketFactory().createSocket(host, port, clientHost, clientPort); + verifyPeerIdentity(host, port, socket); + return socket; + } + + + /** + * Attempts to get a new socket connection to the given host within the + * given time limit. + * + * @param host the host name/IP + * @param port the port on the host + * @param clientHost the local host name/IP to bind the socket to + * @param clientPort the port on the local machine + * @param params {@link HttpConnectionParams Http connection parameters} + * + * @return Socket a new socket + * + * @throws IOException if an I/O error occurs while creating the socket + * @throws UnknownHostException if the IP address of the host cannot be + * determined + */ + public Socket createSocket(final String host, final int port, + final InetAddress localAddress, final int localPort, + final HttpConnectionParams params) throws IOException, + UnknownHostException, ConnectTimeoutException { + Log.d(TAG, "Creating SSL Socket with remote " + host + ":" + port + ", local " + localAddress + ":" + localPort + ", params: " + params); + if (params == null) { + throw new IllegalArgumentException("Parameters may not be null"); + } + int timeout = params.getConnectionTimeout(); + SocketFactory socketfactory = mSslContext.getSocketFactory(); + Log.d(TAG, " ... with connection timeout " + timeout + " and socket timeout " + params.getSoTimeout()); + Socket socket = socketfactory.createSocket(); + SocketAddress localaddr = new InetSocketAddress(localAddress, localPort); + SocketAddress remoteaddr = new InetSocketAddress(host, port); + socket.setSoTimeout(params.getSoTimeout()); + socket.bind(localaddr); + socket.connect(remoteaddr, timeout); + verifyPeerIdentity(host, port, socket); + return socket; + } + + /** + * @see ProtocolSocketFactory#createSocket(java.lang.String,int) + */ + public Socket createSocket(String host, int port) throws IOException, + UnknownHostException { + Log.d(TAG, "Creating SSL Socket with remote " + host + ":" + port); + Socket socket = mSslContext.getSocketFactory().createSocket(host, port); + verifyPeerIdentity(host, port, socket); + return socket; + } + + public boolean equals(Object obj) { + return ((obj != null) && obj.getClass().equals( + AdvancedSslSocketFactory.class)); + } + + public int hashCode() { + return AdvancedSslSocketFactory.class.hashCode(); + } + + + public X509HostnameVerifier getHostNameVerifier() { + return mHostnameVerifier; + } + + + public void setHostNameVerifier(X509HostnameVerifier hostnameVerifier) { + mHostnameVerifier = hostnameVerifier; + } + + /** + * Verifies the identity of the server. + * + * The server certificate is verified first. + * + * Then, the host name is compared with the content of the server certificate using the current host name verifier, if any. + * @param socket + */ + private void verifyPeerIdentity(String host, int port, Socket socket) throws IOException { + try { + CertificateCombinedException failInHandshake = null; + /// 1. VERIFY THE SERVER CERTIFICATE through the registered TrustManager (that should be an instance of AdvancedX509TrustManager) + try { + SSLSocket sock = (SSLSocket) socket; // a new SSLSession instance is created as a "side effect" + sock.startHandshake(); + + } catch (RuntimeException e) { + + if (e instanceof CertificateCombinedException) { + failInHandshake = (CertificateCombinedException) e; + } else { + Throwable cause = e.getCause(); + Throwable previousCause = null; + while (cause != null && cause != previousCause && !(cause instanceof CertificateCombinedException)) { + previousCause = cause; + cause = cause.getCause(); + } + if (cause != null && cause instanceof CertificateCombinedException) { + failInHandshake = (CertificateCombinedException)cause; + } + } + if (failInHandshake == null) { + throw e; + } + failInHandshake.setHostInUrl(host); + + } + + /// 2. VERIFY HOSTNAME + SSLSession newSession = null; + boolean verifiedHostname = true; + if (mHostnameVerifier != null) { + if (failInHandshake != null) { + /// 2.1 : a new SSLSession instance was NOT created in the handshake + X509Certificate serverCert = failInHandshake.getServerCertificate(); + try { + mHostnameVerifier.verify(host, serverCert); + } catch (SSLException e) { + verifiedHostname = false; + } + + } else { + /// 2.2 : a new SSLSession instance was created in the handshake + newSession = ((SSLSocket)socket).getSession(); + if (!mTrustManager.isKnownServer((X509Certificate)(newSession.getPeerCertificates()[0]))) { + verifiedHostname = mHostnameVerifier.verify(host, newSession); + } + } + } + + /// 3. Combine the exceptions to throw, if any + if (!verifiedHostname) { + SSLPeerUnverifiedException pue = new SSLPeerUnverifiedException("Names in the server certificate do not match to " + host + " in the URL"); + if (failInHandshake == null) { + failInHandshake = new CertificateCombinedException((X509Certificate) newSession.getPeerCertificates()[0]); + failInHandshake.setHostInUrl(host); + } + failInHandshake.setSslPeerUnverifiedException(pue); + pue.initCause(failInHandshake); + throw pue; + + } else if (failInHandshake != null) { + SSLHandshakeException hse = new SSLHandshakeException("Server certificate could not be verified"); + hse.initCause(failInHandshake); + throw hse; + } + + } catch (IOException io) { + try { + socket.close(); + } catch (Exception x) { + // NOTHING - irrelevant exception for the caller + } + throw io; + } + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/AdvancedX509TrustManager.java b/oc_framework/src/com/owncloud/android/oc_framework/network/AdvancedX509TrustManager.java new file mode 100644 index 00000000..a329807a --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/AdvancedX509TrustManager.java @@ -0,0 +1,148 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertStoreException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import android.util.Log; + + +/** + * @author David A. Velasco + */ +public class AdvancedX509TrustManager implements X509TrustManager { + + private static final String TAG = AdvancedX509TrustManager.class.getSimpleName(); + + private X509TrustManager mStandardTrustManager = null; + private KeyStore mKnownServersKeyStore; + + /** + * Constructor for AdvancedX509TrustManager + * + * @param knownServersCertStore Local certificates store with server certificates explicitly trusted by the user. + * @throws CertStoreException When no default X509TrustManager instance was found in the system. + */ + public AdvancedX509TrustManager(KeyStore knownServersKeyStore) + throws NoSuchAlgorithmException, KeyStoreException, CertStoreException { + super(); + TrustManagerFactory factory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init((KeyStore)null); + mStandardTrustManager = findX509TrustManager(factory); + + mKnownServersKeyStore = knownServersKeyStore; + } + + + /** + * Locates the first X509TrustManager provided by a given TrustManagerFactory + * @param factory TrustManagerFactory to inspect in the search for a X509TrustManager + * @return The first X509TrustManager found in factory. + * @throws CertStoreException When no X509TrustManager instance was found in factory + */ + private X509TrustManager findX509TrustManager(TrustManagerFactory factory) throws CertStoreException { + TrustManager tms[] = factory.getTrustManagers(); + for (int i = 0; i < tms.length; i++) { + if (tms[i] instanceof X509TrustManager) { + return (X509TrustManager) tms[i]; + } + } + return null; + } + + + /** + * @see javax.net.ssl.X509TrustManager#checkClientTrusted(X509Certificate[], + * String authType) + */ + public void checkClientTrusted(X509Certificate[] certificates, String authType) throws CertificateException { + mStandardTrustManager.checkClientTrusted(certificates, authType); + } + + + /** + * @see javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], + * String authType) + */ + public void checkServerTrusted(X509Certificate[] certificates, String authType) throws CertificateException { + if (!isKnownServer(certificates[0])) { + CertificateCombinedException result = new CertificateCombinedException(certificates[0]); + try { + certificates[0].checkValidity(); + } catch (CertificateExpiredException c) { + result.setCertificateExpiredException(c); + + } catch (CertificateNotYetValidException c) { + result.setCertificateNotYetException(c); + } + + try { + mStandardTrustManager.checkServerTrusted(certificates, authType); + } catch (CertificateException c) { + Throwable cause = c.getCause(); + Throwable previousCause = null; + while (cause != null && cause != previousCause && !(cause instanceof CertPathValidatorException)) { // getCause() is not funny + previousCause = cause; + cause = cause.getCause(); + } + if (cause != null && cause instanceof CertPathValidatorException) { + result.setCertPathValidatorException((CertPathValidatorException)cause); + } else { + result.setOtherCertificateException(c); + } + } + + if (result.isException()) + throw result; + + } + } + + + /** + * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() + */ + public X509Certificate[] getAcceptedIssuers() { + return mStandardTrustManager.getAcceptedIssuers(); + } + + + public boolean isKnownServer(X509Certificate cert) { + try { + return (mKnownServersKeyStore.getCertificateAlias(cert) != null); + } catch (KeyStoreException e) { + Log.d(TAG, "Fail while checking certificate in the known-servers store"); + return false; + } + } + +} \ No newline at end of file diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/BearerAuthScheme.java b/oc_framework/src/com/owncloud/android/oc_framework/network/BearerAuthScheme.java new file mode 100644 index 00000000..8589dbd2 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/BearerAuthScheme.java @@ -0,0 +1,270 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import java.util.Map; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.auth.AuthChallengeParser; +import org.apache.commons.httpclient.auth.AuthScheme; +import org.apache.commons.httpclient.auth.AuthenticationException; +import org.apache.commons.httpclient.auth.InvalidCredentialsException; +import org.apache.commons.httpclient.auth.MalformedChallengeException; + +import android.util.Log; + + +/** + * Bearer authentication scheme as defined in RFC 6750. + * + * @author David A. Velasco + */ + +public class BearerAuthScheme implements AuthScheme /*extends RFC2617Scheme*/ { + + private static final String TAG = BearerAuthScheme.class.getSimpleName(); + + public static final String AUTH_POLICY = "Bearer"; + + /** Whether the bearer authentication process is complete */ + private boolean mComplete; + + /** Authentication parameter map */ + private Map mParams = null; + + + /** + * Default constructor for the bearer authentication scheme. + */ + public BearerAuthScheme() { + mComplete = false; + } + + /** + * Constructor for the basic authentication scheme. + * + * @param challenge Authentication challenge + * + * @throws MalformedChallengeException Thrown if the authentication challenge is malformed + * + * @deprecated Use parameterless constructor and {@link AuthScheme#processChallenge(String)} method + */ + public BearerAuthScheme(final String challenge) throws MalformedChallengeException { + processChallenge(challenge); + mComplete = true; + } + + /** + * Returns textual designation of the bearer authentication scheme. + * + * @return "Bearer" + */ + public String getSchemeName() { + return "bearer"; + } + + /** + * Processes the Bearer challenge. + * + * @param challenge The challenge string + * + * @throws MalformedChallengeException Thrown if the authentication challenge is malformed + */ + public void processChallenge(String challenge) throws MalformedChallengeException { + String s = AuthChallengeParser.extractScheme(challenge); + if (!s.equalsIgnoreCase(getSchemeName())) { + throw new MalformedChallengeException( + "Invalid " + getSchemeName() + " challenge: " + challenge); + } + mParams = AuthChallengeParser.extractParams(challenge); + mComplete = true; + } + + /** + * Tests if the Bearer authentication process has been completed. + * + * @return 'true' if Bearer authorization has been processed, 'false' otherwise. + */ + public boolean isComplete() { + return this.mComplete; + } + + /** + * Produces bearer authorization string for the given set of + * {@link Credentials}. + * + * @param credentials The set of credentials to be used for authentication + * @param method Method name is ignored by the bearer authentication scheme + * @param uri URI is ignored by the bearer authentication scheme + * @throws InvalidCredentialsException If authentication credentials are not valid or not applicable + * for this authentication scheme + * @throws AuthenticationException If authorization string cannot be generated due to an authentication failure + * @return A bearer authorization string + * + * @deprecated Use {@link #authenticate(Credentials, HttpMethod)} + */ + public String authenticate(Credentials credentials, String method, String uri) throws AuthenticationException { + Log.d(TAG, "enter BearerScheme.authenticate(Credentials, String, String)"); + + BearerCredentials bearer = null; + try { + bearer = (BearerCredentials) credentials; + } catch (ClassCastException e) { + throw new InvalidCredentialsException( + "Credentials cannot be used for bearer authentication: " + + credentials.getClass().getName()); + } + return BearerAuthScheme.authenticate(bearer); + } + + + /** + * Returns 'false'. Bearer authentication scheme is request based. + * + * @return 'false'. + */ + public boolean isConnectionBased() { + return false; + } + + /** + * Produces bearer authorization string for the given set of {@link Credentials}. + * + * @param credentials The set of credentials to be used for authentication + * @param method The method being authenticated + * @throws InvalidCredentialsException If authentication credentials are not valid or not applicable for this authentication + * scheme. + * @throws AuthenticationException If authorization string cannot be generated due to an authentication failure. + * + * @return a basic authorization string + */ + public String authenticate(Credentials credentials, HttpMethod method) throws AuthenticationException { + Log.d(TAG, "enter BearerScheme.authenticate(Credentials, HttpMethod)"); + + if (method == null) { + throw new IllegalArgumentException("Method may not be null"); + } + BearerCredentials bearer = null; + try { + bearer = (BearerCredentials) credentials; + } catch (ClassCastException e) { + throw new InvalidCredentialsException( + "Credentials cannot be used for bearer authentication: " + + credentials.getClass().getName()); + } + return BearerAuthScheme.authenticate( + bearer, + method.getParams().getCredentialCharset()); + } + + /** + * @deprecated Use {@link #authenticate(BearerCredentials, String)} + * + * Returns a bearer Authorization header value for the given + * {@link BearerCredentials}. + * + * @param credentials The credentials to encode. + * + * @return A bearer authorization string + */ + public static String authenticate(BearerCredentials credentials) { + return authenticate(credentials, "ISO-8859-1"); + } + + /** + * Returns a bearer Authorization header value for the given + * {@link BearerCredentials} and charset. + * + * @param credentials The credentials to encode. + * @param charset The charset to use for encoding the credentials + * + * @return A bearer authorization string + * + * @since 3.0 + */ + public static String authenticate(BearerCredentials credentials, String charset) { + Log.d(TAG, "enter BearerAuthScheme.authenticate(BearerCredentials, String)"); + + if (credentials == null) { + throw new IllegalArgumentException("Credentials may not be null"); + } + if (charset == null || charset.length() == 0) { + throw new IllegalArgumentException("charset may not be null or empty"); + } + StringBuffer buffer = new StringBuffer(); + buffer.append(credentials.getAccessToken()); + + //return "Bearer " + EncodingUtil.getAsciiString(EncodingUtil.getBytes(buffer.toString(), charset)); + return "Bearer " + buffer.toString(); + } + + /** + * Returns a String identifying the authentication challenge. This is + * used, in combination with the host and port to determine if + * authorization has already been attempted or not. Schemes which + * require multiple requests to complete the authentication should + * return a different value for each stage in the request. + * + * Additionally, the ID should take into account any changes to the + * authentication challenge and return a different value when appropriate. + * For example when the realm changes in basic authentication it should be + * considered a different authentication attempt and a different value should + * be returned. + * + * This method simply returns the realm for the challenge. + * + * @return String a String identifying the authentication challenge. + * + * @deprecated no longer used + */ + @Override + public String getID() { + return getRealm(); + } + + /** + * Returns authentication parameter with the given name, if available. + * + * @param name The name of the parameter to be returned + * + * @return The parameter with the given name + */ + @Override + public String getParameter(String name) { + if (name == null) { + throw new IllegalArgumentException("Parameter name may not be null"); + } + if (mParams == null) { + return null; + } + return (String) mParams.get(name.toLowerCase()); + } + + /** + * Returns authentication realm. The realm may not be null. + * + * @return The authentication realm + */ + @Override + public String getRealm() { + return getParameter("realm"); + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/BearerCredentials.java b/oc_framework/src/com/owncloud/android/oc_framework/network/BearerCredentials.java new file mode 100644 index 00000000..5b18e62d --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/BearerCredentials.java @@ -0,0 +1,98 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.util.LangUtils; + +/** + * Bearer token {@link Credentials} + * + * @author David A. Velasco + */ +public class BearerCredentials implements Credentials { + + + private String mAccessToken; + + + /** + * The constructor with the bearer token + * + * @param token The bearer token + */ + public BearerCredentials(String token) { + /*if (token == null) { + throw new IllegalArgumentException("Bearer token may not be null"); + }*/ + mAccessToken = (token == null) ? "" : token; + } + + + /** + * Returns the access token + * + * @return The access token + */ + public String getAccessToken() { + return mAccessToken; + } + + + /** + * Get this object string. + * + * @return The access token + */ + public String toString() { + return mAccessToken; + } + + /** + * Does a hash of the access token. + * + * @return The hash code of the access token + */ + public int hashCode() { + int hash = LangUtils.HASH_SEED; + hash = LangUtils.hashCode(hash, mAccessToken); + return hash; + } + + /** + * These credentials are assumed equal if accessToken is the same. + * + * @param o The other object to compare with. + * + * @return 'True' if the object is equivalent. + */ + public boolean equals(Object o) { + if (o == null) return false; + if (this == o) return true; + if (this.getClass().equals(o.getClass())) { + BearerCredentials that = (BearerCredentials) o; + if (LangUtils.equals(mAccessToken, that.mAccessToken)) { + return true; + } + } + return false; + } + +} + diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/CertificateCombinedException.java b/oc_framework/src/com/owncloud/android/oc_framework/network/CertificateCombinedException.java new file mode 100644 index 00000000..1b262bc2 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/CertificateCombinedException.java @@ -0,0 +1,131 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLPeerUnverifiedException; + +/** + * Exception joining all the problems that {@link AdvancedX509TrustManager} can find in + * a certificate chain for a server. + * + * This was initially created as an extension of CertificateException, but some + * implementations of the SSL socket layer in existing devices are REPLACING the CertificateException + * instances thrown by {@link javax.net.ssl.X509TrustManager#checkServerTrusted(X509Certificate[], String)} + * with SSLPeerUnverifiedException FORGETTING THE CAUSING EXCEPTION instead of wrapping it. + * + * Due to this, extending RuntimeException is necessary to get that the CertificateCombinedException + * instance reaches {@link AdvancedSslSocketFactory#verifyPeerIdentity}. + * + * BE CAREFUL. As a RuntimeException extensions, Java compilers do not require to handle it + * in client methods. Be sure to use it only when you know exactly where it will go. + * + * @author David A. Velasco + */ +public class CertificateCombinedException extends RuntimeException { + + /** Generated - to refresh every time the class changes */ + private static final long serialVersionUID = -8875782030758554999L; + + private X509Certificate mServerCert = null; + private String mHostInUrl; + + private CertificateExpiredException mCertificateExpiredException = null; + private CertificateNotYetValidException mCertificateNotYetValidException = null; + private CertPathValidatorException mCertPathValidatorException = null; + private CertificateException mOtherCertificateException = null; + private SSLPeerUnverifiedException mSslPeerUnverifiedException = null; + + public CertificateCombinedException(X509Certificate x509Certificate) { + mServerCert = x509Certificate; + } + + public X509Certificate getServerCertificate() { + return mServerCert; + } + + public String getHostInUrl() { + return mHostInUrl; + } + + public void setHostInUrl(String host) { + mHostInUrl = host; + } + + public CertificateExpiredException getCertificateExpiredException() { + return mCertificateExpiredException; + } + + public void setCertificateExpiredException(CertificateExpiredException c) { + mCertificateExpiredException = c; + } + + public CertificateNotYetValidException getCertificateNotYetValidException() { + return mCertificateNotYetValidException; + } + + public void setCertificateNotYetException(CertificateNotYetValidException c) { + mCertificateNotYetValidException = c; + } + + public CertPathValidatorException getCertPathValidatorException() { + return mCertPathValidatorException; + } + + public void setCertPathValidatorException(CertPathValidatorException c) { + mCertPathValidatorException = c; + } + + public CertificateException getOtherCertificateException() { + return mOtherCertificateException; + } + + public void setOtherCertificateException(CertificateException c) { + mOtherCertificateException = c; + } + + public SSLPeerUnverifiedException getSslPeerUnverifiedException() { + return mSslPeerUnverifiedException ; + } + + public void setSslPeerUnverifiedException(SSLPeerUnverifiedException s) { + mSslPeerUnverifiedException = s; + } + + public boolean isException() { + return (mCertificateExpiredException != null || + mCertificateNotYetValidException != null || + mCertPathValidatorException != null || + mOtherCertificateException != null || + mSslPeerUnverifiedException != null); + } + + public boolean isRecoverable() { + return (mCertificateExpiredException != null || + mCertificateNotYetValidException != null || + mCertPathValidatorException != null || + mSslPeerUnverifiedException != null); + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/OwnCloudClientUtils.java b/oc_framework/src/com/owncloud/android/oc_framework/network/OwnCloudClientUtils.java new file mode 100644 index 00000000..579a30e6 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/OwnCloudClientUtils.java @@ -0,0 +1,284 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier; +import org.apache.http.conn.ssl.X509HostnameVerifier; + +import com.owncloud.android.oc_framework.MainApp; +import com.owncloud.android.oc_framework.authentication.AccountAuthenticatorConstants; +import com.owncloud.android.oc_framework.authentication.AccountUtils; +import com.owncloud.android.oc_framework.authentication.AccountUtils.AccountNotFoundException; +import com.owncloud.android.oc_framework.network.webdav.WebdavClient; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +public class OwnCloudClientUtils { + + final private static String TAG = OwnCloudClientUtils.class.getSimpleName(); + + /** Default timeout for waiting data from the server */ + public static final int DEFAULT_DATA_TIMEOUT = 60000; + + /** Default timeout for establishing a connection */ + public static final int DEFAULT_CONNECTION_TIMEOUT = 60000; + + /** Connection manager for all the WebdavClients */ + private static MultiThreadedHttpConnectionManager mConnManager = null; + + private static Protocol mDefaultHttpsProtocol = null; + + private static AdvancedSslSocketFactory mAdvancedSslSocketFactory = null; + + private static X509HostnameVerifier mHostnameVerifier = null; + + + /** + * Creates a WebdavClient setup for an ownCloud account + * + * Do not call this method from the main thread. + * + * @param account The ownCloud account + * @param appContext Android application context + * @return A WebdavClient object ready to be used + * @throws AuthenticatorException If the authenticator failed to get the authorization token for the account. + * @throws OperationCanceledException If the authenticator operation was cancelled while getting the authorization token for the account. + * @throws IOException If there was some I/O error while getting the authorization token for the account. + * @throws AccountNotFoundException If 'account' is unknown for the AccountManager + */ + public static WebdavClient createOwnCloudClient (Account account, Context appContext) throws OperationCanceledException, AuthenticatorException, IOException, AccountNotFoundException { + //Log_OC.d(TAG, "Creating WebdavClient associated to " + account.name); + + Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(appContext, account)); + AccountManager am = AccountManager.get(appContext); + boolean isOauth2 = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_OAUTH2) != null; // TODO avoid calling to getUserData here + boolean isSamlSso = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_SAML_WEB_SSO) != null; + WebdavClient client = createOwnCloudClient(uri, appContext, !isSamlSso); + if (isOauth2) { + String accessToken = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypeAccessToken(), false); + client.setBearerCredentials(accessToken); // TODO not assume that the access token is a bearer token + + } else if (isSamlSso) { // TODO avoid a call to getUserData here + String accessToken = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypeSamlSessionCookie(), false); + client.setSsoSessionCookie(accessToken); + + } else { + String username = account.name.substring(0, account.name.lastIndexOf('@')); + //String password = am.getPassword(account); + String password = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypePass(), false); + client.setBasicCredentials(username, password); + } + + return client; + } + + + public static WebdavClient createOwnCloudClient (Account account, Context appContext, Activity currentActivity) throws OperationCanceledException, AuthenticatorException, IOException, AccountNotFoundException { + Uri uri = Uri.parse(AccountUtils.constructFullURLForAccount(appContext, account)); + AccountManager am = AccountManager.get(appContext); + boolean isOauth2 = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_OAUTH2) != null; // TODO avoid calling to getUserData here + boolean isSamlSso = am.getUserData(account, AccountAuthenticatorConstants.KEY_SUPPORTS_SAML_WEB_SSO) != null; + WebdavClient client = createOwnCloudClient(uri, appContext, !isSamlSso); + + if (isOauth2) { // TODO avoid a call to getUserData here + AccountManagerFuture future = am.getAuthToken(account, MainApp.getAuthTokenTypeAccessToken(), null, currentActivity, null, null); + Bundle result = future.getResult(); + String accessToken = result.getString(AccountManager.KEY_AUTHTOKEN); + if (accessToken == null) throw new AuthenticatorException("WTF!"); + client.setBearerCredentials(accessToken); // TODO not assume that the access token is a bearer token + + } else if (isSamlSso) { // TODO avoid a call to getUserData here + AccountManagerFuture future = am.getAuthToken(account, MainApp.getAuthTokenTypeSamlSessionCookie(), null, currentActivity, null, null); + Bundle result = future.getResult(); + String accessToken = result.getString(AccountManager.KEY_AUTHTOKEN); + if (accessToken == null) throw new AuthenticatorException("WTF!"); + client.setSsoSessionCookie(accessToken); + + } else { + String username = account.name.substring(0, account.name.lastIndexOf('@')); + //String password = am.getPassword(account); + //String password = am.blockingGetAuthToken(account, MainApp.getAuthTokenTypePass(), false); + AccountManagerFuture future = am.getAuthToken(account, MainApp.getAuthTokenTypePass(), null, currentActivity, null, null); + Bundle result = future.getResult(); + String password = result.getString(AccountManager.KEY_AUTHTOKEN); + client.setBasicCredentials(username, password); + } + + return client; + } + + /** + * Creates a WebdavClient to access a URL and sets the desired parameters for ownCloud client connections. + * + * @param uri URL to the ownCloud server + * @param context Android context where the WebdavClient is being created. + * @return A WebdavClient object ready to be used + */ + public static WebdavClient createOwnCloudClient(Uri uri, Context context, boolean followRedirects) { + try { + registerAdvancedSslContext(true, context); + } catch (GeneralSecurityException e) { + Log.e(TAG, "Advanced SSL Context could not be loaded. Default SSL management in the system will be used for HTTPS connections", e); + + } catch (IOException e) { + Log.e(TAG, "The local server truststore could not be read. Default SSL management in the system will be used for HTTPS connections", e); + } + + WebdavClient client = new WebdavClient(getMultiThreadedConnManager()); + + client.setDefaultTimeouts(DEFAULT_DATA_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT); + client.setBaseUri(uri); + client.setFollowRedirects(followRedirects); + + return client; + } + + + /** + * Registers or unregisters the proper components for advanced SSL handling. + * @throws IOException + */ + private static void registerAdvancedSslContext(boolean register, Context context) throws GeneralSecurityException, IOException { + Protocol pr = null; + try { + pr = Protocol.getProtocol("https"); + if (pr != null && mDefaultHttpsProtocol == null) { + mDefaultHttpsProtocol = pr; + } + } catch (IllegalStateException e) { + // nothing to do here; really + } + boolean isRegistered = (pr != null && pr.getSocketFactory() instanceof AdvancedSslSocketFactory); + if (register && !isRegistered) { + Protocol.registerProtocol("https", new Protocol("https", getAdvancedSslSocketFactory(context), 443)); + + } else if (!register && isRegistered) { + if (mDefaultHttpsProtocol != null) { + Protocol.registerProtocol("https", mDefaultHttpsProtocol); + } + } + } + + public static AdvancedSslSocketFactory getAdvancedSslSocketFactory(Context context) throws GeneralSecurityException, IOException { + if (mAdvancedSslSocketFactory == null) { + KeyStore trustStore = getKnownServersStore(context); + AdvancedX509TrustManager trustMgr = new AdvancedX509TrustManager(trustStore); + TrustManager[] tms = new TrustManager[] { trustMgr }; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tms, null); + + mHostnameVerifier = new BrowserCompatHostnameVerifier(); + mAdvancedSslSocketFactory = new AdvancedSslSocketFactory(sslContext, trustMgr, mHostnameVerifier); + } + return mAdvancedSslSocketFactory; + } + + + private static String LOCAL_TRUSTSTORE_FILENAME = "knownServers.bks"; + + private static String LOCAL_TRUSTSTORE_PASSWORD = "password"; + + private static KeyStore mKnownServersStore = null; + + /** + * Returns the local store of reliable server certificates, explicitly accepted by the user. + * + * Returns a KeyStore instance with empty content if the local store was never created. + * + * Loads the store from the storage environment if needed. + * + * @param context Android context where the operation is being performed. + * @return KeyStore instance with explicitly-accepted server certificates. + * @throws KeyStoreException When the KeyStore instance could not be created. + * @throws IOException When an existing local trust store could not be loaded. + * @throws NoSuchAlgorithmException When the existing local trust store was saved with an unsupported algorithm. + * @throws CertificateException When an exception occurred while loading the certificates from the local trust store. + */ + private static KeyStore getKnownServersStore(Context context) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + if (mKnownServersStore == null) { + //mKnownServersStore = KeyStore.getInstance("BKS"); + mKnownServersStore = KeyStore.getInstance(KeyStore.getDefaultType()); + File localTrustStoreFile = new File(context.getFilesDir(), LOCAL_TRUSTSTORE_FILENAME); + Log.d(TAG, "Searching known-servers store at " + localTrustStoreFile.getAbsolutePath()); + if (localTrustStoreFile.exists()) { + InputStream in = new FileInputStream(localTrustStoreFile); + try { + mKnownServersStore.load(in, LOCAL_TRUSTSTORE_PASSWORD.toCharArray()); + } finally { + in.close(); + } + } else { + mKnownServersStore.load(null, LOCAL_TRUSTSTORE_PASSWORD.toCharArray()); // necessary to initialize an empty KeyStore instance + } + } + return mKnownServersStore; + } + + + public static void addCertToKnownServersStore(Certificate cert, Context context) throws KeyStoreException, NoSuchAlgorithmException, + CertificateException, IOException { + KeyStore knownServers = getKnownServersStore(context); + knownServers.setCertificateEntry(Integer.toString(cert.hashCode()), cert); + FileOutputStream fos = null; + try { + fos = context.openFileOutput(LOCAL_TRUSTSTORE_FILENAME, Context.MODE_PRIVATE); + knownServers.store(fos, LOCAL_TRUSTSTORE_PASSWORD.toCharArray()); + } finally { + fos.close(); + } + } + + + static private MultiThreadedHttpConnectionManager getMultiThreadedConnManager() { + if (mConnManager == null) { + mConnManager = new MultiThreadedHttpConnectionManager(); + mConnManager.getParams().setDefaultMaxConnectionsPerHost(5); + mConnManager.getParams().setMaxTotalConnections(5); + } + return mConnManager; + } + + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/ProgressiveDataTransferer.java b/oc_framework/src/com/owncloud/android/oc_framework/network/ProgressiveDataTransferer.java new file mode 100644 index 00000000..3a21d5f7 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/ProgressiveDataTransferer.java @@ -0,0 +1,34 @@ +package com.owncloud.android.oc_framework.network; +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import java.util.Collection; + +import com.owncloud.android.oc_framework.network.webdav.OnDatatransferProgressListener; + + +public interface ProgressiveDataTransferer { + + public void addDatatransferProgressListener (OnDatatransferProgressListener listener); + + public void addDatatransferProgressListeners(Collection listeners); + + public void removeDatatransferProgressListener(OnDatatransferProgressListener listener); + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/ChunkFromFileChannelRequestEntity.java b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/ChunkFromFileChannelRequestEntity.java new file mode 100644 index 00000000..45877373 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/ChunkFromFileChannelRequestEntity.java @@ -0,0 +1,146 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.network.webdav; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.commons.httpclient.methods.RequestEntity; + +import com.owncloud.android.oc_framework.network.ProgressiveDataTransferer; + +import android.util.Log; + + + +/** + * A RequestEntity that represents a PIECE of a file. + * + * @author David A. Velasco + */ +public class ChunkFromFileChannelRequestEntity implements RequestEntity, ProgressiveDataTransferer { + + private static final String TAG = ChunkFromFileChannelRequestEntity.class.getSimpleName(); + + //private final File mFile; + private final FileChannel mChannel; + private final String mContentType; + private final long mChunkSize; + private final File mFile; + private long mOffset; + private long mTransferred; + Set mDataTransferListeners = new HashSet(); + private ByteBuffer mBuffer = ByteBuffer.allocate(4096); + + public ChunkFromFileChannelRequestEntity(final FileChannel channel, final String contentType, long chunkSize, final File file) { + super(); + if (channel == null) { + throw new IllegalArgumentException("File may not be null"); + } + if (chunkSize <= 0) { + throw new IllegalArgumentException("Chunk size must be greater than zero"); + } + mChannel = channel; + mContentType = contentType; + mChunkSize = chunkSize; + mFile = file; + mOffset = 0; + mTransferred = 0; + } + + public void setOffset(long offset) { + mOffset = offset; + } + + public long getContentLength() { + try { + return Math.min(mChunkSize, mChannel.size() - mChannel.position()); + } catch (IOException e) { + return mChunkSize; + } + } + + public String getContentType() { + return mContentType; + } + + public boolean isRepeatable() { + return true; + } + + @Override + public void addDatatransferProgressListener(OnDatatransferProgressListener listener) { + synchronized (mDataTransferListeners) { + mDataTransferListeners.add(listener); + } + } + + @Override + public void addDatatransferProgressListeners(Collection listeners) { + synchronized (mDataTransferListeners) { + mDataTransferListeners.addAll(listeners); + } + } + + @Override + public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) { + synchronized (mDataTransferListeners) { + mDataTransferListeners.remove(listener); + } + } + + + public void writeRequest(final OutputStream out) throws IOException { + int readCount = 0; + Iterator it = null; + + try { + mChannel.position(mOffset); + long size = mFile.length(); + if (size == 0) size = -1; + long maxCount = Math.min(mOffset + mChunkSize, mChannel.size()); + while (mChannel.position() < maxCount) { + readCount = mChannel.read(mBuffer); + out.write(mBuffer.array(), 0, readCount); + mBuffer.clear(); + if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks + mTransferred += readCount; + } + synchronized (mDataTransferListeners) { + it = mDataTransferListeners.iterator(); + while (it.hasNext()) { + it.next().onTransferProgress(readCount, mTransferred, size, mFile.getName()); + } + } + } + + } catch (IOException io) { + Log.e(TAG, io.getMessage()); + throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io); + + } + } + +} \ No newline at end of file diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/FileRequestEntity.java b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/FileRequestEntity.java new file mode 100644 index 00000000..0b1214a0 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/FileRequestEntity.java @@ -0,0 +1,134 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.network.webdav; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.apache.commons.httpclient.methods.RequestEntity; + +import android.util.Log; + +import com.owncloud.android.oc_framework.network.ProgressiveDataTransferer; + + + + +/** + * A RequestEntity that represents a File. + * + */ +public class FileRequestEntity implements RequestEntity, ProgressiveDataTransferer { + + final File mFile; + final String mContentType; + Set mDataTransferListeners = new HashSet(); + + public FileRequestEntity(final File file, final String contentType) { + super(); + this.mFile = file; + this.mContentType = contentType; + if (file == null) { + throw new IllegalArgumentException("File may not be null"); + } + } + + @Override + public long getContentLength() { + return mFile.length(); + } + + @Override + public String getContentType() { + return mContentType; + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public void addDatatransferProgressListener(OnDatatransferProgressListener listener) { + synchronized (mDataTransferListeners) { + mDataTransferListeners.add(listener); + } + } + + @Override + public void addDatatransferProgressListeners(Collection listeners) { + synchronized (mDataTransferListeners) { + mDataTransferListeners.addAll(listeners); + } + } + + @Override + public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) { + synchronized (mDataTransferListeners) { + mDataTransferListeners.remove(listener); + } + } + + + @Override + public void writeRequest(final OutputStream out) throws IOException { + //byte[] tmp = new byte[4096]; + ByteBuffer tmp = ByteBuffer.allocate(4096); + int readResult = 0; + + // TODO(bprzybylski): each mem allocation can throw OutOfMemoryError we need to handle it + // globally in some fashionable manner + RandomAccessFile raf = new RandomAccessFile(mFile, "r"); + FileChannel channel = raf.getChannel(); + Iterator it = null; + long transferred = 0; + long size = mFile.length(); + if (size == 0) size = -1; + try { + while ((readResult = channel.read(tmp)) >= 0) { + out.write(tmp.array(), 0, readResult); + tmp.clear(); + transferred += readResult; + synchronized (mDataTransferListeners) { + it = mDataTransferListeners.iterator(); + while (it.hasNext()) { + it.next().onTransferProgress(readResult, transferred, size, mFile.getName()); + } + } + } + + } catch (IOException io) { + Log.e("FileRequestException", io.getMessage()); + throw new RuntimeException("Ugly solution to workaround the default policy of retries when the server falls while uploading ; temporal fix; really", io); + + } finally { + channel.close(); + raf.close(); + } + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/OnDatatransferProgressListener.java b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/OnDatatransferProgressListener.java new file mode 100644 index 00000000..06f32b59 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/OnDatatransferProgressListener.java @@ -0,0 +1,24 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.network.webdav; + +public interface OnDatatransferProgressListener { + public void onTransferProgress(long progressRate); + public void onTransferProgress(long progressRate, long totalTransferredSoFar, long totalToTransfer, String fileName); +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavClient.java b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavClient.java new file mode 100644 index 00000000..213a28b5 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavClient.java @@ -0,0 +1,256 @@ +/* ownCloud Android client application + * Copyright (C) 2011 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.network.webdav; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpConnectionManager; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.HttpMethodBase; +import org.apache.commons.httpclient.HttpVersion; +import org.apache.commons.httpclient.URI; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.auth.AuthPolicy; +import org.apache.commons.httpclient.auth.AuthScope; +import org.apache.commons.httpclient.cookie.CookiePolicy; +import org.apache.commons.httpclient.methods.HeadMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.http.HttpStatus; +import org.apache.http.params.CoreProtocolPNames; + +import com.owncloud.android.oc_framework.MainApp; +import com.owncloud.android.oc_framework.network.BearerAuthScheme; +import com.owncloud.android.oc_framework.network.BearerCredentials; + + +import android.net.Uri; +import android.util.Log; + +public class WebdavClient extends HttpClient { + private static final int MAX_REDIRECTIONS_COUNT = 3; + + private Uri mUri; + private Credentials mCredentials; + private boolean mFollowRedirects; + private String mSsoSessionCookie; + private String mAuthTokenType; + final private static String TAG = "WebdavClient"; + public static final String USER_AGENT = "Android-ownCloud"; + + static private byte[] sExhaustBuffer = new byte[1024]; + + /** + * Constructor + */ + public WebdavClient(HttpConnectionManager connectionMgr) { + super(connectionMgr); + Log.d(TAG, "Creating WebdavClient"); + getParams().setParameter(HttpMethodParams.USER_AGENT, USER_AGENT); + getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1); + mFollowRedirects = true; + mSsoSessionCookie = null; + mAuthTokenType = MainApp.getAuthTokenTypePass(); + } + + public void setBearerCredentials(String accessToken) { + AuthPolicy.registerAuthScheme(BearerAuthScheme.AUTH_POLICY, BearerAuthScheme.class); + + List authPrefs = new ArrayList(1); + authPrefs.add(BearerAuthScheme.AUTH_POLICY); + getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + + mCredentials = new BearerCredentials(accessToken); + getState().setCredentials(AuthScope.ANY, mCredentials); + mSsoSessionCookie = null; + mAuthTokenType = MainApp.getAuthTokenTypeAccessToken(); + } + + public void setBasicCredentials(String username, String password) { + List authPrefs = new ArrayList(1); + authPrefs.add(AuthPolicy.BASIC); + getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); + + getParams().setAuthenticationPreemptive(true); + mCredentials = new UsernamePasswordCredentials(username, password); + getState().setCredentials(AuthScope.ANY, mCredentials); + mSsoSessionCookie = null; + mAuthTokenType = MainApp.getAuthTokenTypePass(); + } + + public void setSsoSessionCookie(String accessToken) { + getParams().setAuthenticationPreemptive(false); + getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES); + mSsoSessionCookie = accessToken; + mCredentials = null; + mAuthTokenType = MainApp.getAuthTokenTypeSamlSessionCookie(); + } + + + /** + * Check if a file exists in the OC server + * + * TODO replace with ExistenceOperation + * + * @return 'true' if the file exists; 'false' it doesn't exist + * @throws Exception When the existence could not be determined + */ + public boolean existsFile(String path) throws IOException, HttpException { + HeadMethod head = new HeadMethod(mUri.toString() + WebdavUtils.encodePath(path)); + try { + int status = executeMethod(head); + Log.d(TAG, "HEAD to " + path + " finished with HTTP status " + status + ((status != HttpStatus.SC_OK)?"(FAIL)":"")); + exhaustResponse(head.getResponseBodyAsStream()); + return (status == HttpStatus.SC_OK); + + } finally { + head.releaseConnection(); // let the connection available for other methods + } + } + + /** + * Requests the received method with the received timeout (milliseconds). + * + * Executes the method through the inherited HttpClient.executedMethod(method). + * + * Sets the socket and connection timeouts only for the method received. + * + * The timeouts are both in milliseconds; 0 means 'infinite'; < 0 means 'do not change the default' + * + * @param method HTTP method request. + * @param readTimeout Timeout to set for data reception + * @param conntionTimout Timeout to set for connection establishment + */ + public int executeMethod(HttpMethodBase method, int readTimeout, int connectionTimeout) throws HttpException, IOException { + int oldSoTimeout = getParams().getSoTimeout(); + int oldConnectionTimeout = getHttpConnectionManager().getParams().getConnectionTimeout(); + try { + if (readTimeout >= 0) { + method.getParams().setSoTimeout(readTimeout); // this should be enough... + getParams().setSoTimeout(readTimeout); // ... but this looks like necessary for HTTPS + } + if (connectionTimeout >= 0) { + getHttpConnectionManager().getParams().setConnectionTimeout(connectionTimeout); + } + return executeMethod(method); + } finally { + getParams().setSoTimeout(oldSoTimeout); + getHttpConnectionManager().getParams().setConnectionTimeout(oldConnectionTimeout); + } + } + + + @Override + public int executeMethod(HttpMethod method) throws IOException, HttpException { + boolean customRedirectionNeeded = false; + try { + method.setFollowRedirects(mFollowRedirects); + } catch (Exception e) { + //if (mFollowRedirects) Log_OC.d(TAG, "setFollowRedirects failed for " + method.getName() + " method, custom redirection will be used if needed"); + customRedirectionNeeded = mFollowRedirects; + } + if (mSsoSessionCookie != null && mSsoSessionCookie.length() > 0) { + method.setRequestHeader("Cookie", mSsoSessionCookie); + } + int status = super.executeMethod(method); + int redirectionsCount = 0; + while (customRedirectionNeeded && + redirectionsCount < MAX_REDIRECTIONS_COUNT && + ( status == HttpStatus.SC_MOVED_PERMANENTLY || + status == HttpStatus.SC_MOVED_TEMPORARILY || + status == HttpStatus.SC_TEMPORARY_REDIRECT) + ) { + + Header location = method.getResponseHeader("Location"); + if (location != null) { + Log.d(TAG, "Location to redirect: " + location.getValue()); + method.setURI(new URI(location.getValue(), true)); + status = super.executeMethod(method); + redirectionsCount++; + + } else { + Log.d(TAG, "No location to redirect!"); + status = HttpStatus.SC_NOT_FOUND; + } + } + + return status; + } + + + /** + * Exhausts a not interesting HTTP response. Encouraged by HttpClient documentation. + * + * @param responseBodyAsStream InputStream with the HTTP response to exhaust. + */ + public void exhaustResponse(InputStream responseBodyAsStream) { + if (responseBodyAsStream != null) { + try { + while (responseBodyAsStream.read(sExhaustBuffer) >= 0); + responseBodyAsStream.close(); + + } catch (IOException io) { + Log.e(TAG, "Unexpected exception while exhausting not interesting HTTP response; will be IGNORED", io); + } + } + } + + /** + * Sets the connection and wait-for-data timeouts to be applied by default to the methods performed by this client. + */ + public void setDefaultTimeouts(int defaultDataTimeout, int defaultConnectionTimeout) { + getParams().setSoTimeout(defaultDataTimeout); + getHttpConnectionManager().getParams().setConnectionTimeout(defaultConnectionTimeout); + } + + /** + * Sets the base URI for the helper methods that receive paths as parameters, instead of full URLs + * @param uri + */ + public void setBaseUri(Uri uri) { + mUri = uri; + } + + public Uri getBaseUri() { + return mUri; + } + + public final Credentials getCredentials() { + return mCredentials; + } + + public final String getSsoSessionCookie() { + return mSsoSessionCookie; + } + + public void setFollowRedirects(boolean followRedirects) { + mFollowRedirects = followRedirects; + } + + public String getAuthTokenType() { + return mAuthTokenType; + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavEntry.java b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavEntry.java new file mode 100644 index 00000000..85a45df9 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavEntry.java @@ -0,0 +1,149 @@ +/* ownCloud Android client application + * Copyright (C) 2012 ownCloud + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.owncloud.android.oc_framework.network.webdav; + +import java.util.Date; + +import org.apache.jackrabbit.webdav.MultiStatusResponse; +import org.apache.jackrabbit.webdav.property.DavProperty; +import org.apache.jackrabbit.webdav.property.DavPropertyName; +import org.apache.jackrabbit.webdav.property.DavPropertySet; + + +import android.net.Uri; +import android.util.Log; + +public class WebdavEntry { + private String mName, mPath, mUri, mContentType, mEtag; + private long mContentLength, mCreateTimestamp, mModifiedTimestamp; + + public WebdavEntry(MultiStatusResponse ms, String splitElement) { + resetData(); + if (ms.getStatus().length != 0) { + mUri = ms.getHref(); + + mPath = mUri.split(splitElement, 2)[1]; + + int status = ms.getStatus()[0].getStatusCode(); + DavPropertySet propSet = ms.getProperties(status); + @SuppressWarnings("rawtypes") + DavProperty prop = propSet.get(DavPropertyName.DISPLAYNAME); + if (prop != null) { + mName = (String) prop.getName().toString(); + mName = mName.substring(1, mName.length()-1); + } + else { + String[] tmp = mPath.split("/"); + if (tmp.length > 0) + mName = tmp[tmp.length - 1]; + } + + // use unknown mimetype as default behavior + mContentType = "application/octet-stream"; + prop = propSet.get(DavPropertyName.GETCONTENTTYPE); + if (prop != null) { + mContentType = (String) prop.getValue(); + // dvelasco: some builds of ownCloud server 4.0.x added a trailing ';' to the MIME type ; if looks fixed, but let's be cautious + if (mContentType.indexOf(";") >= 0) { + mContentType = mContentType.substring(0, mContentType.indexOf(";")); + } + } + + // check if it's a folder in the standard way: see RFC2518 12.2 . RFC4918 14.3 + prop = propSet.get(DavPropertyName.RESOURCETYPE); + if (prop!= null) { + Object value = prop.getValue(); + if (value != null) { + mContentType = "DIR"; // a specific attribute would be better, but this is enough; unless while we have no reason to distinguish MIME types for folders + } + } + + prop = propSet.get(DavPropertyName.GETCONTENTLENGTH); + if (prop != null) + mContentLength = Long.parseLong((String) prop.getValue()); + + prop = propSet.get(DavPropertyName.GETLASTMODIFIED); + if (prop != null) { + Date d = WebdavUtils + .parseResponseDate((String) prop.getValue()); + mModifiedTimestamp = (d != null) ? d.getTime() : 0; + } + + prop = propSet.get(DavPropertyName.CREATIONDATE); + if (prop != null) { + Date d = WebdavUtils + .parseResponseDate((String) prop.getValue()); + mCreateTimestamp = (d != null) ? d.getTime() : 0; + } + + prop = propSet.get(DavPropertyName.GETETAG); + if (prop != null) { + mEtag = (String) prop.getValue(); + mEtag = mEtag.substring(1, mEtag.length()-1); + } + + } else { + Log.e("WebdavEntry", + "General fuckup, no status for webdav response"); + } + } + + public String path() { + return mPath; + } + + public String decodedPath() { + return Uri.decode(mPath); + } + + public String name() { + return mName; + } + + public boolean isDirectory() { + return mContentType.equals("DIR"); + } + + public String contentType() { + return mContentType; + } + + public String uri() { + return mUri; + } + + public long contentLength() { + return mContentLength; + } + + public long createTimestamp() { + return mCreateTimestamp; + } + + public long modifiedTimestamp() { + return mModifiedTimestamp; + } + + public String etag() { + return mEtag; + } + + private void resetData() { + mName = mUri = mContentType = null; + mContentLength = mCreateTimestamp = mModifiedTimestamp = 0; + } +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavUtils.java b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavUtils.java new file mode 100644 index 00000000..f5681de5 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/network/webdav/WebdavUtils.java @@ -0,0 +1,76 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.network.webdav; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.net.Uri; + +public class WebdavUtils { + public static final SimpleDateFormat DISPLAY_DATE_FORMAT = new SimpleDateFormat( + "dd.MM.yyyy hh:mm"); + private static final SimpleDateFormat DATETIME_FORMATS[] = { + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US), + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.sss'Z'", Locale.US), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US), + new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US), + new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) }; + + public static String prepareXmlForPropFind() { + String ret = ""; + return ret; + } + + public static String prepareXmlForPatch() { + return ""; + } + + public static Date parseResponseDate(String date) { + Date returnDate = null; + for (int i = 0; i < DATETIME_FORMATS.length; ++i) { + try { + returnDate = DATETIME_FORMATS[i].parse(date); + return returnDate; + } catch (ParseException e) { + } + } + return null; + } + + /** + * Encodes a path according to URI RFC 2396. + * + * If the received path doesn't start with "/", the method adds it. + * + * @param remoteFilePath Path + * @return Encoded path according to RFC 2396, always starting with "/" + */ + public static String encodePath(String remoteFilePath) { + String encodedPath = Uri.encode(remoteFilePath, "/"); + if (!encodedPath.startsWith("/")) + encodedPath = "/" + encodedPath; + return encodedPath; + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/operations/OnRemoteOperationListener.java b/oc_framework/src/com/owncloud/android/oc_framework/operations/OnRemoteOperationListener.java new file mode 100644 index 00000000..a81d4017 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/operations/OnRemoteOperationListener.java @@ -0,0 +1,25 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.operations; + +public interface OnRemoteOperationListener { + + void onRemoteOperationFinish(RemoteOperation caller, RemoteOperationResult result); + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/operations/OperationCancelledException.java b/oc_framework/src/com/owncloud/android/oc_framework/operations/OperationCancelledException.java new file mode 100644 index 00000000..d5fd3782 --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/operations/OperationCancelledException.java @@ -0,0 +1,28 @@ +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.owncloud.android.oc_framework.operations; + +public class OperationCancelledException extends Exception { + + /** + * Generated serial version - to avoid Java warning + */ + private static final long serialVersionUID = -6350981497740424983L; + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/operations/RemoteOperation.java b/oc_framework/src/com/owncloud/android/oc_framework/operations/RemoteOperation.java new file mode 100644 index 00000000..0900819a --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/operations/RemoteOperation.java @@ -0,0 +1,287 @@ +/* ownCloud Android client application + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.owncloud.android.oc_framework.operations; + +import java.io.IOException; + +import org.apache.commons.httpclient.Credentials; + +import com.owncloud.android.oc_framework.MainApp; +import com.owncloud.android.oc_framework.network.BearerCredentials; +import com.owncloud.android.oc_framework.network.OwnCloudClientUtils; +import com.owncloud.android.oc_framework.network.webdav.WebdavClient; +import com.owncloud.android.oc_framework.operations.RemoteOperationResult.ResultCode; + + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountsException; +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.util.Log; + + +/** + * Operation which execution involves one or several interactions with an ownCloud server. + * + * Provides methods to execute the operation both synchronously or asynchronously. + * + * @author David A. Velasco + */ +public abstract class RemoteOperation implements Runnable { + + private static final String TAG = RemoteOperation.class.getSimpleName(); + + /** ownCloud account in the remote ownCloud server to operate */ + private Account mAccount = null; + + /** Android Application context */ + private Context mContext = null; + + /** Object to interact with the remote server */ + private WebdavClient mClient = null; + + /** Callback object to notify about the execution of the remote operation */ + private OnRemoteOperationListener mListener = null; + + /** Handler to the thread where mListener methods will be called */ + private Handler mListenerHandler = null; + + /** Activity */ + private Activity mCallerActivity; + + + /** + * Abstract method to implement the operation in derived classes. + */ + protected abstract RemoteOperationResult run(WebdavClient client); + + + /** + * Synchronously executes the remote operation on the received ownCloud account. + * + * Do not call this method from the main thread. + * + * This method should be used whenever an ownCloud account is available, instead of {@link #execute(WebdavClient)}. + * + * @param account ownCloud account in remote ownCloud server to reach during the execution of the operation. + * @param context Android context for the component calling the method. + * @return Result of the operation. + */ + public final RemoteOperationResult execute(Account account, Context context) { + if (account == null) + throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account"); + if (context == null) + throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context"); + mAccount = account; + mContext = context.getApplicationContext(); + try { + mClient = OwnCloudClientUtils.createOwnCloudClient(mAccount, mContext); + } catch (Exception e) { + Log.e(TAG, "Error while trying to access to " + mAccount.name, e); + return new RemoteOperationResult(e); + } + return run(mClient); + } + + + /** + * Synchronously executes the remote operation + * + * Do not call this method from the main thread. + * + * @param client Client object to reach an ownCloud server during the execution of the operation. + * @return Result of the operation. + */ + public final RemoteOperationResult execute(WebdavClient client) { + if (client == null) + throw new IllegalArgumentException("Trying to execute a remote operation with a NULL WebdavClient"); + mClient = client; + return run(client); + } + + + /** + * Asynchronously executes the remote operation + * + * This method should be used whenever an ownCloud account is available, instead of {@link #execute(WebdavClient)}. + * + * @param account ownCloud account in remote ownCloud server to reach during the execution of the operation. + * @param context Android context for the component calling the method. + * @param listener Listener to be notified about the execution of the operation. + * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called. + * @return Thread were the remote operation is executed. + */ + public final Thread execute(Account account, Context context, OnRemoteOperationListener listener, Handler listenerHandler, Activity callerActivity) { + if (account == null) + throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Account"); + if (context == null) + throw new IllegalArgumentException("Trying to execute a remote operation with a NULL Context"); + mAccount = account; + mContext = context.getApplicationContext(); + mCallerActivity = callerActivity; + mClient = null; // the client instance will be created from mAccount and mContext in the runnerThread to create below + + mListener = listener; + + mListenerHandler = listenerHandler; + + Thread runnerThread = new Thread(this); + runnerThread.start(); + return runnerThread; + } + + + /** + * Asynchronously executes the remote operation + * + * @param client Client object to reach an ownCloud server during the execution of the operation. + * @param listener Listener to be notified about the execution of the operation. + * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called. + * @return Thread were the remote operation is executed. + */ + public final Thread execute(WebdavClient client, OnRemoteOperationListener listener, Handler listenerHandler) { + if (client == null) { + throw new IllegalArgumentException("Trying to execute a remote operation with a NULL WebdavClient"); + } + mClient = client; + + if (listener == null) { + throw new IllegalArgumentException("Trying to execute a remote operation asynchronously without a listener to notiy the result"); + } + mListener = listener; + + if (listenerHandler == null) { + throw new IllegalArgumentException("Trying to execute a remote operation asynchronously without a handler to the listener's thread"); + } + mListenerHandler = listenerHandler; + + Thread runnerThread = new Thread(this); + runnerThread.start(); + return runnerThread; + } + + /** + * Synchronously retries the remote operation using the same WebdavClient in the last call to {@link RemoteOperation#execute(WebdavClient)} + * + * @param listener Listener to be notified about the execution of the operation. + * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called. + * @return Thread were the remote operation is executed. + */ + public final RemoteOperationResult retry() { + return execute(mClient); + } + + /** + * Asynchronously retries the remote operation using the same WebdavClient in the last call to {@link RemoteOperation#execute(WebdavClient, OnRemoteOperationListener, Handler)} + * + * @param listener Listener to be notified about the execution of the operation. + * @param listenerHandler Handler associated to the thread where the methods of the listener objects must be called. + * @return Thread were the remote operation is executed. + */ + public final Thread retry(OnRemoteOperationListener listener, Handler listenerHandler) { + return execute(mClient, listener, listenerHandler); + } + + + /** + * Asynchronous execution of the operation + * started by {@link RemoteOperation#execute(WebdavClient, OnRemoteOperationListener, Handler)}, + * and result posting. + * + * TODO refactor && clean the code; now it's a mess + */ + @Override + public final void run() { + RemoteOperationResult result = null; + boolean repeat = false; + do { + try{ + if (mClient == null) { + if (mAccount != null && mContext != null) { + if (mCallerActivity != null) { + mClient = OwnCloudClientUtils.createOwnCloudClient(mAccount, mContext, mCallerActivity); + } else { + mClient = OwnCloudClientUtils.createOwnCloudClient(mAccount, mContext); + } + } else { + throw new IllegalStateException("Trying to run a remote operation asynchronously with no client instance or account"); + } + } + + } catch (IOException e) { + Log.e(TAG, "Error while trying to access to " + mAccount.name, new AccountsException("I/O exception while trying to authorize the account", e)); + result = new RemoteOperationResult(e); + + } catch (AccountsException e) { + Log.e(TAG, "Error while trying to access to " + mAccount.name, e); + result = new RemoteOperationResult(e); + } + + if (result == null) + result = run(mClient); + + repeat = false; + if (mCallerActivity != null && mAccount != null && mContext != null && !result.isSuccess() && +// (result.getCode() == ResultCode.UNAUTHORIZED || (result.isTemporalRedirection() && result.isIdPRedirection()))) { + (result.getCode() == ResultCode.UNAUTHORIZED || result.isIdPRedirection())) { + /// possible fail due to lack of authorization in an operation performed in foreground + Credentials cred = mClient.getCredentials(); + String ssoSessionCookie = mClient.getSsoSessionCookie(); + if (cred != null || ssoSessionCookie != null) { + /// confirmed : unauthorized operation + AccountManager am = AccountManager.get(mContext); + boolean bearerAuthorization = (cred != null && cred instanceof BearerCredentials); + boolean samlBasedSsoAuthorization = (cred == null && ssoSessionCookie != null); + if (bearerAuthorization) { + am.invalidateAuthToken(MainApp.getAccountType(), ((BearerCredentials)cred).getAccessToken()); + } else if (samlBasedSsoAuthorization ) { + am.invalidateAuthToken(MainApp.getAccountType(), ssoSessionCookie); + } else { + am.clearPassword(mAccount); + } + mClient = null; + repeat = true; // when repeated, the creation of a new OwnCloudClient after erasing the saved credentials will trigger the login activity + result = null; + } + } + } while (repeat); + + final RemoteOperationResult resultToSend = result; + if (mListenerHandler != null && mListener != null) { + mListenerHandler.post(new Runnable() { + @Override + public void run() { + mListener.onRemoteOperationFinish(RemoteOperation.this, resultToSend); + } + }); + } + } + + + /** + * Returns the current client instance to access the remote server. + * + * @return Current client instance to access the remote server. + */ + public final WebdavClient getClient() { + return mClient; + } + + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/operations/RemoteOperationResult.java b/oc_framework/src/com/owncloud/android/oc_framework/operations/RemoteOperationResult.java new file mode 100644 index 00000000..543b3a4b --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/operations/RemoteOperationResult.java @@ -0,0 +1,339 @@ +package com.owncloud.android.oc_framework.operations; +/* ownCloud Android client application + * Copyright (C) 2012 Bartek Przybylski + * Copyright (C) 2012-2013 ownCloud Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + + + +import java.io.IOException; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLException; + +import org.apache.commons.httpclient.ConnectTimeoutException; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.jackrabbit.webdav.DavException; + +import com.owncloud.android.oc_framework.authentication.AccountUtils.AccountNotFoundException; +import com.owncloud.android.oc_framework.network.CertificateCombinedException; + +import android.accounts.Account; +import android.accounts.AccountsException; +import android.util.Log; + + +/** + * The result of a remote operation required to an ownCloud server. + * + * Provides a common classification of remote operation results for all the + * application. + * + * @author David A. Velasco + */ +public class RemoteOperationResult implements Serializable { + + /** Generated - should be refreshed every time the class changes!! */ + private static final long serialVersionUID = -4415103901492836870L; + + + + private static final String TAG = "RemoteOperationResult"; + + public enum ResultCode { + OK, + OK_SSL, + OK_NO_SSL, + UNHANDLED_HTTP_CODE, + UNAUTHORIZED, + FILE_NOT_FOUND, + INSTANCE_NOT_CONFIGURED, + UNKNOWN_ERROR, + WRONG_CONNECTION, + TIMEOUT, + INCORRECT_ADDRESS, + HOST_NOT_AVAILABLE, + NO_NETWORK_CONNECTION, + SSL_ERROR, + SSL_RECOVERABLE_PEER_UNVERIFIED, + BAD_OC_VERSION, + CANCELLED, + INVALID_LOCAL_FILE_NAME, + INVALID_OVERWRITE, + CONFLICT, + OAUTH2_ERROR, + SYNC_CONFLICT, + LOCAL_STORAGE_FULL, + LOCAL_STORAGE_NOT_MOVED, + LOCAL_STORAGE_NOT_COPIED, + OAUTH2_ERROR_ACCESS_DENIED, + QUOTA_EXCEEDED, + ACCOUNT_NOT_FOUND, + ACCOUNT_EXCEPTION, + ACCOUNT_NOT_NEW, + ACCOUNT_NOT_THE_SAME + } + + private boolean mSuccess = false; + private int mHttpCode = -1; + private Exception mException = null; + private ResultCode mCode = ResultCode.UNKNOWN_ERROR; + private String mRedirectedLocation; + + public RemoteOperationResult(ResultCode code) { + mCode = code; + mSuccess = (code == ResultCode.OK || code == ResultCode.OK_SSL || code == ResultCode.OK_NO_SSL); + } + + private RemoteOperationResult(boolean success, int httpCode) { + mSuccess = success; + mHttpCode = httpCode; + + if (success) { + mCode = ResultCode.OK; + + } else if (httpCode > 0) { + switch (httpCode) { + case HttpStatus.SC_UNAUTHORIZED: + mCode = ResultCode.UNAUTHORIZED; + break; + case HttpStatus.SC_NOT_FOUND: + mCode = ResultCode.FILE_NOT_FOUND; + break; + case HttpStatus.SC_INTERNAL_SERVER_ERROR: + mCode = ResultCode.INSTANCE_NOT_CONFIGURED; + break; + case HttpStatus.SC_CONFLICT: + mCode = ResultCode.CONFLICT; + break; + case HttpStatus.SC_INSUFFICIENT_STORAGE: + mCode = ResultCode.QUOTA_EXCEEDED; + break; + default: + mCode = ResultCode.UNHANDLED_HTTP_CODE; + Log.d(TAG, "RemoteOperationResult has processed UNHANDLED_HTTP_CODE: " + httpCode); + } + } + } + + public RemoteOperationResult(boolean success, int httpCode, Header[] headers) { + this(success, httpCode); + if (headers != null) { + Header current; + for (int i=0; i= HttpStatus.SC_INTERNAL_SERVER_ERROR); + } + + public boolean isException() { + return (mException != null); + } + + public boolean isTemporalRedirection() { + return (mHttpCode == 302 || mHttpCode == 307); + } + + public String getRedirectedLocation() { + return mRedirectedLocation; + } + + public boolean isIdPRedirection() { + return (mRedirectedLocation != null && + (mRedirectedLocation.toUpperCase().contains("SAML") || + mRedirectedLocation.toLowerCase().contains("wayf"))); + } + +} diff --git a/oc_framework/src/com/owncloud/android/oc_framework/operations/remote/CreateRemoteFolderOperation.java b/oc_framework/src/com/owncloud/android/oc_framework/operations/remote/CreateRemoteFolderOperation.java new file mode 100644 index 00000000..47870dec --- /dev/null +++ b/oc_framework/src/com/owncloud/android/oc_framework/operations/remote/CreateRemoteFolderOperation.java @@ -0,0 +1,93 @@ +package com.owncloud.android.oc_framework.operations.remote; + +import java.io.File; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.jackrabbit.webdav.client.methods.MkColMethod; + +import android.util.Log; + +import com.owncloud.android.oc_framework.network.webdav.WebdavClient; +import com.owncloud.android.oc_framework.network.webdav.WebdavUtils; +import com.owncloud.android.oc_framework.operations.RemoteOperation; +import com.owncloud.android.oc_framework.operations.RemoteOperationResult; + + + +/** + * Remote operation performing the creation of a new folder in the ownCloud server. + * + * @author David A. Velasco + * @author masensio + * + */ +public class CreateRemoteFolderOperation extends RemoteOperation { + + private static final String TAG = CreateRemoteFolderOperation.class.getSimpleName(); + + private static final int READ_TIMEOUT = 10000; + private static final int CONNECTION_TIMEOUT = 5000; + + private static final String PATH_SEPARATOR = "/"; + + protected String mRemotePath; + protected boolean mCreateFullPath; + + /** + * Constructor + * + * @param remotePath Full path to the new directory to create in the remote server. + * @param createFullPath 'True' means that all the ancestor folders should be created if don't exist yet. + */ + public CreateRemoteFolderOperation(String remotePath, boolean createFullPath) { + mRemotePath = remotePath; + mCreateFullPath = createFullPath; + } + + /** + * Performs the operation + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Override + protected RemoteOperationResult run(WebdavClient client) { + RemoteOperationResult result = null; + MkColMethod mkcol = null; + + try { + mkcol = new MkColMethod(client.getBaseUri() + WebdavUtils.encodePath(mRemotePath)); + int status = client.executeMethod(mkcol, READ_TIMEOUT, CONNECTION_TIMEOUT); + if (!mkcol.succeeded() && mkcol.getStatusCode() == HttpStatus.SC_CONFLICT && mCreateFullPath) { + result = createParentFolder(getParentPath(), client); + status = client.executeMethod(mkcol, READ_TIMEOUT, CONNECTION_TIMEOUT); // second (and last) try + } + + result = new RemoteOperationResult(mkcol.succeeded(), status, mkcol.getResponseHeaders()); + Log.d(TAG, "Create directory " + mRemotePath + ": " + result.getLogMessage()); + client.exhaustResponse(mkcol.getResponseBodyAsStream()); + + } catch (Exception e) { + result = new RemoteOperationResult(e); + Log.e(TAG, "Create directory " + mRemotePath + ": " + result.getLogMessage(), e); + + } finally { + if (mkcol != null) + mkcol.releaseConnection(); + } + return result; + } + + + private RemoteOperationResult createParentFolder(String parentPath, WebdavClient client) { + RemoteOperation operation = new CreateRemoteFolderOperation( parentPath, + mCreateFullPath); + return operation.execute(client); + } + + private String getParentPath() { + String parentPath = new File(mRemotePath).getParent(); + parentPath = parentPath.endsWith(PATH_SEPARATOR) ? parentPath : parentPath + PATH_SEPARATOR; + return parentPath; + } + +}