280 lines
12 KiB
Kotlin
280 lines
12 KiB
Kotlin
package org.mariotaku.twidere.util
|
|
|
|
import android.content.Context
|
|
import android.content.SharedPreferences
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.util.Base64
|
|
import android.util.Log
|
|
import okhttp3.*
|
|
import org.mariotaku.kpreferences.get
|
|
import org.mariotaku.ktextension.toIntOr
|
|
import org.mariotaku.restfu.http.RestHttpClient
|
|
import org.mariotaku.restfu.okhttp3.OkHttpRestClient
|
|
import org.mariotaku.twidere.constant.SharedPreferenceConstants.*
|
|
import org.mariotaku.twidere.constant.cacheSizeLimitKey
|
|
import org.mariotaku.twidere.util.dagger.DependencyHolder
|
|
import java.io.IOException
|
|
import java.net.InetSocketAddress
|
|
import java.net.Proxy
|
|
import java.security.KeyStore
|
|
import java.security.NoSuchAlgorithmException
|
|
import java.util.*
|
|
import java.util.concurrent.TimeUnit
|
|
import javax.net.ssl.SSLContext
|
|
import javax.net.ssl.TrustManagerFactory
|
|
import javax.net.ssl.X509TrustManager
|
|
|
|
/**
|
|
* Created by mariotaku on 16/1/27.
|
|
*/
|
|
object HttpClientFactory {
|
|
fun createRestHttpClient(conf: HttpClientConfiguration, dns: Dns, connectionPool: ConnectionPool,
|
|
cache: Cache): RestHttpClient {
|
|
val builder = OkHttpClient.Builder()
|
|
initOkHttpClient(conf, builder, dns, connectionPool, cache)
|
|
return OkHttpRestClient(builder.build())
|
|
}
|
|
|
|
fun initOkHttpClient(conf: HttpClientConfiguration, builder: OkHttpClient.Builder, dns: Dns,
|
|
connectionPool: ConnectionPool, cache: Cache) {
|
|
updateHttpClientConfiguration(builder, conf, dns, connectionPool, cache)
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
val tlsTocketFactory = TLSSocketFactory()
|
|
builder.sslSocketFactory(tlsTocketFactory, tlsTocketFactory.trustManager);
|
|
}
|
|
updateTLSConnectionSpecs(builder)
|
|
DebugModeUtils.initForOkHttpClient(builder)
|
|
}
|
|
|
|
fun reloadConnectivitySettings(context: Context) {
|
|
val holder = DependencyHolder.get(context)
|
|
val client = holder.restHttpClient as? OkHttpRestClient ?: return
|
|
val builder = OkHttpClient.Builder()
|
|
initOkHttpClient(HttpClientConfiguration(holder.preferences), builder, holder.dns,
|
|
holder.connectionPool, holder.cache)
|
|
client.client = builder.build()
|
|
}
|
|
|
|
/**
|
|
* # Supported patterns
|
|
*
|
|
* * `[SCHEME]`: E.g. `http` or `https`
|
|
* * `[HOST]`: Host address
|
|
* * `[PORT]`: Port number
|
|
* * `[AUTHORITY]`: `[HOST]`:`[PORT]` or `[HOST]` if port is default. Colon **will be** URL encoded
|
|
* * `[PATH]`: Raw path part, **without leading slash**
|
|
* * `[/PATH]`: Raw path part, **with leading slash**
|
|
* * `[PATH_ENCODED]`: Path, **will be** URL encoded again
|
|
* * `[QUERY]`: Raw query part
|
|
* * `[?QUERY]`: Raw query part, with `?` prefix
|
|
* * `[QUERY_ENCODED]`: Raw query part, **will be** URL encoded again
|
|
* * `[FRAGMENT]`: Raw fragment part
|
|
* * `[#FRAGMENT]`: Raw fragment part, with `#` prefix
|
|
* * `[FRAGMENT_ENCODED]`: Raw fragment part, **will be** URL encoded again
|
|
* * `[URL_ENCODED]`: URL Encoded `url` itself
|
|
* * `[URL_BASE64]`: Base64 Encoded `url` itself
|
|
*
|
|
* # Null values
|
|
* `[PATH]`, `[/PATH]`, `[QUERY]`, `[?QUERY]`, `[FRAGMENT]`, `[#FRAGMENT]` will be empty when
|
|
* it's null, values and base64-encoded will be string `"null"`.
|
|
*
|
|
* A valid format looks like
|
|
*
|
|
* `https://proxy.com/[SCHEME]/[AUTHORITY]/[PATH][?QUERY][#FRAGMENT]`,
|
|
*
|
|
* A request
|
|
*
|
|
* `https://example.com:8080/path?query=value#fragment`
|
|
*
|
|
* Will be transformed to
|
|
*
|
|
* `https://proxy.com/https/example.com%3A8080/path?query=value#fragment`
|
|
*/
|
|
@Suppress("KDocUnresolvedReference")
|
|
fun replaceUrl(url: HttpUrl, format: String): String {
|
|
val sb = StringBuffer()
|
|
var startIndex = 0
|
|
while (startIndex != -1) {
|
|
val find = format.findAnyOf(urlSupportedPatterns, startIndex) ?: break
|
|
sb.append(format, startIndex, find.first)
|
|
sb.append(when (find.second) {
|
|
"[SCHEME]" -> url.scheme()
|
|
"[HOST]" -> url.host()
|
|
"[PORT]" -> url.port()
|
|
"[AUTHORITY]" -> url.authority()
|
|
"[PATH]" -> url.encodedPath().removePrefix("/")
|
|
"[/PATH]" -> url.encodedPath().orEmpty()
|
|
"[PATH_ENCODED]" -> url.encodedPath().removePrefix("/").urlEncoded()
|
|
"[QUERY]" -> url.encodedQuery().orEmpty()
|
|
"[?QUERY]" -> url.encodedQuery()?.prefix("?").orEmpty()
|
|
"[QUERY_ENCODED]" -> url.encodedQuery()?.urlEncoded()
|
|
"[FRAGMENT]" -> url.encodedFragment().orEmpty()
|
|
"[#FRAGMENT]" -> url.encodedFragment()?.prefix("#").orEmpty()
|
|
"[FRAGMENT_ENCODED]" -> url.encodedFragment()?.urlEncoded()
|
|
"[URL_ENCODED]" -> url.toString().urlEncoded()
|
|
"[URL_BASE64]" -> Base64.encodeToString(url.toString().toByteArray(Charsets.UTF_8),
|
|
Base64.URL_SAFE)
|
|
else -> throw AssertionError()
|
|
})
|
|
startIndex = find.first + find.second.length
|
|
}
|
|
sb.append(format, startIndex, format.length)
|
|
return sb.toString()
|
|
}
|
|
|
|
private fun updateHttpClientConfiguration(builder: OkHttpClient.Builder, conf: HttpClientConfiguration,
|
|
dns: Dns, connectionPool: ConnectionPool, cache: Cache) {
|
|
conf.applyTo(builder)
|
|
builder.dns(dns)
|
|
builder.connectionPool(connectionPool)
|
|
builder.cache(cache)
|
|
}
|
|
|
|
private fun updateTLSConnectionSpecs(builder: OkHttpClient.Builder) {
|
|
//Default spec list from OkHttpClient.DEFAULT_CONNECTION_SPECS
|
|
val specList: ArrayList<ConnectionSpec> = ArrayList()
|
|
specList.add(ConnectionSpec.MODERN_TLS)
|
|
nougatECCFix(specList)
|
|
specList.add(ConnectionSpec.CLEARTEXT)
|
|
builder.connectionSpecs(specList)
|
|
}
|
|
|
|
private fun nougatECCFix(specList: ArrayList<ConnectionSpec>) {
|
|
// Shamelessly stolen from Tusky
|
|
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N) {
|
|
return
|
|
}
|
|
val sslContext = try {
|
|
SSLContext.getInstance("TLS")
|
|
} catch (e: NoSuchAlgorithmException) {
|
|
Log.e("HttpClientFactory", "Failed obtaining TLS Context.")
|
|
return
|
|
}
|
|
|
|
sslContext.init(null, null, null)
|
|
val cipherSuites = sslContext.socketFactory.defaultCipherSuites
|
|
val allowedList = cipherSuites.filterNotTo(ArrayList<String>()) { it.contains("ECDH") }
|
|
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
|
.cipherSuites(*allowedList.toTypedArray())
|
|
.supportsTlsExtensions(true)
|
|
.build()
|
|
specList.add(spec)
|
|
}
|
|
|
|
private fun HttpUrl.authority(): String {
|
|
val host = host()
|
|
val port = port()
|
|
if (port == HttpUrl.defaultPort(scheme())) return host
|
|
return "$host%3A$port"
|
|
}
|
|
|
|
private fun String.urlEncoded() = Uri.encode(this)
|
|
|
|
private fun String.prefix(prefix: String) = prefix + this
|
|
|
|
private fun systemDefaultTrustManager(): X509TrustManager {
|
|
val trustManagerFactory = TrustManagerFactory.getInstance(
|
|
TrustManagerFactory.getDefaultAlgorithm())
|
|
trustManagerFactory.init(null as KeyStore?)
|
|
val trustManagers = trustManagerFactory.trustManagers
|
|
if (trustManagers.size != 1 || trustManagers[0] !is X509TrustManager) {
|
|
throw IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers))
|
|
}
|
|
return trustManagers[0] as X509TrustManager
|
|
}
|
|
|
|
private val urlSupportedPatterns = listOf("[SCHEME]", "[HOST]", "[PORT]", "[AUTHORITY]",
|
|
"[PATH]", "[/PATH]", "[PATH_ENCODED]", "[QUERY]", "[?QUERY]", "[QUERY_ENCODED]",
|
|
"[FRAGMENT]", "[#FRAGMENT]", "[FRAGMENT_ENCODED]", "[URL_ENCODED]", "[URL_BASE64]")
|
|
|
|
class HttpClientConfiguration(val prefs: SharedPreferences) {
|
|
|
|
var readTimeoutSecs: Long = -1
|
|
var writeTimeoutSecs: Long = -1
|
|
var connectionTimeoutSecs: Long = prefs.getInt(KEY_CONNECTION_TIMEOUT, 10).toLong()
|
|
var cacheSize: Int = prefs[cacheSizeLimitKey]
|
|
|
|
internal fun applyTo(builder: OkHttpClient.Builder) {
|
|
if (connectionTimeoutSecs >= 0) {
|
|
builder.connectTimeout(connectionTimeoutSecs, TimeUnit.SECONDS)
|
|
}
|
|
if (writeTimeoutSecs >= 0) {
|
|
builder.writeTimeout(writeTimeoutSecs, TimeUnit.SECONDS)
|
|
}
|
|
if (readTimeoutSecs >= 0) {
|
|
builder.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
|
}
|
|
if (prefs.getBoolean(KEY_ENABLE_PROXY, false)) {
|
|
configProxy(builder)
|
|
}
|
|
}
|
|
|
|
private fun configProxy(builder: OkHttpClient.Builder) {
|
|
val proxyType = prefs.getString(KEY_PROXY_TYPE, null) ?: return
|
|
val proxyHost = prefs.getString(KEY_PROXY_HOST, null)?.takeIf(String::isNotEmpty) ?: return
|
|
val proxyPort = prefs.getString(KEY_PROXY_PORT, null).toIntOr(-1)
|
|
val username = prefs.getString(KEY_PROXY_USERNAME, null)?.takeIf(String::isNotEmpty)
|
|
val password = prefs.getString(KEY_PROXY_PASSWORD, null)?.takeIf(String::isNotEmpty)
|
|
when (proxyType) {
|
|
"http" -> {
|
|
if (proxyPort !in (0..65535)) {
|
|
return
|
|
}
|
|
val address = InetSocketAddress.createUnresolved(proxyHost, proxyPort)
|
|
builder.proxy(Proxy(Proxy.Type.HTTP, address))
|
|
builder.proxyAuthenticator { _, response ->
|
|
val b = response.request().newBuilder()
|
|
if (response.code() == 407) {
|
|
if (username != null && password != null) {
|
|
val credential = Credentials.basic(username, password)
|
|
b.header("Proxy-Authorization", credential)
|
|
}
|
|
}
|
|
b.build()
|
|
}
|
|
builder.authenticator { _, response ->
|
|
val b = response.request().newBuilder()
|
|
if (response.code() == 407) {
|
|
if (username != null && password != null) {
|
|
val credential = Credentials.basic(username, password)
|
|
b.header("Proxy-Authorization", credential)
|
|
}
|
|
}
|
|
b.build()
|
|
}
|
|
}
|
|
"reverse" -> {
|
|
builder.addInterceptor(ReverseProxyInterceptor(proxyHost, username, password))
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Intercept and replace proxy patterns to real URL
|
|
*/
|
|
class ReverseProxyInterceptor(val proxyFormat: String, val proxyUsername: String?,
|
|
val proxyPassword: String?) : Interceptor {
|
|
|
|
override fun intercept(chain: Interceptor.Chain): Response {
|
|
val request = chain.request()
|
|
val url = request.url()
|
|
val builder = request.newBuilder()
|
|
val replacedUrl = HttpUrl.parse(replaceUrl(url, proxyFormat)) ?: run {
|
|
throw IOException("Invalid reverse proxy format")
|
|
}
|
|
builder.url(replacedUrl)
|
|
if (proxyUsername != null && proxyPassword != null) {
|
|
val headerValue = Base64.encodeToString("$proxyUsername:$proxyPassword".toByteArray(Charsets.UTF_8),
|
|
Base64.URL_SAFE)
|
|
builder.addHeader("Proxy-Authorization", headerValue)
|
|
}
|
|
return chain.proceed(builder.build())
|
|
}
|
|
|
|
}
|
|
}
|