Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request processing refactor #542

Merged
merged 3 commits into from
Jan 29, 2021
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,17 @@ import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import com.chuckerteam.chucker.internal.support.CacheDirectoryProvider
import com.chuckerteam.chucker.internal.support.DepletingSource
import com.chuckerteam.chucker.internal.support.FileFactory
import com.chuckerteam.chucker.internal.support.IOUtils
import com.chuckerteam.chucker.internal.support.Logger
import com.chuckerteam.chucker.internal.support.ReportingSink
import com.chuckerteam.chucker.internal.support.RequestProcessor
import com.chuckerteam.chucker.internal.support.TeeSource
import com.chuckerteam.chucker.internal.support.contentType
import com.chuckerteam.chucker.internal.support.hasBody
import com.chuckerteam.chucker.internal.support.hasSupportedContentEncoding
import com.chuckerteam.chucker.internal.support.isGzipped
import com.chuckerteam.chucker.internal.support.isProbablyPlainText
import com.chuckerteam.chucker.internal.support.uncompress
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.asResponseBody
import okio.Buffer
Expand Down Expand Up @@ -53,8 +51,8 @@ public class ChuckerInterceptor private constructor(
private val maxContentLength = builder.maxContentLength
private val cacheDirectoryProvider = builder.cacheDirectoryProvider ?: CacheDirectoryProvider { context.filesDir }
private val alwaysReadResponseBody = builder.alwaysReadResponseBody
private val io = IOUtils(builder.context)
private val headersToRedact = builder.headersToRedact.toMutableSet()
private val requestProcessor = RequestProcessor(context, collector, maxContentLength)

/** Adds [headerName] into [headersToRedact] */
public fun redactHeader(vararg headerName: String) {
Expand All @@ -63,11 +61,8 @@ public class ChuckerInterceptor private constructor(

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val transaction = HttpTransaction()

processRequest(request, transaction)
collector.onRequestSent(transaction)
val request = requestProcessor.process(chain.request(), transaction)

val response = try {
chain.proceed(request)
Expand All @@ -81,39 +76,6 @@ public class ChuckerInterceptor private constructor(
return multiCastResponseBody(response, transaction)
}

/**
* Processes a [Request] and populates corresponding fields of a [HttpTransaction].
*/
private fun processRequest(request: Request, transaction: HttpTransaction) {
val requestBody = request.body

val encodingIsSupported = request.headers.hasSupportedContentEncoding

transaction.apply {
setRequestHeaders(request.headers)
populateUrl(request.url)

isRequestBodyPlainText = encodingIsSupported
requestDate = System.currentTimeMillis()
method = request.method
requestContentType = requestBody?.contentType()?.toString()
requestPayloadSize = requestBody?.contentLength() ?: 0L
}

if (requestBody != null && encodingIsSupported) {
val source = io.getNativeSource(Buffer(), request.isGzipped)
val buffer = source.buffer
requestBody.writeTo(buffer)
val charset = requestBody.contentType()?.charset() ?: UTF_8
if (buffer.isProbablyPlainText) {
val content = io.readFromBuffer(buffer, charset, maxContentLength)
transaction.requestBody = content
} else {
transaction.isRequestBodyPlainText = false
}
}
}

/**
* Processes [Response] metadata and populates corresponding fields of a [HttpTransaction].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ internal class HttpTransaction(
@ColumnInfo(name = "requestContentType") var requestContentType: String?,
@ColumnInfo(name = "requestHeaders") var requestHeaders: String?,
@ColumnInfo(name = "requestBody") var requestBody: String?,
@ColumnInfo(name = "isRequestBodyPlainText") var isRequestBodyPlainText: Boolean = true,
@ColumnInfo(name = "isRequestBodyPlainText") var isRequestBodyPlainText: Boolean = false,
@ColumnInfo(name = "responseCode") var responseCode: Int?,
@ColumnInfo(name = "responseMessage") var responseMessage: String?,
@ColumnInfo(name = "error") var error: String?,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.chuckerteam.chucker.internal.support

import okio.Buffer
import okio.ForwardingSource
import okio.Source

internal class LimitingSource(
delegate: Source,
private val bytesCountThreshold: Long,
) : ForwardingSource(delegate) {
private var bytesRead = 0L
val isThresholdReached get() = bytesRead >= bytesCountThreshold

override fun read(sink: Buffer, byteCount: Long) = if (!isThresholdReached) {
super.read(sink, byteCount).also { bytesRead += it }
} else {
-1L
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.chuckerteam.chucker.internal.support

import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import okio.Source
import okio.gzip
Expand Down Expand Up @@ -47,18 +46,6 @@ internal val Response.contentType: String?
return this.header("Content-Type")
}

/** Checks if the OkHttp response uses gzip encoding. */
internal val Response.isGzipped: Boolean
get() {
return this.headers.containsGzip
}

/** Checks if the OkHttp request uses gzip encoding. */
internal val Request.isGzipped: Boolean
get() {
return this.headers.containsGzip
}

private val Headers.containsGzip: Boolean
get() {
return this["Content-Encoding"].equals("gzip", ignoreCase = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.chuckerteam.chucker.internal.support

import android.content.Context
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import okhttp3.Request
import okio.Buffer
import okio.IOException
import kotlin.text.Charsets.UTF_8

internal class RequestProcessor(
private val context: Context,
private val collector: ChuckerCollector,
private val maxContentLength: Long,
) {
fun process(request: Request, transaction: HttpTransaction): Request {
processMetadata(request, transaction)
processBody(request, transaction)
collector.onRequestSent(transaction)
return request
}

private fun processMetadata(request: Request, transaction: HttpTransaction) {
transaction.apply {
setRequestHeaders(request.headers)
populateUrl(request.url)

requestDate = System.currentTimeMillis()
method = request.method
requestContentType = request.body?.contentType()?.toString()
requestPayloadSize = request.body?.contentLength()
}
}

private fun processBody(request: Request, transaction: HttpTransaction) {
val body = request.body ?: return

val isEncodingSupported = request.headers.hasSupportedContentEncoding
if (!isEncodingSupported) {
return
}

val limitingSource = try {
Buffer().apply { body.writeTo(this) }
} catch (e: IOException) {
Logger.error("Failed to read request payload", e)
return
}.uncompress(request.headers).let { LimitingSource(it, maxContentLength) }

val contentBuffer = Buffer().apply { limitingSource.use { writeAll(it) } }
if (!contentBuffer.isProbablyPlainText) {
return
}

transaction.isRequestBodyPlainText = true
try {
transaction.requestBody = contentBuffer.readString(body.contentType()?.charset() ?: UTF_8)
} catch (e: IOException) {
Logger.error("Failed to process request payload", e)
}
if (limitingSource.isThresholdReached) {
transaction.requestBody += context.getString(R.string.chucker_body_content_truncated)
}
}
}
1 change: 0 additions & 1 deletion library/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
<string name="chucker_body_empty">(body is empty)</string>
<string name="chucker_body_omitted">(encoded or binary body omitted)</string>
<string name="chucker_search">Search</string>
<string name="chucker_body_unexpected_eof">\n\n--- Unexpected end of content ---</string>
<string name="chucker_body_content_truncated">\n\n--- Content truncated ---</string>
<string name="chucker_tab_network">Network</string>
<string name="chucker_tab_errors">Errors</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal class ChuckerInterceptorDelegate(
private val transactions = CopyOnWriteArrayList<HttpTransaction>()

private val mockContext = mockk<Context> {
every { getString(any()) } returns ""
every { getString(R.string.chucker_body_content_truncated) } returns "\n\n--- Content truncated ---"
}
private val mockCollector = mockk<ChuckerCollector> {
every { onRequestSent(any()) } returns Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import okio.Buffer
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
import okio.GzipSink
import okio.buffer
import org.junit.Rule
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
Expand Down Expand Up @@ -421,5 +422,79 @@ internal class ChuckerInterceptorTest {
assertThat(transaction.isRequestBodyPlainText).isFalse()
}

@ParameterizedTest
@EnumSource(value = ClientFactory::class)
fun requestBody_isAvailableToServer(factory: ClientFactory) {
server.enqueue(MockResponse())
val client = factory.create(chuckerInterceptor)

val request = "Hello, world!".toRequestBody().toServerRequest()
client.newCall(request).execute().readByteStringBody()
val serverRequestContent = server.takeRequest().body.readByteString()

assertThat(serverRequestContent.utf8()).isEqualTo("Hello, world!")
}

@ParameterizedTest
@EnumSource(value = ClientFactory::class)
fun plainTextRequestBody_isAvailableToChucker(factory: ClientFactory) {
server.enqueue(MockResponse())
val client = factory.create(chuckerInterceptor)

val request = "Hello, world!".toRequestBody().toServerRequest()
client.newCall(request).execute().readByteStringBody()

val transaction = chuckerInterceptor.expectTransaction()
assertThat(transaction.isRequestBodyPlainText).isTrue()
assertThat(transaction.requestBody).isEqualTo("Hello, world!")
assertThat(transaction.requestPayloadSize).isEqualTo(request.body!!.contentLength())
}

@ParameterizedTest
@EnumSource(value = ClientFactory::class)
fun gzippedRequestBody_isGunzippedForChucker(factory: ClientFactory) {
server.enqueue(MockResponse())
val client = factory.create(chuckerInterceptor)

val gzippedBytes = Buffer().apply {
GzipSink(this).buffer().use { sink -> sink.writeUtf8("Hello, world!") }
}.readByteString()
val request = gzippedBytes.toRequestBody().toServerRequest()
.newBuilder()
.header("Content-Encoding", "gzip")
.build()
client.newCall(request).execute().readByteStringBody()

val transaction = chuckerInterceptor.expectTransaction()
assertThat(transaction.isRequestBodyPlainText).isTrue()
assertThat(transaction.requestBody).isEqualTo("Hello, world!")
assertThat(transaction.requestPayloadSize).isEqualTo(request.body!!.contentLength())
}

@ParameterizedTest
@EnumSource(value = ClientFactory::class)
fun requestBody_isTruncatedToMaxContentLength(factory: ClientFactory) {
server.enqueue(MockResponse())
val chuckerInterceptor = ChuckerInterceptorDelegate(
maxContentLength = SEGMENT_SIZE,
cacheDirectoryProvider = { tempDir },
)
val client = factory.create(chuckerInterceptor)

val request = "!".repeat(SEGMENT_SIZE.toInt() * 10).toRequestBody().toServerRequest()
client.newCall(request).execute().readByteStringBody()

val transaction = chuckerInterceptor.expectTransaction()
assertThat(transaction.isRequestBodyPlainText).isTrue()
assertThat(transaction.requestBody).isEqualTo(
"""
${"!".repeat(SEGMENT_SIZE.toInt())}

--- Content truncated ---
""".trimIndent()
)
assertThat(transaction.requestPayloadSize).isEqualTo(request.body!!.contentLength())
}

private fun RequestBody.toServerRequest() = Request.Builder().url(serverUrl).post(this).build()
}
Loading