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 index 7cf6ef28..c85f4fd5 100644 --- 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 @@ -29,8 +29,9 @@ object LogBuilder { fun logHttp( networkPetition: NetworkPetition, networkNode: NetworkNode, + requestId: String? = "", description: String - ) = Timber.d("[Network, $networkPetition] [$networkNode] $description") + ) = Timber.d("[Network, $networkPetition] [$networkNode] [$requestId] $description") } enum class NetworkPetition { 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 index 9aad57a2..0877deca 100644 --- 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 @@ -23,37 +23,145 @@ */ package com.owncloud.android.lib.common.http +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 { - val response = chain.proceed(chain.request()) + 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 { - if (httpLogsEnabled) { - // Log request - logHttp(REQUEST, INFO, "Type: ${it.request.method} URL: ${it.request.url}") - it.request.headers.forEach { header -> logHttp(REQUEST, HEADER, header.toString()) } - logHttp(REQUEST, BODY, it.request.body.toString()) - - // Log response - logHttp(RESPONSE, INFO, "Code: ${it.code} Message: ${it.message} IsSuccessful: ${it.isSuccessful}") - it.headers.forEach { header -> logHttp(RESPONSE, HEADER, header.toString()) } - logHttp(RESPONSE, BODY, it.body.toString()) - } + 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 -> + logHttp(networkPetition, HEADER, requestId, "${header.first}: ${header.second}") + } + } + + 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 (buffer.isProbablyUtf8()) { + 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 request") + + val source = responseBody.source() + source.request(LIMIT_BODY_LOG) + val buffer = source.buffer + + if (!buffer.isProbablyUtf8()) { + logHttp( + REQUEST, + BODY, + requestId, + "<-- Body end for request -- Binary -- Omitted: ${responseBody.contentLength()} bytes" + ) + } + + if (responseBody.contentLength() < LIMIT_BODY_LOG) { + logHttp(REQUEST, BODY, requestId, buffer.clone().readString(charset)) + } else { + logHttp(REQUEST, BODY, requestId, buffer.clone().readString(LIMIT_BODY_LOG, charset)) + } + logHttp( + REQUEST, + BODY, + requestId, + "<-- Body end for request -- Omitted: ${max(0, responseBody.contentLength() - LIMIT_BODY_LOG)} 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/http/utf8.kt b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/utf8.kt new file mode 100644 index 00000000..9591e425 --- /dev/null +++ b/owncloudComLibrary/src/main/java/com/owncloud/android/lib/common/http/utf8.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.owncloud.android.lib.common.http + +import java.io.EOFException +import okio.Buffer + +/** + * Returns true if the body in question probably contains human readable text. Uses a small + * sample of code points to detect unicode control characters commonly used in binary file + * signatures. + */ +internal fun Buffer.isProbablyUtf8(): Boolean { + try { + val prefix = Buffer() + val byteCount = size.coerceAtMost(64) + copyTo(prefix, 0, byteCount) + for (i in 0 until 16) { + if (prefix.exhausted()) { + break + } + val codePoint = prefix.readUtf8CodePoint() + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false + } + } + return true + } catch (_: EOFException) { + return false // Truncated UTF-8 sequence. + } +}