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

Fix JWT from being logged #946

Merged
merged 3 commits into from
Jul 2, 2023
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
20 changes: 4 additions & 16 deletions app/src/main/java/com/jerboa/api/Http.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import android.util.Log
import com.jerboa.datatypes.types.*
import com.jerboa.db.Account
import com.jerboa.toastException
import com.jerboa.util.DisableLog
import com.jerboa.util.CustomHttpLoggingInterceptor
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONObject
import retrofit2.Invocation
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
Expand All @@ -24,6 +22,8 @@ import okhttp3.Response as HttpResponse
const val VERSION = "v3"
const val DEFAULT_INSTANCE = "lemmy.ml"
const val MINIMUM_API_VERSION: String = "0.18"
val REDACTED_QUERY_PARAMS = setOf("auth")
val REDACTED_BODY_FIELDS = setOf("jwt", "password")

interface API {
@GET("site")
Expand All @@ -44,7 +44,6 @@ interface API {
/**
* Log into lemmy.
*/
@DisableLog
@POST("user/login")
suspend fun login(@Body form: Login): Response<LoginResponse>

Expand Down Expand Up @@ -264,24 +263,13 @@ interface API {
}

private fun buildApi(): API {
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
val client: OkHttpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val requestBuilder = chain.request().newBuilder()
.header("User-Agent", "Jerboa")
val newRequest = requestBuilder.build()
chain.proceed(newRequest)
}
.addInterceptor { chain ->
// based on https://stackoverflow.com/a/76264357
val request = chain.request()
val invocation = request.tag(Invocation::class.java)
val disableLog = invocation?.method()?.getAnnotation(DisableLog::class.java)
val shouldLogBody: Boolean = disableLog == null
interceptor.setLevel(if (shouldLogBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE)
chain.proceed(request)
}
// this should probably be a network interceptor,
.addInterceptor { chain ->
val request = chain.request()
Expand All @@ -302,7 +290,7 @@ interface API {
.build()
}
}
.addInterceptor(interceptor)
.addInterceptor(CustomHttpLoggingInterceptor(REDACTED_QUERY_PARAMS, REDACTED_BODY_FIELDS))
.build()

return Retrofit.Builder()
Expand Down
208 changes: 208 additions & 0 deletions app/src/main/java/com/jerboa/util/CustomHttpLoggingInterceptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package com.jerboa.util

import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.charset
import okhttp3.internal.http.promisesBody
import okhttp3.internal.platform.Platform
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.internal.isProbablyUtf8
import okio.Buffer
import okio.GzipSource
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit

/**
* Based of [HttpLoggingInterceptor], redacts the giving fields
*/
class CustomHttpLoggingInterceptor @JvmOverloads constructor(
private val redactedQueryParams: Set<String> = emptySet(),
private val redactedBodyFields: Set<String> = emptySet(),
private val redaction: String = "REDACTED",
private val logger: Logger = Logger.DEFAULT,
) : Interceptor {

fun interface Logger {
fun log(message: String)

companion object {
/** A [Logger] defaults output appropriate for the current platform. */
@JvmField
val DEFAULT: Logger = DefaultLogger()
private class DefaultLogger : Logger {
override fun log(message: String) {
Platform.get().log(message)
}
}
}
}

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

val requestBody = request.body

val connection = chain.connection()
var requestStartMessage =
("--> ${request.method} ${redactQueryParams(request.url)}${if (connection != null) " " + connection.protocol() else ""}")
if (requestBody != null) {
requestStartMessage += " (${requestBody.contentLength()}-byte body)"
}
logger.log(requestStartMessage)

val headers = request.headers

if (requestBody != null) {
// Request body headers are only present when installed as a network interceptor. When not
// already present, force them to be included (if available) so their values are known.
requestBody.contentType()?.let {
if (headers["Content-Type"] == null) {
logger.log("Content-Type: $it")
}
}
}

for (i in 0 until headers.size) {
logHeader(headers, i)
}

if (requestBody == null) {
logger.log("--> END ${request.method}")
} else if (bodyHasUnknownEncoding(request.headers)) {
logger.log("--> END ${request.method} (encoded body omitted)")
} else if (requestBody.isDuplex()) {
logger.log("--> END ${request.method} (duplex request body omitted)")
} else if (requestBody.isOneShot()) {
logger.log("--> END ${request.method} (one-shot body omitted)")
} else {
var buffer = Buffer()
requestBody.writeTo(buffer)

var gzippedLength: Long? = null
if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) {
gzippedLength = buffer.size
GzipSource(buffer).use { gzippedResponseBody ->
buffer = Buffer()
buffer.writeAll(gzippedResponseBody)
}
}

logger.log("")
if (!buffer.isProbablyUtf8()) {
logger.log(
"--> END ${request.method} (binary ${requestBody.contentLength()}-byte body omitted)",
)
} else if (gzippedLength != null) {
logger.log("--> END ${request.method} (${buffer.size}-byte, $gzippedLength-gzipped-byte body)")
} else {
val charset: Charset = requestBody.contentType().charset()
logger.log(redactBody(buffer.readString(charset)))
logger.log("--> END ${request.method} (${requestBody.contentLength()}-byte body)")
}
}

val startNs = System.nanoTime()
val response: Response
try {
response = chain.proceed(request)
} catch (e: Exception) {
logger.log("<-- HTTP FAILED: $e")
throw e
}

val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)

val responseBody = response.body
val contentLength = responseBody.contentLength()
val bodySize = if (contentLength != -1L) "$contentLength-byte" else "unknown-length"
logger.log(
"<-- ${response.code}${if (response.message.isEmpty()) "" else ' ' + response.message} ${redactQueryParams(response.request.url)} (${tookMs}ms${", $bodySize body"})",
)

val headersRes = response.headers
for (i in 0 until headersRes.size) {
logHeader(headersRes, i)
}

if (!response.promisesBody()) {
logger.log("<-- END HTTP")
} else if (bodyHasUnknownEncoding(response.headers)) {
logger.log("<-- END HTTP (encoded body omitted)")
} else if (bodyIsStreaming(response)) {
logger.log("<-- END HTTP (streaming)")
} else {
val source = responseBody.source()
source.request(Long.MAX_VALUE) // Buffer the entire body.
var buffer = source.buffer

var gzippedLength: Long? = null
if ("gzip".equals(headersRes["Content-Encoding"], ignoreCase = true)) {
gzippedLength = buffer.size
GzipSource(buffer.clone()).use { gzippedResponseBody ->
buffer = Buffer()
buffer.writeAll(gzippedResponseBody)
}
}

val charset: Charset = responseBody.contentType().charset()

if (!buffer.isProbablyUtf8()) {
logger.log("")
logger.log("<-- END HTTP (binary ${buffer.size}-byte body omitted)")
return response
}

if (contentLength != 0L) {
logger.log("")
logger.log(redactBody(buffer.clone().readString(charset)))
}

if (gzippedLength != null) {
logger.log("<-- END HTTP (${buffer.size}-byte, $gzippedLength-gzipped-byte body)")
} else {
logger.log("<-- END HTTP (${buffer.size}-byte body)")
}
}

return response
}

private fun logHeader(headers: Headers, i: Int) {
val value = headers.value(i)
logger.log(headers.name(i) + ": " + value)
}

private fun bodyHasUnknownEncoding(headers: Headers): Boolean {
val contentEncoding = headers["Content-Encoding"] ?: return false
return !contentEncoding.equals("identity", ignoreCase = true) &&
!contentEncoding.equals("gzip", ignoreCase = true)
}

private fun bodyIsStreaming(response: Response): Boolean {
val contentType = response.body.contentType()
return contentType != null && contentType.type == "text" && contentType.subtype == "event-stream"
}

private fun redactQueryParams(httpUrl: HttpUrl): String {
val builder = httpUrl.newBuilder()
val toRedact = httpUrl.queryParameterNames.intersect(redactedQueryParams)
toRedact.forEach { builder.setQueryParameter(it, redaction) }
return builder.toString()
}

private fun redactBody(body: String): String {
return try {
val json = JSONObject(body)
redactedBodyFields.filter { json.has(it) }.forEach { json.put(it, redaction) }
json.toString()
} catch (_: JSONException) {
body
}
}
}
5 changes: 0 additions & 5 deletions app/src/main/java/com/jerboa/util/DisableLog.kt

This file was deleted.