From 8e3931edf6a8d10701c88d4ade00380431905b58 Mon Sep 17 00:00:00 2001 From: tobiasKaminsky Date: Thu, 24 Oct 2019 07:53:57 +0200 Subject: [PATCH] SSO: return response headers new headers in parallel Signed-off-by: tobiasKaminsky --- scripts/analysis/findbugs-results.txt | 2 +- .../android/sso/aidl/IInputStreamService.aidl | 5 + .../android/sso/InputStreamBinder.java | 121 +++++++++++++++++- .../nextcloud/android/sso/PlainHeader.java | 54 ++++++++ .../com/nextcloud/android/sso/Response.java | 65 ++++++++++ 5 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/nextcloud/android/sso/PlainHeader.java create mode 100644 src/main/java/com/nextcloud/android/sso/Response.java diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 1e59c84a3a..5721413633 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -420 \ No newline at end of file +423 diff --git a/src/main/aidl/com/nextcloud/android/sso/aidl/IInputStreamService.aidl b/src/main/aidl/com/nextcloud/android/sso/aidl/IInputStreamService.aidl index eacceb8b42..fdff85181b 100644 --- a/src/main/aidl/com/nextcloud/android/sso/aidl/IInputStreamService.aidl +++ b/src/main/aidl/com/nextcloud/android/sso/aidl/IInputStreamService.aidl @@ -24,4 +24,9 @@ interface IInputStreamService { in ParcelFileDescriptor requestBodyParcelFileDescriptor); ParcelFileDescriptor performNextcloudRequest(in ParcelFileDescriptor input); + + ParcelFileDescriptor performNextcloudRequestAndBodyStreamV2(in ParcelFileDescriptor input, + in ParcelFileDescriptor requestBodyParcelFileDescriptor); + + ParcelFileDescriptor performNextcloudRequestV2(in ParcelFileDescriptor input); } diff --git a/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java b/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java index 257442e9fe..20d2503dfc 100644 --- a/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java +++ b/src/main/java/com/nextcloud/android/sso/InputStreamBinder.java @@ -94,6 +94,7 @@ 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; @@ -112,6 +113,42 @@ public class InputStreamBinder extends IInputStreamService.Stub { return nvp; } + public ParcelFileDescriptor performNextcloudRequestV2(ParcelFileDescriptor input) { + return performNextcloudRequestAndBodyStreamV2(input, null); + } + + public ParcelFileDescriptor performNextcloudRequestAndBodyStreamV2( + 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; + Response response = new Response(); + + try { + // Start request and catch exceptions + NextcloudRequest request = deserializeObjectAndCloseStream(is); + response = processRequestV2(request, requestBodyInputStream); + } 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 = serializeObjectToInputStreamV2(exception, response.getPlainHeadersString()); + InputStream resultStream = new java.io.SequenceInputStream(exceptionStream, response.getBody()); + + return ParcelFileDescriptorUtil.pipeFrom(resultStream, thread -> Log.d(TAG, "Done sending result")); + } catch (IOException e) { + Log_OC.e(TAG, "Error while sending response back to client app", e); + } + return null; + } + public ParcelFileDescriptor performNextcloudRequest(ParcelFileDescriptor input) { return performNextcloudRequestAndBodyStream(input, null); } @@ -128,7 +165,7 @@ public class InputStreamBinder extends IInputStreamService.Stub { InputStream httpStream = new InputStream() { @Override public int read() { - return 0; + return ZERO_LENGTH; } }; @@ -157,6 +194,23 @@ public class InputStreamBinder extends IInputStreamService.Stub { return null; } + private ByteArrayInputStream serializeObjectToInputStreamV2(Exception exception, String headers) { + byte[] baosByteArray = new byte[0]; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(exception); + oos.writeObject(headers); + oos.flush(); + oos.close(); + + baosByteArray = baos.toByteArray(); + } catch (IOException e) { + Log_OC.e(TAG, "Error while sending response back to client app", e); + } + + return new ByteArrayInputStream(baosByteArray); + } + private ByteArrayInputStream serializeObjectToInputStream(T obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); @@ -315,6 +369,71 @@ public class InputStreamBinder extends IInputStreamService.Stub { } } + 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()); + 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); + + method.setQueryString(convertMapToNVP(request.getParameter())); + method.addRequestHeader("OCS-APIREQUEST", "true"); + + for (Map.Entry> 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 new Response(method.getResponseBodyAsStream(), method.getResponseHeaders()); + } else { + StringBuilder total = new StringBuilder(); + InputStream inputStream = method.getResponseBodyAsStream(); + // If response body is available + if (inputStream != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line = reader.readLine(); + while (line != null) { + total.append(line).append('\n'); + line = reader.readLine(); + } + Log_OC.e(TAG, total.toString()); + } + throw new IllegalStateException(EXCEPTION_HTTP_REQUEST_FAILED, + new IllegalStateException(String.valueOf(status), + new IllegalStateException(total.toString()))); + } + } + private boolean isValid(NextcloudRequest request) { String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid()); diff --git a/src/main/java/com/nextcloud/android/sso/PlainHeader.java b/src/main/java/com/nextcloud/android/sso/PlainHeader.java new file mode 100644 index 0000000000..43afe66c70 --- /dev/null +++ b/src/main/java/com/nextcloud/android/sso/PlainHeader.java @@ -0,0 +1,54 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2019 Tobias Kaminsky + * Copyright (C) 2019 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.android.sso; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import lombok.Getter; + +public class PlainHeader implements Serializable { + private static final long serialVersionUID = 3284979177401282512L; + + @Getter + private String name; + @Getter + private String value; + + PlainHeader(String name, String value) { + this.name = name; + this.value = value; + } + + private void writeObject(ObjectOutputStream oos) throws IOException { + oos.writeObject(name); + oos.writeObject(value); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + name = (String) in.readObject(); + value = (String) in.readObject(); + } +} diff --git a/src/main/java/com/nextcloud/android/sso/Response.java b/src/main/java/com/nextcloud/android/sso/Response.java new file mode 100644 index 0000000000..138dcaac3f --- /dev/null +++ b/src/main/java/com/nextcloud/android/sso/Response.java @@ -0,0 +1,65 @@ +/* + * + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * Copyright (C) 2019 Tobias Kaminsky + * Copyright (C) 2019 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.android.sso; + +import com.google.gson.Gson; + +import org.apache.commons.httpclient.Header; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; + +public class Response { + @Getter + private InputStream body; + private Header[] headers; + + public Response() { + headers = new Header[0]; + body = new InputStream() { + @Override + public int read() { + return 0; + } + }; + } + + public Response(InputStream inputStream, Header[] headers) { + this.body = inputStream; + this.headers = headers; + } + + public String getPlainHeadersString() { + List arrayList = new ArrayList<>(headers.length); + + for (Header header : headers) { + arrayList.add(new PlainHeader(header.getName(), header.getValue())); + } + + Gson gson = new Gson(); + return gson.toJson(arrayList); + } +}