react-native-security-suite 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/android/build.gradle +46 -28
- package/android/src/main/AndroidManifest.xml +47 -0
- package/android/src/main/{AndroidManifestDeprecated.xml → AndroidManifestNew.xml} +1 -2
- package/android/src/main/java/com/securitysuite/NetworkLogger.java +25 -0
- package/android/src/main/java/com/securitysuite/Sslpinning.java +39 -54
- package/android/src/main/java/com/securitysuite/api/BodyDecoder.kt +35 -0
- package/android/src/main/java/com/securitysuite/api/Chucker.kt +108 -0
- package/android/src/main/java/com/securitysuite/api/ChuckerCollector.kt +129 -0
- package/android/src/main/java/com/securitysuite/api/ChuckerInterceptor.kt +280 -0
- package/android/src/main/java/com/securitysuite/api/ExportFormat.kt +12 -0
- package/android/src/main/java/com/securitysuite/api/RetentionManager.kt +109 -0
- package/android/src/main/java/com/securitysuite/internal/data/entity/HttpHeader.kt +8 -0
- package/android/src/main/java/com/securitysuite/internal/data/entity/HttpTransaction.kt +344 -0
- package/android/src/main/java/com/securitysuite/internal/data/entity/HttpTransactionTuple.kt +62 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/Har.kt +13 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/Log.kt +24 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Browser.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Creator.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Entry.kt +49 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/Page.kt +14 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Cache.kt +12 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Header.kt +17 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Request.kt +35 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Response.kt +33 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/Timings.kt +25 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/cache/SecondaryRequest.kt +13 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/PostData.kt +20 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/QueryString.kt +23 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/request/postdata/Params.kt +13 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/entry/response/Content.kt +30 -0
- package/android/src/main/java/com/securitysuite/internal/data/har/log/page/PageTimings.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/data/model/DialogData.kt +8 -0
- package/android/src/main/java/com/securitysuite/internal/data/repository/HttpTransactionDatabaseRepository.kt +60 -0
- package/android/src/main/java/com/securitysuite/internal/data/repository/HttpTransactionRepository.kt +33 -0
- package/android/src/main/java/com/securitysuite/internal/data/repository/RepositoryProvider.kt +38 -0
- package/android/src/main/java/com/securitysuite/internal/data/room/ChuckerDatabase.kt +22 -0
- package/android/src/main/java/com/securitysuite/internal/data/room/HttpTransactionDao.kt +53 -0
- package/android/src/main/java/com/securitysuite/internal/support/BitmapUtils.kt +45 -0
- package/android/src/main/java/com/securitysuite/internal/support/CacheDirectoryProvider.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/support/ChessboardDrawable.kt +76 -0
- package/android/src/main/java/com/securitysuite/internal/support/ChuckerFileProvider.kt +9 -0
- package/android/src/main/java/com/securitysuite/internal/support/ClearDatabaseJobIntentServiceReceiver.kt +14 -0
- package/android/src/main/java/com/securitysuite/internal/support/ClearDatabaseService.kt +32 -0
- package/android/src/main/java/com/securitysuite/internal/support/ContextExt.kt +22 -0
- package/android/src/main/java/com/securitysuite/internal/support/DepletingSource.kt +37 -0
- package/android/src/main/java/com/securitysuite/internal/support/FileFactory.kt +30 -0
- package/android/src/main/java/com/securitysuite/internal/support/FileSaver.kt +41 -0
- package/android/src/main/java/com/securitysuite/internal/support/FormatUtils.kt +117 -0
- package/android/src/main/java/com/securitysuite/internal/support/FormattedUrl.kt +76 -0
- package/android/src/main/java/com/securitysuite/internal/support/HarUtils.kt +28 -0
- package/android/src/main/java/com/securitysuite/internal/support/HttpUrlUtils.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/support/JsonConverter.kt +19 -0
- package/android/src/main/java/com/securitysuite/internal/support/LimitingSource.kt +22 -0
- package/android/src/main/java/com/securitysuite/internal/support/LiveDataUtils.kt +68 -0
- package/android/src/main/java/com/securitysuite/internal/support/Logger.kt +43 -0
- package/android/src/main/java/com/securitysuite/internal/support/NotificationHelper.kt +149 -0
- package/android/src/main/java/com/securitysuite/internal/support/OkHttpUtils.kt +86 -0
- package/android/src/main/java/com/securitysuite/internal/support/OkioUtils.kt +34 -0
- package/android/src/main/java/com/securitysuite/internal/support/PlainTextDecoder.kt +30 -0
- package/android/src/main/java/com/securitysuite/internal/support/ReportingSink.kt +114 -0
- package/android/src/main/java/com/securitysuite/internal/support/RequestProcessor.kt +102 -0
- package/android/src/main/java/com/securitysuite/internal/support/ResponseProcessor.kt +170 -0
- package/android/src/main/java/com/securitysuite/internal/support/SearchHighlightUtil.kt +80 -0
- package/android/src/main/java/com/securitysuite/internal/support/Sharable.kt +86 -0
- package/android/src/main/java/com/securitysuite/internal/support/SpanTextUtil.kt +202 -0
- package/android/src/main/java/com/securitysuite/internal/support/TeeSource.kt +68 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionCurlCommandSharable.kt +46 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionDetailsHarSharable.kt +11 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionDetailsSharable.kt +73 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionDiffCallback.kt +26 -0
- package/android/src/main/java/com/securitysuite/internal/support/TransactionListDetailsSharable.kt +23 -0
- package/android/src/main/java/com/securitysuite/internal/ui/BaseChuckerActivity.kt +35 -0
- package/android/src/main/java/com/securitysuite/internal/ui/MainActivity.kt +375 -0
- package/android/src/main/java/com/securitysuite/internal/ui/MainViewModel.kt +47 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/PayloadType.kt +6 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/ProtocolResources.kt +14 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionActivity.kt +186 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionAdapter.kt +139 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionOverviewFragment.kt +100 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionPagerAdapter.kt +29 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionPayloadAdapter.kt +269 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionPayloadFragment.kt +529 -0
- package/android/src/main/java/com/securitysuite/internal/ui/transaction/TransactionViewModel.kt +69 -0
- package/android/src/main/res/A.java +12 -0
- package/android/src/main/res/color/chucker_fab_background_colour.xml +5 -0
- package/android/src/main/res/drawable/chucker_empty_payload.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_arrow_down.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_copy.xml +12 -0
- package/android/src/main/res/drawable/chucker_ic_decoded_url_white.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_delete_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_encoded_url_white.xml +11 -0
- package/android/src/main/res/drawable/chucker_ic_graphql.xml +27 -0
- package/android/src/main/res/drawable/chucker_ic_http.xml +10 -0
- package/android/src/main/res/drawable/chucker_ic_https.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_launcher_foreground.xml +14 -0
- package/android/src/main/res/drawable/chucker_ic_save_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_search_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_share_white.xml +9 -0
- package/android/src/main/res/drawable/chucker_ic_transaction_notification.xml +9 -0
- package/android/src/main/res/layout/activity_main.xml +83 -0
- package/android/src/main/res/layout/activity_transaction.xml +48 -0
- package/android/src/main/res/layout/fragment_transaction_overview.xml +365 -0
- package/android/src/main/res/layout/fragment_transaction_payload.xml +132 -0
- package/android/src/main/res/layout/list_item_transaction.xml +122 -0
- package/android/src/main/res/layout/transaction_item_body_line.xml +13 -0
- package/android/src/main/res/layout/transaction_item_copy.xml +19 -0
- package/android/src/main/res/layout/transaction_item_headers.xml +12 -0
- package/android/src/main/res/layout/transaction_item_image.xml +16 -0
- package/android/src/main/res/menu/chucker_transaction.xml +46 -0
- package/android/src/main/res/menu/chucker_transactions_list.xml +41 -0
- package/android/src/main/res/mipmap-anydpi-v26/chucker_ic_launcher.xml +5 -0
- package/android/src/main/res/mipmap-hdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/chucker_ic_launcher.png +0 -0
- package/android/src/main/res/values/chucker_ic_launcher_background.xml +4 -0
- package/android/src/main/res/values/colors.xml +38 -0
- package/android/src/main/res/values/dimens.xml +10 -0
- package/android/src/main/res/values/public.xml +5 -0
- package/android/src/main/res/values/strings.xml +77 -0
- package/android/src/main/res/values/styles.xml +44 -0
- package/android/src/main/res/values-es/strings.xml +75 -0
- package/android/src/main/res/values-night/colors.xml +32 -0
- package/android/src/main/res/xml/chucker_provider_paths.xml +4 -0
- package/ios/SecuritySuite.swift +0 -2
- package/ios/SslPinning.swift +0 -26
- package/lib/commonjs/helpers.js +1 -1
- package/lib/commonjs/helpers.js.map +1 -1
- package/lib/commonjs/index.js +23 -40
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/helpers.js +3 -1
- package/lib/module/helpers.js.map +1 -1
- package/lib/module/index.js +21 -33
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/helpers.d.ts.map +1 -0
- package/lib/typescript/{index.d.ts → commonjs/src/index.d.ts} +1 -6
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/helpers.d.ts +3 -0
- package/lib/typescript/module/src/helpers.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +72 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/package.json +70 -43
- package/react-native-security-suite.podspec +23 -15
- package/src/helpers.ts +1 -1
- package/src/index.tsx +5 -18
- package/android/src/main/java/com/securitysuite/AndroidLogger.kt +0 -19
- package/android/src/main/java/com/securitysuite/modifier/Base64Decoder.kt +0 -11
- package/android/src/main/java/com/securitysuite/modifier/BasicAuthorizationHeaderModifier.kt +0 -16
- package/ios/SecuritySuite.xcodeproj/project.pbxproj +0 -293
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/SecuritySuite.xcodeproj/project.xcworkspace/xcuserdata/m.navabifar.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/SecuritySuite.xcodeproj/xcuserdata/m.navabifar.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
- package/lib/typescript/helpers.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- /package/lib/typescript/{helpers.d.ts → commonjs/src/helpers.d.ts} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import com.securitysuite.internal.data.entity.HttpHeader
|
|
4
|
+
import com.google.gson.JsonParseException
|
|
5
|
+
import com.google.gson.JsonParser
|
|
6
|
+
import org.w3c.dom.Document
|
|
7
|
+
import org.xml.sax.InputSource
|
|
8
|
+
import org.xml.sax.SAXParseException
|
|
9
|
+
import java.io.ByteArrayInputStream
|
|
10
|
+
import java.io.IOException
|
|
11
|
+
import java.io.StringWriter
|
|
12
|
+
import java.io.UnsupportedEncodingException
|
|
13
|
+
import java.net.URLDecoder
|
|
14
|
+
import java.nio.charset.Charset
|
|
15
|
+
import java.util.Locale
|
|
16
|
+
import javax.xml.XMLConstants
|
|
17
|
+
import javax.xml.parsers.DocumentBuilder
|
|
18
|
+
import javax.xml.parsers.DocumentBuilderFactory
|
|
19
|
+
import javax.xml.transform.OutputKeys
|
|
20
|
+
import javax.xml.transform.TransformerException
|
|
21
|
+
import javax.xml.transform.TransformerFactory
|
|
22
|
+
import javax.xml.transform.dom.DOMSource
|
|
23
|
+
import javax.xml.transform.stream.StreamResult
|
|
24
|
+
import kotlin.math.ln
|
|
25
|
+
import kotlin.math.pow
|
|
26
|
+
|
|
27
|
+
internal object FormatUtils {
|
|
28
|
+
private const val SI_MULTIPLE = 1000
|
|
29
|
+
private const val BASE_TWO_MULTIPLE = 1024
|
|
30
|
+
|
|
31
|
+
fun formatHeaders(
|
|
32
|
+
httpHeaders: List<HttpHeader>?,
|
|
33
|
+
withMarkup: Boolean,
|
|
34
|
+
): String {
|
|
35
|
+
return httpHeaders?.joinToString(separator = "") { header ->
|
|
36
|
+
if (withMarkup) {
|
|
37
|
+
"<b> ${header.name}: </b>${header.value} <br />"
|
|
38
|
+
} else {
|
|
39
|
+
"${header.name}: ${header.value}\n"
|
|
40
|
+
}
|
|
41
|
+
} ?: ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun formatByteCount(
|
|
45
|
+
bytes: Long,
|
|
46
|
+
si: Boolean,
|
|
47
|
+
): String {
|
|
48
|
+
val unit = if (si) SI_MULTIPLE else BASE_TWO_MULTIPLE
|
|
49
|
+
|
|
50
|
+
if (bytes < unit) {
|
|
51
|
+
return "$bytes B"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
|
55
|
+
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
|
|
56
|
+
|
|
57
|
+
return String.format(Locale.US, "%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fun formatJson(json: String): String {
|
|
61
|
+
return try {
|
|
62
|
+
val je = JsonParser.parseString(json)
|
|
63
|
+
JsonConverter.instance.toJson(je)
|
|
64
|
+
} catch (e: JsonParseException) {
|
|
65
|
+
json
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun formatXml(xml: String): String {
|
|
70
|
+
return try {
|
|
71
|
+
val documentFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
|
|
72
|
+
// This flag is required for security reasons
|
|
73
|
+
documentFactory.isExpandEntityReferences = false
|
|
74
|
+
|
|
75
|
+
val documentBuilder: DocumentBuilder = documentFactory.newDocumentBuilder()
|
|
76
|
+
val inputSource = InputSource(ByteArrayInputStream(xml.toByteArray(Charset.defaultCharset())))
|
|
77
|
+
val document: Document = documentBuilder.parse(inputSource)
|
|
78
|
+
|
|
79
|
+
val domSource = DOMSource(document)
|
|
80
|
+
val writer = StringWriter()
|
|
81
|
+
val result = StreamResult(writer)
|
|
82
|
+
|
|
83
|
+
TransformerFactory.newInstance().apply {
|
|
84
|
+
setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
|
85
|
+
}.newTransformer().apply {
|
|
86
|
+
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
|
|
87
|
+
setOutputProperty(OutputKeys.INDENT, "yes")
|
|
88
|
+
transform(domSource, result)
|
|
89
|
+
}
|
|
90
|
+
writer.toString()
|
|
91
|
+
} catch (ignore: SAXParseException) {
|
|
92
|
+
xml
|
|
93
|
+
} catch (ignore: IOException) {
|
|
94
|
+
xml
|
|
95
|
+
} catch (ignore: TransformerException) {
|
|
96
|
+
xml
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun formatUrlEncodedForm(form: String): String {
|
|
101
|
+
return try {
|
|
102
|
+
if (form.isBlank()) {
|
|
103
|
+
return form
|
|
104
|
+
}
|
|
105
|
+
form.split("&").joinToString(separator = "\n") { entry ->
|
|
106
|
+
val keyValue = entry.split("=")
|
|
107
|
+
val key = keyValue[0]
|
|
108
|
+
val value = if (keyValue.size > 1) URLDecoder.decode(keyValue[1], "UTF-8") else ""
|
|
109
|
+
"$key: $value"
|
|
110
|
+
}
|
|
111
|
+
} catch (ignore: IllegalArgumentException) {
|
|
112
|
+
form
|
|
113
|
+
} catch (ignore: UnsupportedEncodingException) {
|
|
114
|
+
form
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import okhttp3.HttpUrl
|
|
4
|
+
|
|
5
|
+
internal class FormattedUrl private constructor(
|
|
6
|
+
val scheme: String,
|
|
7
|
+
val host: String,
|
|
8
|
+
val port: Int,
|
|
9
|
+
val path: String,
|
|
10
|
+
val query: String,
|
|
11
|
+
) {
|
|
12
|
+
val pathWithQuery: String
|
|
13
|
+
get() =
|
|
14
|
+
if (query.isBlank()) {
|
|
15
|
+
path
|
|
16
|
+
} else {
|
|
17
|
+
"$path?$query"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
val url: String
|
|
21
|
+
get() {
|
|
22
|
+
return if (shouldShowPort()) {
|
|
23
|
+
"$scheme://$host:$port$pathWithQuery"
|
|
24
|
+
} else {
|
|
25
|
+
"$scheme://$host$pathWithQuery"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private fun shouldShowPort(): Boolean {
|
|
30
|
+
if (scheme == "https" && port == HTTPS_PORT) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
if (scheme == "http" && port == HTTP_PORT) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
private const val HTTPS_PORT = 443
|
|
41
|
+
private const val HTTP_PORT = 80
|
|
42
|
+
|
|
43
|
+
fun fromHttpUrl(
|
|
44
|
+
httpUrl: HttpUrl,
|
|
45
|
+
encoded: Boolean,
|
|
46
|
+
): FormattedUrl {
|
|
47
|
+
return if (encoded) {
|
|
48
|
+
encodedUrl(httpUrl)
|
|
49
|
+
} else {
|
|
50
|
+
decodedUrl(httpUrl)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun encodedUrl(httpUrl: HttpUrl): FormattedUrl {
|
|
55
|
+
val path = httpUrl.encodedPathSegments.joinToString("/")
|
|
56
|
+
return FormattedUrl(
|
|
57
|
+
httpUrl.scheme,
|
|
58
|
+
httpUrl.host,
|
|
59
|
+
httpUrl.port,
|
|
60
|
+
if (path.isNotBlank()) "/$path" else "",
|
|
61
|
+
httpUrl.encodedQuery.orEmpty(),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fun decodedUrl(httpUrl: HttpUrl): FormattedUrl {
|
|
66
|
+
val path = httpUrl.pathSegments.joinToString("/")
|
|
67
|
+
return FormattedUrl(
|
|
68
|
+
httpUrl.scheme,
|
|
69
|
+
httpUrl.host,
|
|
70
|
+
httpUrl.port,
|
|
71
|
+
if (path.isNotBlank()) "/$path" else "",
|
|
72
|
+
httpUrl.query.orEmpty(),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
4
|
+
import com.securitysuite.internal.data.har.Har
|
|
5
|
+
import com.securitysuite.internal.data.har.log.Creator
|
|
6
|
+
import kotlinx.coroutines.Dispatchers
|
|
7
|
+
import kotlinx.coroutines.withContext
|
|
8
|
+
|
|
9
|
+
// http://www.softwareishard.com/blog/har-12-spec/
|
|
10
|
+
// https://github.com/ahmadnassri/har-spec/blob/master/versions/1.2.md
|
|
11
|
+
internal object HarUtils {
|
|
12
|
+
suspend fun harStringFromTransactions(
|
|
13
|
+
transactions: List<HttpTransaction>,
|
|
14
|
+
name: String,
|
|
15
|
+
version: String,
|
|
16
|
+
): String =
|
|
17
|
+
withContext(Dispatchers.Default) {
|
|
18
|
+
JsonConverter.nonNullSerializerInstance
|
|
19
|
+
.toJson(fromHttpTransactions(transactions, Creator(name, version)))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
internal fun fromHttpTransactions(
|
|
23
|
+
transactions: List<HttpTransaction>,
|
|
24
|
+
creator: Creator,
|
|
25
|
+
): Har {
|
|
26
|
+
return Har(transactions, creator)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import okhttp3.HttpUrl
|
|
4
|
+
|
|
5
|
+
private const val PATH_SEGMENTS_DELIMITER = "/"
|
|
6
|
+
|
|
7
|
+
public fun HttpUrl.Builder.addNonBlankPathSegments(candidatePath: String): HttpUrl.Builder =
|
|
8
|
+
apply {
|
|
9
|
+
candidatePath.split(PATH_SEGMENTS_DELIMITER).filter { it.isNotBlank() }
|
|
10
|
+
.forEach { item -> addPathSegment(item) }
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import com.google.gson.Gson
|
|
4
|
+
import com.google.gson.GsonBuilder
|
|
5
|
+
|
|
6
|
+
internal object JsonConverter {
|
|
7
|
+
val nonNullSerializerInstance: Gson by lazy {
|
|
8
|
+
GsonBuilder()
|
|
9
|
+
.disableHtmlEscaping()
|
|
10
|
+
.setPrettyPrinting()
|
|
11
|
+
.create()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
val instance: Gson by lazy {
|
|
15
|
+
nonNullSerializerInstance.newBuilder()
|
|
16
|
+
.serializeNulls()
|
|
17
|
+
.create()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import okio.Buffer
|
|
4
|
+
import okio.ForwardingSource
|
|
5
|
+
import okio.Source
|
|
6
|
+
|
|
7
|
+
internal class LimitingSource(
|
|
8
|
+
delegate: Source,
|
|
9
|
+
private val bytesCountThreshold: Long,
|
|
10
|
+
) : ForwardingSource(delegate) {
|
|
11
|
+
private var bytesRead = 0L
|
|
12
|
+
val isThresholdReached get() = bytesRead >= bytesCountThreshold
|
|
13
|
+
|
|
14
|
+
override fun read(
|
|
15
|
+
sink: Buffer,
|
|
16
|
+
byteCount: Long,
|
|
17
|
+
) = if (!isThresholdReached) {
|
|
18
|
+
super.read(sink, byteCount).also { bytesRead += it }
|
|
19
|
+
} else {
|
|
20
|
+
-1L
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import androidx.arch.core.executor.ArchTaskExecutor
|
|
5
|
+
import androidx.lifecycle.LiveData
|
|
6
|
+
import androidx.lifecycle.MediatorLiveData
|
|
7
|
+
import java.util.concurrent.Executor
|
|
8
|
+
|
|
9
|
+
internal fun <T1, T2, R> LiveData<T1>.combineLatest(
|
|
10
|
+
other: LiveData<T2>,
|
|
11
|
+
func: (T1, T2) -> R,
|
|
12
|
+
): LiveData<R> {
|
|
13
|
+
return MediatorLiveData<R>().apply {
|
|
14
|
+
var lastA: T1? = null
|
|
15
|
+
var lastB: T2? = null
|
|
16
|
+
|
|
17
|
+
addSource(this@combineLatest) {
|
|
18
|
+
lastA = it
|
|
19
|
+
val observedB = lastB
|
|
20
|
+
if (it == null && value != null) {
|
|
21
|
+
value = null
|
|
22
|
+
} else if (it != null && observedB != null) {
|
|
23
|
+
value = func(it, observedB)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
addSource(other) {
|
|
28
|
+
lastB = it
|
|
29
|
+
val observedA = lastA
|
|
30
|
+
if (it == null && value != null) {
|
|
31
|
+
value = null
|
|
32
|
+
} else if (observedA != null && it != null) {
|
|
33
|
+
value = func(observedA, it)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
internal fun <T1, T2> LiveData<T1>.combineLatest(other: LiveData<T2>): LiveData<Pair<T1, T2>> {
|
|
40
|
+
return combineLatest(other) { a, b -> a to b }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Unlike built-in extension operation is performed on a provided thread pool.
|
|
44
|
+
// This is needed in our case since we compare requests and responses which can be big
|
|
45
|
+
// and result in frame drops.
|
|
46
|
+
internal fun <T> LiveData<T>.distinctUntilChanged(
|
|
47
|
+
executor: Executor = ioExecutor(),
|
|
48
|
+
areEqual: (old: T, new: T) -> Boolean = { old, new -> old == new },
|
|
49
|
+
): LiveData<T> {
|
|
50
|
+
val distinctMediator = MediatorLiveData<T>()
|
|
51
|
+
var old = uninitializedToken
|
|
52
|
+
distinctMediator.addSource(this) { new ->
|
|
53
|
+
executor.execute {
|
|
54
|
+
@Suppress("UNCHECKED_CAST")
|
|
55
|
+
if (old === uninitializedToken || !areEqual(old as T, new)) {
|
|
56
|
+
old = new
|
|
57
|
+
distinctMediator.postValue(new)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return distinctMediator
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private val uninitializedToken: Any? = Any()
|
|
65
|
+
|
|
66
|
+
// It is lesser evil than providing a custom executor.
|
|
67
|
+
@SuppressLint("RestrictedApi")
|
|
68
|
+
private fun ioExecutor() = ArchTaskExecutor.getIOThreadExecutor()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import com.securitysuite.api.Chucker
|
|
4
|
+
|
|
5
|
+
internal interface Logger {
|
|
6
|
+
fun info(
|
|
7
|
+
message: String,
|
|
8
|
+
throwable: Throwable? = null,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
fun warn(
|
|
12
|
+
message: String,
|
|
13
|
+
throwable: Throwable? = null,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
fun error(
|
|
17
|
+
message: String,
|
|
18
|
+
throwable: Throwable? = null,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
companion object : Logger {
|
|
22
|
+
override fun info(
|
|
23
|
+
message: String,
|
|
24
|
+
throwable: Throwable?,
|
|
25
|
+
) {
|
|
26
|
+
Chucker.logger.info(message, throwable)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override fun warn(
|
|
30
|
+
message: String,
|
|
31
|
+
throwable: Throwable?,
|
|
32
|
+
) {
|
|
33
|
+
Chucker.logger.warn(message, throwable)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override fun error(
|
|
37
|
+
message: String,
|
|
38
|
+
throwable: Throwable?,
|
|
39
|
+
) {
|
|
40
|
+
Chucker.logger.error(message, throwable)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.app.PendingIntent
|
|
6
|
+
import android.content.Context
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.util.LongSparseArray
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
import androidx.core.content.ContextCompat
|
|
12
|
+
import com.securitysuite.R
|
|
13
|
+
import com.securitysuite.api.Chucker
|
|
14
|
+
import com.securitysuite.internal.data.entity.HttpTransaction
|
|
15
|
+
import com.securitysuite.internal.ui.BaseChuckerActivity
|
|
16
|
+
import java.util.HashSet
|
|
17
|
+
|
|
18
|
+
internal class NotificationHelper(val context: Context) {
|
|
19
|
+
companion object {
|
|
20
|
+
private const val TRANSACTIONS_CHANNEL_ID = "chucker_transactions"
|
|
21
|
+
|
|
22
|
+
private const val TRANSACTION_NOTIFICATION_ID = 1138
|
|
23
|
+
|
|
24
|
+
private const val BUFFER_SIZE = 10
|
|
25
|
+
private const val INTENT_REQUEST_CODE = 11
|
|
26
|
+
private val transactionBuffer = LongSparseArray<HttpTransaction>()
|
|
27
|
+
private val transactionIdsSet = HashSet<Long>()
|
|
28
|
+
|
|
29
|
+
fun clearBuffer() {
|
|
30
|
+
synchronized(transactionBuffer) {
|
|
31
|
+
transactionBuffer.clear()
|
|
32
|
+
transactionIdsSet.clear()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private val notificationManager: NotificationManager =
|
|
38
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
39
|
+
|
|
40
|
+
private val transactionsScreenIntent by lazy {
|
|
41
|
+
PendingIntent.getActivity(
|
|
42
|
+
context,
|
|
43
|
+
TRANSACTION_NOTIFICATION_ID,
|
|
44
|
+
Chucker.getLaunchIntent(context),
|
|
45
|
+
PendingIntent.FLAG_UPDATE_CURRENT or immutableFlag(),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
init {
|
|
50
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
51
|
+
val transactionsChannel =
|
|
52
|
+
NotificationChannel(
|
|
53
|
+
TRANSACTIONS_CHANNEL_ID,
|
|
54
|
+
context.getString(R.string.chucker_network_notification_category),
|
|
55
|
+
NotificationManager.IMPORTANCE_LOW,
|
|
56
|
+
)
|
|
57
|
+
notificationManager.createNotificationChannels(listOf(transactionsChannel))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private fun addToBuffer(transaction: HttpTransaction) {
|
|
62
|
+
if (transaction.id == 0L) {
|
|
63
|
+
// Don't store Transactions with an invalid ID (0).
|
|
64
|
+
// Transaction with an Invalid ID will be shown twice in the notification
|
|
65
|
+
// with both the invalid and the valid ID and we want to avoid this.
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
synchronized(transactionBuffer) {
|
|
69
|
+
transactionIdsSet.add(transaction.id)
|
|
70
|
+
transactionBuffer.put(transaction.id, transaction)
|
|
71
|
+
if (transactionBuffer.size() > BUFFER_SIZE) {
|
|
72
|
+
transactionBuffer.removeAt(0)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private fun canShowNotifications(): Boolean {
|
|
78
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
79
|
+
notificationManager.areNotificationsEnabled()
|
|
80
|
+
} else {
|
|
81
|
+
true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun show(transaction: HttpTransaction) {
|
|
86
|
+
addToBuffer(transaction)
|
|
87
|
+
if (!BaseChuckerActivity.isInForeground && canShowNotifications()) {
|
|
88
|
+
val builder =
|
|
89
|
+
NotificationCompat.Builder(context, TRANSACTIONS_CHANNEL_ID)
|
|
90
|
+
.setContentIntent(transactionsScreenIntent)
|
|
91
|
+
.setLocalOnly(true)
|
|
92
|
+
.setSmallIcon(R.drawable.chucker_ic_transaction_notification)
|
|
93
|
+
.setColor(ContextCompat.getColor(context, R.color.chucker_color_primary))
|
|
94
|
+
.setContentTitle(context.getString(R.string.chucker_http_notification_title))
|
|
95
|
+
.setAutoCancel(true)
|
|
96
|
+
.addAction(createClearAction())
|
|
97
|
+
val inboxStyle = NotificationCompat.InboxStyle()
|
|
98
|
+
synchronized(transactionBuffer) {
|
|
99
|
+
var count = 0
|
|
100
|
+
for (i in transactionBuffer.size() - 1 downTo 0) {
|
|
101
|
+
val bufferedTransaction = transactionBuffer.valueAt(i)
|
|
102
|
+
if ((bufferedTransaction != null) && count < BUFFER_SIZE) {
|
|
103
|
+
if (count == 0) {
|
|
104
|
+
builder.setContentText(bufferedTransaction.notificationText)
|
|
105
|
+
}
|
|
106
|
+
inboxStyle.addLine(bufferedTransaction.notificationText)
|
|
107
|
+
}
|
|
108
|
+
count++
|
|
109
|
+
}
|
|
110
|
+
builder.setStyle(inboxStyle)
|
|
111
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
112
|
+
builder.setSubText(transactionIdsSet.size.toString())
|
|
113
|
+
} else {
|
|
114
|
+
builder.setNumber(transactionIdsSet.size)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
notificationManager.notify(TRANSACTION_NOTIFICATION_ID, builder.build())
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun createClearAction(): NotificationCompat.Action {
|
|
122
|
+
val clearTitle = context.getString(R.string.chucker_clear)
|
|
123
|
+
val clearTransactionsBroadcastIntent =
|
|
124
|
+
Intent(context, ClearDatabaseJobIntentServiceReceiver::class.java)
|
|
125
|
+
val pendingBroadcastIntent =
|
|
126
|
+
PendingIntent.getBroadcast(
|
|
127
|
+
context,
|
|
128
|
+
INTENT_REQUEST_CODE,
|
|
129
|
+
clearTransactionsBroadcastIntent,
|
|
130
|
+
PendingIntent.FLAG_ONE_SHOT or immutableFlag(),
|
|
131
|
+
)
|
|
132
|
+
return NotificationCompat.Action(
|
|
133
|
+
R.drawable.chucker_ic_delete_white,
|
|
134
|
+
clearTitle,
|
|
135
|
+
pendingBroadcastIntent,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fun dismissNotifications() {
|
|
140
|
+
notificationManager.cancel(TRANSACTION_NOTIFICATION_ID)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun immutableFlag() =
|
|
144
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
145
|
+
PendingIntent.FLAG_IMMUTABLE
|
|
146
|
+
} else {
|
|
147
|
+
0
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import okhttp3.Headers
|
|
4
|
+
import okhttp3.Response
|
|
5
|
+
import okio.Source
|
|
6
|
+
import okio.buffer
|
|
7
|
+
import okio.gzip
|
|
8
|
+
import okio.source
|
|
9
|
+
import org.brotli.dec.BrotliInputStream
|
|
10
|
+
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
|
|
11
|
+
import java.net.HttpURLConnection.HTTP_NO_CONTENT
|
|
12
|
+
import java.net.HttpURLConnection.HTTP_OK
|
|
13
|
+
import java.util.Locale
|
|
14
|
+
|
|
15
|
+
private const val HTTP_CONTINUE = 100
|
|
16
|
+
|
|
17
|
+
/** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */
|
|
18
|
+
internal fun Response.hasBody(): Boolean {
|
|
19
|
+
// HEAD requests never yield a body regardless of the response headers.
|
|
20
|
+
if (request.method == "HEAD") {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
val responseCode = code
|
|
25
|
+
if ((responseCode < HTTP_CONTINUE || responseCode >= HTTP_OK) &&
|
|
26
|
+
(responseCode != HTTP_NO_CONTENT) &&
|
|
27
|
+
(responseCode != HTTP_NOT_MODIFIED)
|
|
28
|
+
) {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
|
|
33
|
+
// response is malformed. For best compatibility, we honor the headers.
|
|
34
|
+
return ((contentLength > 0) || isChunked)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private val Response.contentLength: Long
|
|
38
|
+
get() {
|
|
39
|
+
return this.header("Content-Length")?.toLongOrNull() ?: -1L
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
internal val Response.isChunked: Boolean
|
|
43
|
+
get() {
|
|
44
|
+
return this.header("Transfer-Encoding").equals("chunked", ignoreCase = true)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
internal val Response.contentType: String?
|
|
48
|
+
get() {
|
|
49
|
+
return this.header("Content-Type")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private val Headers.containsGzip: Boolean
|
|
53
|
+
get() {
|
|
54
|
+
return this["Content-Encoding"].equals("gzip", ignoreCase = true)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private val Headers.containsBrotli: Boolean
|
|
58
|
+
get() {
|
|
59
|
+
return this["Content-Encoding"].equals("br", ignoreCase = true)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private val supportedEncodings = listOf("identity", "gzip", "br")
|
|
63
|
+
|
|
64
|
+
internal val Headers.hasSupportedContentEncoding: Boolean
|
|
65
|
+
get() =
|
|
66
|
+
get("Content-Encoding")
|
|
67
|
+
?.takeIf { it.isNotEmpty() }
|
|
68
|
+
?.let { it.lowercase(Locale.ROOT) in supportedEncodings }
|
|
69
|
+
?: true
|
|
70
|
+
|
|
71
|
+
internal fun Source.uncompress(headers: Headers) =
|
|
72
|
+
when {
|
|
73
|
+
headers.containsGzip -> gzip()
|
|
74
|
+
headers.containsBrotli -> BrotliInputStream(this.buffer().inputStream()).source()
|
|
75
|
+
else -> this
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
internal fun Headers.redact(names: Iterable<String>): Headers {
|
|
79
|
+
val builder = newBuilder()
|
|
80
|
+
for (name in names()) {
|
|
81
|
+
if (names.any { userHeader -> userHeader.equals(name, ignoreCase = true) }) {
|
|
82
|
+
builder[name] = "**"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return builder.build()
|
|
86
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import okio.Buffer
|
|
4
|
+
import okio.ByteString
|
|
5
|
+
import java.io.EOFException
|
|
6
|
+
import kotlin.math.min
|
|
7
|
+
|
|
8
|
+
private const val MAX_PREFIX_SIZE = 64L
|
|
9
|
+
private const val CODE_POINT_SIZE = 16
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns true if the [Buffer] contains human readable text. Uses a small sample
|
|
13
|
+
* of code points to detect unicode control characters commonly used in binary file signatures.
|
|
14
|
+
*/
|
|
15
|
+
internal val Buffer.isProbablyPlainText
|
|
16
|
+
get() =
|
|
17
|
+
try {
|
|
18
|
+
val prefix = Buffer()
|
|
19
|
+
val byteCount = min(size, MAX_PREFIX_SIZE)
|
|
20
|
+
copyTo(prefix, 0, byteCount)
|
|
21
|
+
sequence { while (!prefix.exhausted()) yield(prefix.readUtf8CodePoint()) }
|
|
22
|
+
.take(CODE_POINT_SIZE)
|
|
23
|
+
.all { codePoint -> codePoint.isPlainTextChar() }
|
|
24
|
+
} catch (_: EOFException) {
|
|
25
|
+
false // Truncated UTF-8 sequence
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
internal val ByteString.isProbablyPlainText: Boolean
|
|
29
|
+
get() {
|
|
30
|
+
val byteCount = min(size, MAX_PREFIX_SIZE.toInt())
|
|
31
|
+
return Buffer().write(this, offset = 0, byteCount).isProbablyPlainText
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private fun Int.isPlainTextChar() = Character.isWhitespace(this) || !Character.isISOControl(this)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.securitysuite.internal.support
|
|
2
|
+
|
|
3
|
+
import com.securitysuite.api.BodyDecoder
|
|
4
|
+
import okhttp3.Headers
|
|
5
|
+
import okhttp3.MediaType
|
|
6
|
+
import okhttp3.Request
|
|
7
|
+
import okhttp3.Response
|
|
8
|
+
import okio.ByteString
|
|
9
|
+
import kotlin.text.Charsets.UTF_8
|
|
10
|
+
|
|
11
|
+
internal object PlainTextDecoder : BodyDecoder {
|
|
12
|
+
override fun decodeRequest(
|
|
13
|
+
request: Request,
|
|
14
|
+
body: ByteString,
|
|
15
|
+
) = body.tryDecodeAsPlainText(request.headers, request.body?.contentType())
|
|
16
|
+
|
|
17
|
+
override fun decodeResponse(
|
|
18
|
+
response: Response,
|
|
19
|
+
body: ByteString,
|
|
20
|
+
) = body.tryDecodeAsPlainText(response.headers, response.body?.contentType())
|
|
21
|
+
|
|
22
|
+
private fun ByteString.tryDecodeAsPlainText(
|
|
23
|
+
headers: Headers,
|
|
24
|
+
contentType: MediaType?,
|
|
25
|
+
) = if (headers.hasSupportedContentEncoding && isProbablyPlainText) {
|
|
26
|
+
string(contentType?.charset() ?: UTF_8)
|
|
27
|
+
} else {
|
|
28
|
+
null
|
|
29
|
+
}
|
|
30
|
+
}
|