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