diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java index 12571a88..06e8056d 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpClient.java @@ -57,6 +57,7 @@ public class HttpClient { private static OkHttpClient sOkHttpClient; private static Context sContext; private static HashMap> sCookieStore = new HashMap<>(); + private static LogInterceptor sLogInterceptor; public static OkHttpClient getOkHttpClient() { if (sOkHttpClient == null) { @@ -110,6 +111,7 @@ public class HttpClient { }; OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() + .addNetworkInterceptor(getLogInterceptor()) .protocols(Arrays.asList(Protocol.HTTP_1_1)) .readTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) .writeTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) @@ -120,6 +122,7 @@ public class HttpClient { .cookieJar(cookieJar); // TODO: Not verifying the hostname against certificate. ask owncloud security human if this is ok. //.hostnameVerifier(new BrowserCompatHostnameVerifier()); + sOkHttpClient = clientBuilder.build(); } catch (Exception e) { @@ -137,6 +140,13 @@ public class HttpClient { sContext = context; } + public static LogInterceptor getLogInterceptor() { + if (sLogInterceptor == null) { + sLogInterceptor = new LogInterceptor(); + } + return sLogInterceptor; + } + public List getCookiesFromUrl(HttpUrl httpUrl) { return sCookieStore.get(httpUrl.host()); } diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java index 3850cd3f..b86534a3 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/HttpConstants.java @@ -51,6 +51,14 @@ public class HttpConstants { public static final String ACCEPT_ENCODING_IDENTITY = "identity"; public static final String OC_FILE_REMOTE_ID = "OC-FileId"; + /*********************************************************************************************************** + ************************************************ CONTENT TYPES ******************************************** + ***********************************************************************************************************/ + + public static final String CONTENT_TYPE_XML = "application/xml"; + public static final String CONTENT_TYPE_JSON = "application/json"; + public static final String CONTENT_TYPE_WWW_FORM = "application/x-www-form-urlencoded"; + /*********************************************************************************************************** ************************************************ STATUS CODES ********************************************* ***********************************************************************************************************/ diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/LogBuilder.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/LogBuilder.kt new file mode 100644 index 00000000..c0ca4fb0 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/LogBuilder.kt @@ -0,0 +1,65 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2020 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.owncloud.android.lib.common.http + +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_JSON +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_WWW_FORM +import com.owncloud.android.lib.common.http.HttpConstants.CONTENT_TYPE_XML +import okhttp3.MediaType +import timber.log.Timber +import java.util.Locale + +object LogBuilder { + fun logHttp( + networkPetition: NetworkPetition, + networkNode: NetworkNode, + requestId: String? = "", + description: String + ) = Timber.d("[Network, $networkPetition] [$networkNode] [$requestId] $description") +} + +enum class NetworkPetition { + REQUEST, RESPONSE; + + override fun toString(): String = super.toString().toLowerCase(Locale.ROOT) +} + +enum class NetworkNode { + INFO, HEADER, BODY; + + override fun toString(): String = super.toString().toLowerCase(Locale.ROOT) +} + +/** + * Check whether a media type is loggable. + * + * @return true if its type is text, xml, json, or x-www-form-urlencoded. + */ +fun MediaType?.isLoggable(): Boolean = + this?.let { mediaType -> + val mediaTypeString = mediaType.toString() + (mediaType.type == "text" || + mediaTypeString.contains(CONTENT_TYPE_XML) || + mediaTypeString.contains(CONTENT_TYPE_JSON) || + mediaTypeString.contains(CONTENT_TYPE_WWW_FORM)) + } ?: false diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/LogInterceptor.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/LogInterceptor.kt new file mode 100644 index 00000000..0ca90a47 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/LogInterceptor.kt @@ -0,0 +1,179 @@ +/* ownCloud Android Library is available under MIT license + * Copyright (C) 2020 ownCloud GmbH. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +package com.owncloud.android.lib.common.http + +import com.owncloud.android.lib.common.http.HttpConstants.AUTHORIZATION_HEADER +import com.owncloud.android.lib.common.http.HttpConstants.OC_X_REQUEST_ID +import com.owncloud.android.lib.common.http.LogBuilder.logHttp +import com.owncloud.android.lib.common.http.NetworkNode.BODY +import com.owncloud.android.lib.common.http.NetworkNode.HEADER +import com.owncloud.android.lib.common.http.NetworkNode.INFO +import com.owncloud.android.lib.common.http.NetworkPetition.REQUEST +import com.owncloud.android.lib.common.http.NetworkPetition.RESPONSE +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.ResponseBody +import okio.Buffer +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import kotlin.math.max + +class LogInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + + if (!httpLogsEnabled) { + return chain.proceed(chain.request()) + } + + val request = chain.request().also { + val requestId = it.headers[OC_X_REQUEST_ID] + logHttp(REQUEST, INFO, requestId, "Type: ${it.method} URL: ${it.url}") + logHeaders(requestId, it.headers, REQUEST) + logRequestBody(requestId, it.body) + } + + val response = chain.proceed(request) + + return response.also { + val requestId = it.request.headers[OC_X_REQUEST_ID] + logHttp( + RESPONSE, + INFO, + requestId, + "Code: ${it.code} Message: ${it.message} IsSuccessful: ${it.isSuccessful}" + ) + logHeaders(requestId, it.headers, RESPONSE) + logResponseBody(requestId, it.body) + } + } + + private fun logHeaders(requestId: String?, headers: Headers, networkPetition: NetworkPetition) { + headers.forEach { header -> + val headerValue: String = if (header.first.equals(AUTHORIZATION_HEADER, true)) { + "[redacted]" + } else { + header.second + } + logHttp(networkPetition, HEADER, requestId, "${header.first}: $headerValue") + } + } + + private fun logRequestBody(requestId: String?, requestBodyParam: RequestBody?) { + requestBodyParam?.let { requestBody -> + + if (requestBody.isOneShot()) { + logHttp(REQUEST, BODY, requestId, "One shot body -- Omitted") + return@let + } + + if (requestBody.isDuplex()) { + logHttp(REQUEST, BODY, requestId, "Duplex body -- Omitted") + return@let + } + + val buffer = Buffer() + requestBody.writeTo(buffer) + + val contentType = requestBody.contentType() + val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 + + logHttp(REQUEST, BODY, requestId, "Length: ${requestBody.contentLength()} byte body") + logHttp(REQUEST, BODY, requestId, "Type: ${requestBody.contentType()}") + logHttp(REQUEST, BODY, requestId, "--> Body start for request") + + if (contentType.isLoggable()) { + if (requestBody.contentLength() < LIMIT_BODY_LOG) { + logHttp(REQUEST, BODY, requestId, buffer.readString(charset)) + } else { + logHttp(REQUEST, BODY, requestId, buffer.readString(LIMIT_BODY_LOG, charset)) + } + logHttp( + REQUEST, + BODY, + requestId, + "<-- Body end for request -- Omitted: ${max(0, requestBody.contentLength() - LIMIT_BODY_LOG)} bytes" + ) + } else { + logHttp( + REQUEST, + BODY, + requestId, + "<-- Body end for request -- Binary -- Omitted: ${requestBody.contentLength()} bytes" + ) + } + + } ?: logHttp(REQUEST, BODY, requestId, "Empty body") + } + + private fun logResponseBody(requestId: String?, responseBodyParam: ResponseBody?) { + responseBodyParam?.let { responseBody -> + + val contentType = responseBody.contentType() + val charset: Charset = contentType?.charset(StandardCharsets.UTF_8) ?: StandardCharsets.UTF_8 + + logHttp(RESPONSE, BODY, requestId, "Length: ${responseBody.contentLength()} byte body") + logHttp(RESPONSE, BODY, requestId, "Type: ${responseBody.contentType()}") + logHttp(RESPONSE, BODY, requestId, "--> Body start for response") + + val source = responseBody.source() + source.request(LIMIT_BODY_LOG) + val buffer = source.buffer + + if (contentType.isLoggable()) { + + if (responseBody.contentLength() < LIMIT_BODY_LOG) { + logHttp(RESPONSE, BODY, requestId, buffer.clone().readString(charset)) + } else { + logHttp(RESPONSE, BODY, requestId, buffer.clone().readString(LIMIT_BODY_LOG, charset)) + } + logHttp( + RESPONSE, + BODY, + requestId, + "<-- Body end for response -- Omitted: ${ + max( + 0, + responseBody.contentLength() - LIMIT_BODY_LOG + ) + } bytes" + ) + } else { + logHttp( + RESPONSE, + BODY, + requestId, + "<-- Body end for response -- Binary -- Omitted: ${responseBody.contentLength()} bytes" + ) + } + } ?: logHttp(RESPONSE, BODY, requestId, "Empty body") + } + + companion object { + var httpLogsEnabled: Boolean = false + private const val LIMIT_BODY_LOG: Long = 1024 + } +} diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.java index 53abaf9a..40f168ea 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/ChunkFromFileRequestBody.java @@ -84,18 +84,12 @@ public class ChunkFromFileRequestBody extends FileRequestBody { long maxCount = Math.min(mOffset + mChunkSize, mChannel.size()); while (mChannel.position() < maxCount) { - Timber.v("Sink buffer size: %s", sink.buffer().size()); - readCount = mChannel.read(mBuffer); - Timber.v("Read " + readCount + " bytes from file channel to " + mBuffer.toString()); - - sink.buffer().write(mBuffer.array(), 0, readCount); + sink.getBuffer().write(mBuffer.array(), 0, readCount); sink.flush(); - Timber.v("Write " + readCount + " bytes to sink buffer with size " + sink.buffer().size()); - mBuffer.clear(); if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks mTransferred += readCount; diff --git a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/FileRequestBody.java b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/FileRequestBody.java index 2df9884d..18746ba7 100644 --- a/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/FileRequestBody.java +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/network/FileRequestBody.java @@ -53,6 +53,11 @@ public class FileRequestBody extends RequestBody implements ProgressiveDataTrans mContentType = contentType; } + @Override + public boolean isOneShot() { + return true; + } + @Override public MediaType contentType() { return mContentType;