Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions library/src/main/java/com/nextcloud/common/NextcloudClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.owncloud.android.lib.common.OwnCloudClientFactory.DEFAULT_CONNECTION_
import com.owncloud.android.lib.common.OwnCloudClientFactory.DEFAULT_DATA_TIMEOUT_LONG
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
import com.owncloud.android.lib.common.accounts.AccountUtils
import com.owncloud.android.lib.common.interceptor.ClientInterceptor
import com.owncloud.android.lib.common.network.AdvancedX509KeyManager
import com.owncloud.android.lib.common.network.AdvancedX509TrustManager
import com.owncloud.android.lib.common.network.NetworkUtils
Expand All @@ -43,6 +44,7 @@ class NextcloudClient private constructor(
val context: Context
) : NextcloudUriProvider by delegate {
var followRedirects = true
private val interceptor = ClientInterceptor()

constructor(
baseUri: Uri,
Expand Down Expand Up @@ -126,6 +128,7 @@ class NextcloudClient private constructor(

@Throws(IOException::class)
fun execute(method: OkHttpMethodBase): Int {
interceptor.interceptOkHttpMethodBaseRequest(method)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tobiasKaminsky

How can I completely bypass the interception process? Currently, LogOC not going to print anything in prod, but I would also like to skip the parsing step altogether. When I checked the isEnabled value in LogOC, it returned false during my local tests, even though it should be enabled.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interceptor.interceptOkHttpMethodBaseRequest(method)
if (BuildConfig.DEBUG) {
interceptor.interceptOkHttpMethodBaseRequest(method)
}

Maybe something like that?

Copy link
Contributor Author

@alperozturk96 alperozturk96 Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some cases (such as beta or QA versions), even in release mode, we want to track logs to detect issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems Log_OC inside libray is not started.
I will check it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea: move files' logger to library logging

val httpStatus = method.execute(this)
if (httpStatus == HttpStatus.SC_BAD_REQUEST) {
val uri = method.uri
Expand All @@ -137,7 +140,10 @@ class NextcloudClient private constructor(

internal fun execute(request: Request): ResponseOrError =
try {
interceptor.interceptOkHttp3Request(request)
val response = client.newCall(request).execute()
interceptor.interceptOkHttp3Response(response)

if (response.code == HttpStatus.SC_BAD_REQUEST) {
val url = request.url
Log_OC.e(TAG, "Received http status 400 for $url -> removing client certificate")
Expand All @@ -150,6 +156,7 @@ class NextcloudClient private constructor(

@Throws(IOException::class)
fun followRedirection(method: OkHttpMethodBase): RedirectionPath {
interceptor.interceptOkHttpMethodBaseRequest(method)
var redirectionsCount = 0
var status = method.getStatusCode()
val result = RedirectionPath(status, OwnCloudClient.MAX_REDIRECTIONS_COUNT)
Expand Down Expand Up @@ -179,6 +186,7 @@ class NextcloudClient private constructor(
}

status = method.execute(this)
interceptor.interceptOkHttpMethodBaseResponse(method, status)
result.addStatus(status)
redirectionsCount++
} else {
Expand Down
12 changes: 11 additions & 1 deletion library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ abstract class OkHttpMethodBase(

private var response: Response? = null
private var queryMap: Map<String, String> = HashMap()
private val requestHeaders: MutableMap<String, String> = HashMap()
val requestHeaders: MutableMap<String, String> = HashMap()
private val requestBuilder: Request.Builder = Request.Builder()
private var request: Request? = null

Expand Down Expand Up @@ -152,6 +152,16 @@ abstract class OkHttpMethodBase(
return response?.code ?: UNKNOWN_STATUS_CODE
}

fun getRequestBodyAsString(): String =
try {
val copy = request?.newBuilder()?.build()
val buffer = okio.Buffer()
copy?.body?.writeTo(buffer)
buffer.readUtf8()
} catch (_: Exception) {
""
}

abstract fun applyType(temp: Request.Builder)

fun isSuccess(): Boolean = getStatusCode() == HttpURLConnection.HTTP_OK
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.nextcloud.common.DNSCache;
import com.nextcloud.common.NextcloudUriDelegate;
import com.owncloud.android.lib.common.accounts.AccountUtils;
import com.owncloud.android.lib.common.interceptor.ClientInterceptor;
import com.owncloud.android.lib.common.network.AdvancedX509KeyManager;
import com.owncloud.android.lib.common.network.RedirectionPath;
import com.owncloud.android.lib.common.utils.Log_OC;
Expand Down Expand Up @@ -66,6 +67,7 @@ public class OwnCloudClient extends HttpClient {
private int mInstanceNumber;

private AdvancedX509KeyManager keyManager;
private final ClientInterceptor interceptor = new ClientInterceptor();

/**
* Constructor
Expand Down Expand Up @@ -93,7 +95,6 @@ public OwnCloudClient(Uri baseUri, HttpConnectionManager connectionMgr, Context
getParams().setParameter(PARAM_SINGLE_COOKIE_HEADER, PARAM_SINGLE_COOKIE_HEADER_VALUE);

applyProxySettings();

clearCredentials();
}

Expand Down Expand Up @@ -141,6 +142,7 @@ public void clearCredentials() {
* @param connectionTimeout Timeout to set for connection establishment
*/
public int executeMethod(HttpMethodBase method, int readTimeout, int connectionTimeout) throws IOException {
interceptor.interceptHttpMethodBaseRequest(method);

int oldSoTimeout = getParams().getSoTimeout();
int oldConnectionTimeout = getHttpConnectionManager().getParams().getConnectionTimeout();
Expand All @@ -158,6 +160,9 @@ public int executeMethod(HttpMethodBase method, int readTimeout, int connectionT
Log_OC.e(TAG, "Received http status 400 for " + uri + " -> removing client certificate");
keyManager.removeKeys(uri);
}

interceptor.interceptHttpMethodBaseResponse(method, httpStatus);

return httpStatus;
} finally {
getParams().setSoTimeout(oldSoTimeout);
Expand All @@ -175,6 +180,7 @@ public int executeMethod(HttpMethodBase method, int readTimeout, int connectionT
*/
@Override
public int executeMethod(HttpMethod method) throws IOException {
interceptor.interceptHttpMethodRequest(method);
final String hostname = method.getURI().getHost();

try {
Expand Down Expand Up @@ -207,6 +213,7 @@ public int executeMethod(HttpMethod method) throws IOException {
// logCookiesAtState("after");
// logSetCookiesAtResponse(method.getResponseHeaders());

interceptor.interceptHttpMethodResponse(method, status);
return status;

} catch (SocketTimeoutException | ConnectException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* Nextcloud Android Library
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: MIT
*/

package com.owncloud.android.lib.common.interceptor

import com.nextcloud.common.OkHttpMethodBase
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.common.utils.responseFormat.ResponseFormat
import com.owncloud.android.lib.common.utils.responseFormat.ResponseFormatDetector
import okhttp3.Request
import okhttp3.Response
import org.apache.commons.httpclient.HttpMethod
import org.apache.commons.httpclient.HttpMethodBase
import org.json.JSONArray
import org.json.JSONObject
import org.xml.sax.InputSource
import java.io.StringReader
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.Transformer
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.collections.component1
import kotlin.collections.component2

@Suppress("TooManyFunctions")
class ClientInterceptor {
companion object {
private const val TAG = "ClientInterceptor"
}

fun interceptHttpMethodBaseRequest(method: HttpMethodBase) {
Log_OC.d(TAG, "➡️ Method: ${method.name} 🌐 URL: ${method.uri}")
logHeaders(method.requestHeaders.map { it.name to it.value })

if (method is org.apache.commons.httpclient.methods.EntityEnclosingMethod) {
val buffer = java.io.ByteArrayOutputStream()
method.requestEntity?.writeRequest(buffer)
val body = buffer.toString(method.requestCharSet ?: Charsets.UTF_8.name())
logBody(body, method.getRequestHeader("Content-Type")?.value, "Request")
}
Log_OC.d(TAG, "-------------------------")
}

fun interceptHttpMethodBaseResponse(
method: HttpMethodBase,
statusCode: Int
) {
Log_OC.d(TAG, "⬅️ Status Code: $statusCode")
logHeaders(method.responseHeaders.map { it.name to it.value })
logBody(method.responseBodyAsString, method.getResponseHeader("Content-Type")?.value, "Response")
Log_OC.d(TAG, "-------------------------")
}

fun interceptHttpMethodRequest(method: HttpMethod) {
Log_OC.d(TAG, "➡️ Method: ${method.name} 🌐 URL: ${method.uri}")
logHeaders(method.requestHeaders.map { it.name to it.value })

if (method is org.apache.commons.httpclient.methods.EntityEnclosingMethod) {
val buffer = java.io.ByteArrayOutputStream()
method.requestEntity?.writeRequest(buffer)
val body = buffer.toString(method.requestCharSet ?: Charsets.UTF_8.name())
logBody(body, method.getRequestHeader("Content-Type")?.value, "Request")
}
Log_OC.d(TAG, "-------------------------")
}

fun interceptHttpMethodResponse(
method: HttpMethod,
statusCode: Int
) {
Log_OC.d(TAG, "⬅️ Status Code: $statusCode")
logHeaders(method.responseHeaders.map { it.name to it.value })
logBody(method.responseBodyAsString, method.getResponseHeader("Content-Type")?.value, "Response")
Log_OC.d(TAG, "-------------------------")
}

fun interceptOkHttp3Request(request: Request) {
Log_OC.d(TAG, "➡️ Method: ${request.method} 🌐 URL: ${request.url}")
request.headers?.toMultimap()?.let { headerMap ->
logHeaders(headerMap.flatMap { (k, vList) -> vList.map { k to it } })
}
request.body?.let {
val buffer = okio.Buffer()
it.writeTo(buffer)
logBody(buffer.readUtf8(), it.contentType()?.toString(), "Request")
}
Log_OC.d(TAG, "-------------------------")
}

fun interceptOkHttp3Response(response: Response) {
Log_OC.d(TAG, "⬅️ Status: ${response.code}")
response.headers?.toMultimap()?.let { headerMap ->
logHeaders(headerMap.flatMap { (k, vList) -> vList.map { k to it } })
}

val body =
try {
response.peekBody(Long.MAX_VALUE)
} catch (_: Exception) {
null
}

body?.string()?.let { bodyString ->
logBody(bodyString, response.body?.contentType()?.toString(), "Response")
}

Log_OC.d(TAG, "-------------------------")
}

fun interceptOkHttpMethodBaseRequest(method: OkHttpMethodBase) {
Log_OC.d(TAG, "➡️ Method: ${method.javaClass.simpleName} 🌐 URL: ${method.uri}")
logHeaders(method.requestHeaders.map { it.key to it.value })
logBody(method.getRequestBodyAsString(), method.getRequestHeader("Content-Type"), "Request")
Log_OC.d(TAG, "-------------------------")
}

fun interceptOkHttpMethodBaseResponse(
method: OkHttpMethodBase,
statusCode: Int
) {
Log_OC.d(TAG, "⬅️ Status Code: $statusCode")
logHeaders(method.getResponseHeaders().toMultimap().flatMap { (k, vList) -> vList.map { k to it } })
logBody(method.getResponseBodyAsString(), method.getResponseHeader("Content-Type"), "Response")
Log_OC.d(TAG, "-------------------------")
}

// region Private Methods
private val xmlDocBuilder =
DocumentBuilderFactory
.newInstance()
.apply {
isNamespaceAware = true
isIgnoringComments = true
isIgnoringElementContentWhitespace = true
}.newDocumentBuilder()

private val threadLocalTransformer = ThreadLocal<Transformer>()

private fun getTransformer(): Transformer {
var transformer = threadLocalTransformer.get()
if (transformer == null) {
transformer =
TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
}
threadLocalTransformer.set(transformer)
}
return transformer
}

private fun formatXml(xml: String): String =
try {
val characterStream = StringReader(xml)
val inputSource = InputSource(characterStream)
val doc = xmlDocBuilder.parse(inputSource)
val writer = StringWriter()
val domSource = DOMSource(doc)
val streamResult = StreamResult(writer)
getTransformer().transform(domSource, streamResult)
writer.toString()
} catch (_: Exception) {
xml
}

private fun formatJson(
json: String,
indent: Int = 2
): String =
try {
val trimmed = json.trim()
when {
ResponseFormatDetector.isJsonObject(trimmed) -> JSONObject(trimmed).toString(indent)
ResponseFormatDetector.isJsonArray(trimmed) -> JSONArray(trimmed).toString(indent)
else -> json
}
} catch (_: Exception) {
json
}

private fun formatBody(
body: String,
contentType: String
): String {
val bodyFormat = ResponseFormatDetector.detectFormat(body)

return when {
contentType.contains("xml", true) || bodyFormat == ResponseFormat.XML -> formatXml(body)
contentType.contains("json", true) || bodyFormat == ResponseFormat.JSON -> formatJson(body)
else -> body
}
}

private fun isValidContentType(contentType: String): Boolean =
contentType.contains("application/json") ||
contentType.contains("text") ||
contentType.contains("xml") ||
contentType.isEmpty()

private fun logHeaders(headers: Iterable<Pair<String, String>>) {
headers.forEach { (name, value) -> Log_OC.d(TAG, "📑 Header: $name: $value") }
}

@Suppress("TooGenericExceptionCaught")
private fun logBody(
body: String?,
contentType: String?,
label: String
) {
if (!body.isNullOrBlank() && isValidContentType(contentType ?: "")) {
try {
val formatted = formatBody(body, contentType ?: "")
Log_OC.d(TAG, "📦 $label Body:\n$formatted")
} catch (e: Exception) {
Log_OC.w(TAG, "⚠️ Error reading $label body: $e")
}
}
}
// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public interface Adapter {
void wtf(String tag, String message);
}

public static boolean isEnabled() {
return isEnabled;
}

/**
* This is legacy logger implementation extracted to allow
* the code to compile and run without hiccup.
Expand Down
Loading
Loading