Apply proxy settings to WebView
parent
2c8476cabd
commit
b3028258ca
@ -0,0 +1,82 @@
|
||||
package org.koitharu.kotatsu.browser
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
|
||||
import org.koitharu.kotatsu.core.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.core.util.ext.consumeAll
|
||||
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
|
||||
|
||||
@Inject
|
||||
lateinit var proxyProvider: ProxyProvider
|
||||
|
||||
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
|
||||
return
|
||||
}
|
||||
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
|
||||
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
|
||||
onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(
|
||||
v: View,
|
||||
insets: WindowInsetsCompat
|
||||
): WindowInsetsCompat {
|
||||
val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()
|
||||
val barsInsets = insets.getInsets(type)
|
||||
viewBinding.webView.updatePadding(
|
||||
left = barsInsets.left,
|
||||
right = barsInsets.right,
|
||||
bottom = barsInsets.bottom,
|
||||
)
|
||||
viewBinding.appbar.updatePadding(
|
||||
left = barsInsets.left,
|
||||
right = barsInsets.right,
|
||||
top = barsInsets.top,
|
||||
)
|
||||
return insets.consumeAll(type)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewBinding.webView.onPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewBinding.webView.onResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (hasViewBinding()) {
|
||||
viewBinding.webView.stopLoading()
|
||||
viewBinding.webView.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||
viewBinding.progressBar.isVisible = isLoading
|
||||
}
|
||||
|
||||
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
|
||||
this.title = title
|
||||
supportActionBar?.subtitle = subtitle
|
||||
}
|
||||
|
||||
override fun onHistoryChanged() {
|
||||
onBackPressedCallback.onHistoryChanged()
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okio.IOException
|
||||
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.ProxySelector
|
||||
import java.net.SocketAddress
|
||||
import java.net.URI
|
||||
|
||||
class AppProxySelector(
|
||||
private val settings: AppSettings,
|
||||
) : ProxySelector() {
|
||||
|
||||
init {
|
||||
setDefault(this)
|
||||
}
|
||||
|
||||
private var cachedProxy: Proxy? = null
|
||||
|
||||
override fun select(uri: URI?): List<Proxy> {
|
||||
return listOf(getProxy())
|
||||
}
|
||||
|
||||
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
|
||||
ioe?.printStackTraceDebug()
|
||||
}
|
||||
|
||||
private fun getProxy(): Proxy {
|
||||
val type = settings.proxyType
|
||||
val address = settings.proxyAddress
|
||||
val port = settings.proxyPort
|
||||
if (type == Proxy.Type.DIRECT) {
|
||||
return Proxy.NO_PROXY
|
||||
}
|
||||
if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) {
|
||||
throw ProxyConfigException()
|
||||
}
|
||||
cachedProxy?.let {
|
||||
val addr = it.address() as? InetSocketAddress
|
||||
if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
val proxy = Proxy(type, InetSocketAddress(address, port))
|
||||
cachedProxy = proxy
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
package org.koitharu.kotatsu.core.network
|
||||
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.Proxy
|
||||
|
||||
class ProxyAuthenticator(
|
||||
private val settings: AppSettings,
|
||||
) : Authenticator, java.net.Authenticator() {
|
||||
|
||||
init {
|
||||
setDefault(this)
|
||||
}
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
|
||||
return null
|
||||
}
|
||||
val login = settings.proxyLogin ?: return null
|
||||
val password = settings.proxyPassword ?: return null
|
||||
val credential = Credentials.basic(login, password)
|
||||
return response.request.newBuilder()
|
||||
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
val login = settings.proxyLogin ?: return null
|
||||
val password = settings.proxyPassword ?: return null
|
||||
return PasswordAuthentication(login, password.toCharArray())
|
||||
}
|
||||
|
||||
private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
package org.koitharu.kotatsu.core.network.proxy
|
||||
|
||||
import androidx.webkit.ProxyConfig
|
||||
import androidx.webkit.ProxyController
|
||||
import androidx.webkit.WebViewFeature
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.PasswordAuthentication
|
||||
import java.net.Proxy
|
||||
import java.net.ProxySelector
|
||||
import java.net.SocketAddress
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import java.net.Authenticator as JavaAuthenticator
|
||||
|
||||
@Singleton
|
||||
class ProxyProvider @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
private var cachedProxy: Proxy? = null
|
||||
|
||||
val selector = object : ProxySelector() {
|
||||
override fun select(uri: URI?): List<Proxy> {
|
||||
return listOf(getProxy())
|
||||
}
|
||||
|
||||
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: okio.IOException?) {
|
||||
ioe?.printStackTraceDebug()
|
||||
}
|
||||
}
|
||||
|
||||
val authenticator = ProxyAuthenticator()
|
||||
|
||||
init {
|
||||
ProxySelector.setDefault(selector)
|
||||
JavaAuthenticator.setDefault(authenticator)
|
||||
}
|
||||
|
||||
suspend fun applyWebViewConfig() {
|
||||
val isProxyEnabled = isProxyEnabled()
|
||||
if (!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
|
||||
if (isProxyEnabled) {
|
||||
throw IllegalArgumentException("Proxy for WebView is not supported") // TODO localize
|
||||
}
|
||||
} else {
|
||||
val controller = ProxyController.getInstance()
|
||||
if (settings.proxyType == Proxy.Type.DIRECT) {
|
||||
suspendCoroutine { cont ->
|
||||
controller.clearProxyOverride(
|
||||
(cont.context[CoroutineDispatcher] ?: Dispatchers.Main).asExecutor(),
|
||||
) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val url = buildString {
|
||||
when (settings.proxyType) {
|
||||
Proxy.Type.DIRECT -> Unit
|
||||
Proxy.Type.HTTP -> append("http")
|
||||
Proxy.Type.SOCKS -> append("socks")
|
||||
}
|
||||
append("://")
|
||||
append(settings.proxyAddress)
|
||||
append(':')
|
||||
append(settings.proxyPort)
|
||||
}
|
||||
if (settings.proxyType == Proxy.Type.SOCKS) {
|
||||
System.setProperty("java.net.socks.username", settings.proxyLogin);
|
||||
System.setProperty("java.net.socks.password", settings.proxyPassword);
|
||||
}
|
||||
val proxyConfig = ProxyConfig.Builder()
|
||||
.addProxyRule(url)
|
||||
.build()
|
||||
suspendCoroutine { cont ->
|
||||
controller.setProxyOverride(
|
||||
proxyConfig,
|
||||
(cont.context[CoroutineDispatcher] ?: Dispatchers.Main).asExecutor(),
|
||||
) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT
|
||||
|
||||
private fun getProxy(): Proxy {
|
||||
val type = settings.proxyType
|
||||
val address = settings.proxyAddress
|
||||
val port = settings.proxyPort
|
||||
if (type == Proxy.Type.DIRECT) {
|
||||
return Proxy.NO_PROXY
|
||||
}
|
||||
if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) {
|
||||
throw ProxyConfigException()
|
||||
}
|
||||
cachedProxy?.let {
|
||||
val addr = it.address() as? InetSocketAddress
|
||||
if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
val proxy = Proxy(type, InetSocketAddress(address, port))
|
||||
cachedProxy = proxy
|
||||
return proxy
|
||||
}
|
||||
|
||||
inner class ProxyAuthenticator : Authenticator, JavaAuthenticator() {
|
||||
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
|
||||
return null
|
||||
}
|
||||
val login = settings.proxyLogin ?: return null
|
||||
val password = settings.proxyPassword ?: return null
|
||||
val credential = Credentials.basic(login, password)
|
||||
return response.request.newBuilder()
|
||||
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
|
||||
.build()
|
||||
}
|
||||
|
||||
public override fun getPasswordAuthentication(): PasswordAuthentication? {
|
||||
if (!isProxyEnabled()) {
|
||||
return null
|
||||
}
|
||||
val login = settings.proxyLogin ?: return null
|
||||
val password = settings.proxyPassword ?: return null
|
||||
return PasswordAuthentication(login, password.toCharArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue