1
0
mirror of https://github.com/owncloud/android-library.git synced 2025-06-07 16:06:08 +00:00

Merge pull request #345 from owncloud/add_network_logs

Log network calls
This commit is contained in:
Abel García de Prada 2020-10-28 13:18:18 +01:00 committed by GitHub
commit 61937825b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 268 additions and 7 deletions

View File

@ -57,6 +57,7 @@ public class HttpClient {
private static OkHttpClient sOkHttpClient; private static OkHttpClient sOkHttpClient;
private static Context sContext; private static Context sContext;
private static HashMap<String, List<Cookie>> sCookieStore = new HashMap<>(); private static HashMap<String, List<Cookie>> sCookieStore = new HashMap<>();
private static LogInterceptor sLogInterceptor;
public static OkHttpClient getOkHttpClient() { public static OkHttpClient getOkHttpClient() {
if (sOkHttpClient == null) { if (sOkHttpClient == null) {
@ -110,6 +111,7 @@ public class HttpClient {
}; };
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder() OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
.addNetworkInterceptor(getLogInterceptor())
.protocols(Arrays.asList(Protocol.HTTP_1_1)) .protocols(Arrays.asList(Protocol.HTTP_1_1))
.readTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) .readTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS) .writeTimeout(HttpConstants.DEFAULT_DATA_TIMEOUT, TimeUnit.MILLISECONDS)
@ -120,6 +122,7 @@ public class HttpClient {
.cookieJar(cookieJar); .cookieJar(cookieJar);
// TODO: Not verifying the hostname against certificate. ask owncloud security human if this is ok. // TODO: Not verifying the hostname against certificate. ask owncloud security human if this is ok.
//.hostnameVerifier(new BrowserCompatHostnameVerifier()); //.hostnameVerifier(new BrowserCompatHostnameVerifier());
sOkHttpClient = clientBuilder.build(); sOkHttpClient = clientBuilder.build();
} catch (Exception e) { } catch (Exception e) {
@ -137,6 +140,13 @@ public class HttpClient {
sContext = context; sContext = context;
} }
public static LogInterceptor getLogInterceptor() {
if (sLogInterceptor == null) {
sLogInterceptor = new LogInterceptor();
}
return sLogInterceptor;
}
public List<Cookie> getCookiesFromUrl(HttpUrl httpUrl) { public List<Cookie> getCookiesFromUrl(HttpUrl httpUrl) {
return sCookieStore.get(httpUrl.host()); return sCookieStore.get(httpUrl.host());
} }

View File

@ -51,6 +51,14 @@ public class HttpConstants {
public static final String ACCEPT_ENCODING_IDENTITY = "identity"; public static final String ACCEPT_ENCODING_IDENTITY = "identity";
public static final String OC_FILE_REMOTE_ID = "OC-FileId"; 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 ********************************************* ************************************************ STATUS CODES *********************************************
***********************************************************************************************************/ ***********************************************************************************************************/

View File

@ -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

View File

@ -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
}
}

View File

@ -84,18 +84,12 @@ public class ChunkFromFileRequestBody extends FileRequestBody {
long maxCount = Math.min(mOffset + mChunkSize, mChannel.size()); long maxCount = Math.min(mOffset + mChunkSize, mChannel.size());
while (mChannel.position() < maxCount) { while (mChannel.position() < maxCount) {
Timber.v("Sink buffer size: %s", sink.buffer().size());
readCount = mChannel.read(mBuffer); readCount = mChannel.read(mBuffer);
Timber.v("Read " + readCount + " bytes from file channel to " + mBuffer.toString()); sink.getBuffer().write(mBuffer.array(), 0, readCount);
sink.buffer().write(mBuffer.array(), 0, readCount);
sink.flush(); sink.flush();
Timber.v("Write " + readCount + " bytes to sink buffer with size " + sink.buffer().size());
mBuffer.clear(); mBuffer.clear();
if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks
mTransferred += readCount; mTransferred += readCount;

View File

@ -53,6 +53,11 @@ public class FileRequestBody extends RequestBody implements ProgressiveDataTrans
mContentType = contentType; mContentType = contentType;
} }
@Override
public boolean isOneShot() {
return true;
}
@Override @Override
public MediaType contentType() { public MediaType contentType() {
return mContentType; return mContentType;