Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e57e07cd authored by Mohammed Althaf T's avatar Mohammed Althaf T 😊
Browse files

AM: import /e/ Specific changes from account manager

parent 4f1ca571
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -204,5 +204,5 @@ dependencies {
    // third-party libs
    implementation(libs.guava)
    implementation(libs.okhttp.base)
    implementation(libs.openid.appauth)
    implementation(libs.appauth)
}
 No newline at end of file
+10 −5
Original line number Diff line number Diff line
@@ -14,6 +14,11 @@
    <uses-permission android:name="foundation.e.permission.READ_TASKS"/>
    <uses-permission android:name="foundation.e.permission.WRITE_TASKS"/>

    <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"
            tools:ignore="ProtectedPermissions" />

    <uses-permission android:name="foundation.e.permission.ADD_ACCOUNT"/>

    <application>

        <!-- AppAuth login flow redirect -->
@@ -254,16 +259,16 @@
        <receiver
                android:name="foundation.e.accountmanager.sync.SyncBroadcastReceiver" />

        <activity
                android:name="com.owncloud.android.ui.activity.SsoGrantPermissionActivity"
                android:exported="true" />

        <service
                android:name="com.owncloud.android.services.AccountManagerService"
                android:enabled="true"
                android:exported="true"
                tools:ignore="ExportedService" />

        <activity
                android:name="com.owncloud.android.ui.activity.SsoGrantPermissionActivity"
                android:exported="true" />

        <!-- endregion /e/ Specific -->

    </application>
+9 −1
Original line number Diff line number Diff line
@@ -183,7 +183,6 @@ dependencies {
    implementation(libs.okhttp.base)
    implementation(libs.okhttp.brotli)
    implementation(libs.okhttp.logging)
    implementation(libs.openid.appauth)
    implementation(libs.unifiedpush) {
        // UnifiedPush connector seems to be using a workaround by importing this library.
        // Will be removed after https://github.com/tink-crypto/tink-java-apps/pull/5 is merged.
@@ -197,9 +196,12 @@ dependencies {
    implementation(libs.commons.lang)

    // e-Specific dependencies
    implementation(libs.appauth)
    implementation(libs.android.singlesignon)
    implementation(libs.androidx.runtime.livedata)
    implementation(libs.elib)
    implementation(libs.ez.vcard)
    implementation(libs.jackrabbit.webdav)
    implementation(libs.synctools) {
        exclude(group="androidx.test")
        exclude(group = "junit")
@@ -207,6 +209,12 @@ dependencies {
    implementation(libs.ical4j) {
        exclude(group = "commons-logging", module = "commons-logging")
    }
    implementation(libs.nextcloud.library) {
        exclude(group = "org.ogce", module = "xpp3")
    }
    implementation(libs.commons.httpclient) {
        exclude(group = "commons-logging", module = "commons-logging")
    }

    // for tests
    androidTestImplementation(libs.androidx.arch.core.testing)
+7 −0
Original line number Diff line number Diff line
@@ -20,3 +20,10 @@
# https://github.com/bitfireAT/davx5/issues/711 / https://github.com/square/okhttp/issues/8574
-keep class okhttp3.internal.idn.IdnaMappingTable { *; }
-keep class okhttp3.internal.idn.IdnaMappingTableInstanceKt{ *; }

-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings

-keep class com.nextcloud.android.sso.** { *; }
-keep interface com.nextcloud.android.sso.** { *; }
-keep class org.apache.commons.httpclient.** { *; }
-keep interface org.apache.commons.httpclient.** { *; }
+60 −172
Original line number Diff line number Diff line
@@ -21,13 +21,13 @@ import android.text.TextUtils;
import com.nextcloud.android.sso.aidl.IInputStreamService;
import com.nextcloud.android.sso.aidl.NextcloudRequest;
import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil;
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.android.utils.AccountManagerUtils;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.lib.common.OwnCloudAccount;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.OwnCloudClientManager;
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.utils.EncryptionUtils;
import com.owncloud.android.lib.common.operations.RemoteOperation;

import org.apache.commons.httpclient.HttpConnection;
import org.apache.commons.httpclient.HttpMethodBase;
@@ -58,10 +58,12 @@ import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import static com.nextcloud.android.sso.Constants.DELIMITER;
import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_NOT_FOUND;
import static com.nextcloud.android.sso.Constants.EXCEPTION_HTTP_REQUEST_FAILED;
import static com.nextcloud.android.sso.Constants.EXCEPTION_INVALID_REQUEST_URL;
@@ -74,7 +76,6 @@ import static com.nextcloud.android.sso.Constants.SSO_SHARED_PREFERENCE;
 * Stream binder to pass usable InputStreams across the process boundary in Android.
 */
public class InputStreamBinder extends IInputStreamService.Stub {

    private final static String TAG = "InputStreamBinder";
    private static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
    private static final String CHARSET_UTF8 = "UTF-8";
@@ -83,13 +84,12 @@ public class InputStreamBinder extends IInputStreamService.Stub {
    private static final int HTTP_STATUS_CODE_MULTIPLE_CHOICES = 300;

    private static final char PATH_SEPARATOR = '/';
    private static final int ZERO_LENGTH = 0;
    private Context context;
    private UserAccountManager accountManager;
    public static final String DELIMITER = "_";
    private final Context context;
    private final Logger logger = Logger.getLogger(TAG);

    public InputStreamBinder(Context context, UserAccountManager accountManager) {
    public InputStreamBinder(Context context) {
        this.context = context;
        this.accountManager = accountManager;
    }

    public ParcelFileDescriptor performNextcloudRequestV2(ParcelFileDescriptor input) {
@@ -112,7 +112,7 @@ public class InputStreamBinder extends IInputStreamService.Stub {
            NextcloudRequest request = deserializeObjectAndCloseStream(is);
            response = processRequestV2(request, requestBodyInputStream);
        } catch (Exception e) {
            Log_OC.e(TAG, "Error during Nextcloud request", e);
            logger.log(Level.SEVERE, "Error during Nextcloud request", e);
            exception = e;
        }

@@ -122,61 +122,22 @@ public class InputStreamBinder extends IInputStreamService.Stub {
            InputStream resultStream = new java.io.SequenceInputStream(exceptionStream, response.getBody());

            return ParcelFileDescriptorUtil.pipeFrom(resultStream,
                                                     thread -> Log_OC.d(TAG, "Done sending result"),
                                                     response.getMethod());
                    thread -> logger.log(Level.INFO, "InputStreamBinder: Done sending result"));
        } catch (IOException e) {
            Log_OC.e(TAG, "Error while sending response back to client app", e);
            logger.log(Level.SEVERE, "Error while sending response back to client app", e);
        }

        return null;
    }

    public ParcelFileDescriptor performNextcloudRequest(ParcelFileDescriptor input) {
        return performNextcloudRequestAndBodyStream(input, null);
        return performNextcloudRequestAndBodyStreamV2(input, null);
    }

    public ParcelFileDescriptor performNextcloudRequestAndBodyStream(
            ParcelFileDescriptor input,
            ParcelFileDescriptor requestBodyParcelFileDescriptor) {
        // read the input
        final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(input);

        final InputStream requestBodyInputStream = requestBodyParcelFileDescriptor != null ?
            new ParcelFileDescriptor.AutoCloseInputStream(requestBodyParcelFileDescriptor) : null;
        Exception exception = null;
        HttpMethodBase httpMethod = null;
        InputStream httpStream = new InputStream() {
            @Override
            public int read() {
                return ZERO_LENGTH;
            }
        };

        try {
            // Start request and catch exceptions
            NextcloudRequest request = deserializeObjectAndCloseStream(is);
            httpMethod = processRequest(request, requestBodyInputStream);
            httpStream = httpMethod.getResponseBodyAsStream();
        } catch (Exception e) {
            Log_OC.e(TAG, "Error during Nextcloud request", e);
            exception = e;
        }

        try {
            // Write exception to the stream followed by the actual network stream
            InputStream exceptionStream = serializeObjectToInputStream(exception);
            InputStream resultStream;
            if (httpStream != null) {
                resultStream = new java.io.SequenceInputStream(exceptionStream, httpStream);
            } else {
                resultStream = exceptionStream;
            }
            return ParcelFileDescriptorUtil.pipeFrom(resultStream,
                                                     thread -> Log_OC.d(TAG, "Done sending result"),
                                                     httpMethod);
        } catch (IOException e) {
            Log_OC.e(TAG, "Error while sending response back to client app", e);
        }
        return null;
        return performNextcloudRequestAndBodyStreamV2(input, requestBodyParcelFileDescriptor);
    }

    private ByteArrayInputStream serializeObjectToInputStreamV2(Exception exception, String headers) {
@@ -190,21 +151,12 @@ public class InputStreamBinder extends IInputStreamService.Stub {

            baosByteArray = baos.toByteArray();
        } catch (IOException e) {
            Log_OC.e(TAG, "Error while sending response back to client app", e);
            logger.log(Level.SEVERE, "Error while sending response back to client app", e);
        }

        return new ByteArrayInputStream(baosByteArray);
    }

    private <T extends Serializable> ByteArrayInputStream serializeObjectToInputStream(T obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        oos.flush();
        oos.close();
        return new ByteArrayInputStream(baos.toByteArray());
    }

    private <T extends Serializable> T deserializeObjectAndCloseStream(InputStream is) throws IOException,
        ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(is);
@@ -307,78 +259,26 @@ public class InputStreamBinder extends IInputStreamService.Stub {
        return method;
    }

    private HttpMethodBase processRequest(final NextcloudRequest request, final InputStream requestBodyInputStream)
        throws UnsupportedOperationException,
        com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException,
        OperationCanceledException, AuthenticatorException, IOException {
        Account account = accountManager.getAccountByName(request.getAccountName());
        if (account == null) {
            throw new IllegalStateException(EXCEPTION_ACCOUNT_NOT_FOUND);
        }

        // Validate token
        if (!isValid(request)) {
            throw new IllegalStateException(EXCEPTION_INVALID_TOKEN);
        }

        // Validate URL
        if (request.getUrl().length() == 0 || request.getUrl().charAt(0) != PATH_SEPARATOR) {
            throw new IllegalStateException(EXCEPTION_INVALID_REQUEST_URL,
                                            new IllegalStateException("URL need to start with a /"));
        }

        OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
        OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
        OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context);

        HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream);

        if (request.getParameterV2() != null && !request.getParameterV2().isEmpty()) {
            method.setQueryString(convertListToNVP(request.getParameterV2()));
        } else {
            method.setQueryString(convertMapToNVP(request.getParameter()));
        }
        method.addRequestHeader("OCS-APIREQUEST", "true");

        for (Map.Entry<String, List<String>> header : request.getHeader().entrySet()) {
            // https://stackoverflow.com/a/3097052
            method.addRequestHeader(header.getKey(), TextUtils.join(",", header.getValue()));

            if ("OCS-APIREQUEST".equalsIgnoreCase(header.getKey())) {
                throw new IllegalStateException(
                    "The 'OCS-APIREQUEST' header will be automatically added by the Nextcloud SSO Library. " +
                        "Please remove the header before making a request");
            }
        }

        client.setFollowRedirects(request.isFollowRedirects());
        int status = client.executeMethod(method);

        // Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
        if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
            return method;
        } else {
            InputStream inputStream = method.getResponseBodyAsStream();
            String total = "No response body";

            // If response body is available
            if (inputStream != null) {
                total = inputStreamToString(inputStream);
                Log_OC.e(TAG, total);
    /*
    * for non ocs/dav requests (nextcloud app: ex: notes app), when OIDC is used, we need to pass an special header.
    * We should not pass this header for ocs/dav requests as it can cause session cookie not being used for those request.
    *
    * These nextcloud app request paths contain `/index.php/apps/` on them.
     */
    private boolean shouldAddHeaderForOidcLogin(@NonNull Context context, @NonNull Account account, @NonNull String path) {
        boolean isOidcAccount = AccountManagerUtils.isOidcAccount(context, account);
        if (!isOidcAccount) {
            return false;
        }

            method.releaseConnection();
            throw new IllegalStateException(EXCEPTION_HTTP_REQUEST_FAILED,
                                            new IllegalStateException(String.valueOf(status),
                                                                      new IllegalStateException(total)));
        }
        return path.contains("/index.php/apps/");
    }

    private Response processRequestV2(final NextcloudRequest request, final InputStream requestBodyInputStream)
        throws UnsupportedOperationException,
        com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException,
        OperationCanceledException, AuthenticatorException, IOException {
        Account account = accountManager.getAccountByName(request.getAccountName());
        Account account = AccountManagerUtils.getAccountByName(context, request.getAccountName());
        if (account == null) {
            throw new IllegalStateException(EXCEPTION_ACCOUNT_NOT_FOUND);
        }
@@ -389,39 +289,48 @@ public class InputStreamBinder extends IInputStreamService.Stub {
        }

        // Validate URL
        if (request.getUrl().length() == 0 || request.getUrl().charAt(0) != PATH_SEPARATOR) {
        if (request.getUrl().isEmpty() || request.getUrl().charAt(0) != PATH_SEPARATOR) {
            throw new IllegalStateException(EXCEPTION_INVALID_REQUEST_URL,
                    new IllegalStateException("URL need to start with a /"));
        }

        OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
        OwnCloudAccount ocAccount = new OwnCloudAccount(account, context);
        OwnCloudClient client = ownCloudClientManager.getClientFor(ocAccount, context);
        if (AccountManagerUtils.isOidcAccount(context, account)) {
            // Blocking call
            OidcTokenRefresher.refresh(context, account);
        }

        final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton();
        final OwnCloudAccount ownCloudAccount = new OwnCloudAccount(account, context);
        final OwnCloudClient client = ownCloudClientManager.getClientFor(ownCloudAccount, context, OwnCloudClient.DONT_USE_COOKIES);

        HttpMethodBase method = buildMethod(request, client.getBaseUri(), requestBodyInputStream);

        if (request.getParameterV2() != null && !request.getParameterV2().isEmpty()) {
            method.setQueryString(convertListToNVP(request.getParameterV2()));
        } else {
            method.setQueryString(convertMapToNVP(request.getParameter()));
        }

        method.addRequestHeader("OCS-APIREQUEST", "true");

        for (Map.Entry<String, List<String>> header : request.getHeader().entrySet()) {
            // https://stackoverflow.com/a/3097052
            method.addRequestHeader(header.getKey(), TextUtils.join(",", header.getValue()));

            if ("OCS-APIREQUEST".equalsIgnoreCase(header.getKey())) {
                throw new IllegalStateException(
                    "The 'OCS-APIREQUEST' header will be automatically added by the Nextcloud SSO Library. " +
                        "Please remove the header before making a request");
        }

        method.setRequestHeader(
                RemoteOperation.OCS_API_HEADER,
                RemoteOperation.OCS_API_HEADER_VALUE
        );

        if (shouldAddHeaderForOidcLogin(context, account, request.getUrl())) {
            method.setRequestHeader(
                    RemoteOperation.OIDC_LOGIN_WITH_TOKEN,
                    RemoteOperation.OIDC_LOGIN_WITH_TOKEN_VALUE
            );
        }

        client.setFollowRedirects(request.isFollowRedirects());
        client.setFollowRedirects(true);
        int status = client.executeMethod(method);

        ownCloudClientManager.saveAllClients(context, account.type);

        // Check if status code is 2xx --> https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
        if (status >= HTTP_STATUS_CODE_OK && status < HTTP_STATUS_CODE_MULTIPLE_CHOICES) {
            return new Response(method);
@@ -432,7 +341,7 @@ public class InputStreamBinder extends IInputStreamService.Stub {
            // If response body is available
            if (inputStream != null) {
                total = inputStreamToString(inputStream);
                Log_OC.e(TAG, total);
                logger.severe(total);
            }

            method.releaseConnection();
@@ -443,19 +352,12 @@ public class InputStreamBinder extends IInputStreamService.Stub {
    }

    private boolean isValid(NextcloudRequest request) {
        String[] callingPackageNames = context.getPackageManager().getPackagesForUid(Binder.getCallingUid());
        String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());

        SharedPreferences sharedPreferences = context.getSharedPreferences(SSO_SHARED_PREFERENCE,
                Context.MODE_PRIVATE);
        for (String callingPackageName : callingPackageNames) {
        String hash = sharedPreferences.getString(callingPackageName + DELIMITER + request.getAccountName(), "");
            if (hash.isEmpty())
                continue;
            if (validateToken(hash, request.getToken())) {
                return true;
            }
        }
        return false;
        return validateToken(hash, request.getToken());
    }

    private boolean validateToken(String hash, String token) {
@@ -500,20 +402,6 @@ public class InputStreamBinder extends IInputStreamService.Stub {
        }
    }

    @VisibleForTesting
    public static NameValuePair[] convertMapToNVP(Map<String, String> map) {
        final var nvp = new NameValuePair[map.size()];
        int i = 0;

        for (Map.Entry<String, String> entry : map.entrySet()) {
            final var nameValuePair = new NameValuePair(entry.getKey(), entry.getValue());
            nvp[i] = nameValuePair;
            i++;
        }

        return nvp;
    }

    @VisibleForTesting
    public static NameValuePair[] convertListToNVP(Collection<QueryParam> list) {
        NameValuePair[] nvp = new NameValuePair[list.size()];
Loading