From bda542d2e5d90b219effee2ea9fabc0c6dcbae3d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 7 Dec 2019 01:30:35 +0800 Subject: [PATCH] add url test --- .../com/github/kr328/clash/MainApplication.kt | 9 ++ .../com/github/kr328/clash/ProxyActivity.kt | 53 +++++++++- .../kr328/clash/adapter/ProxyAdapter.kt | 47 +++++++-- .../com/github/kr328/clash/model/ListProxy.kt | 4 +- .../com/github/kr328/clash/utils/DpUtils.kt | 13 +++ build.gradle | 4 +- buildSrc/build.gradle | 2 +- core/src/main/golang/server/profile.go | 27 +---- core/src/main/golang/server/proxies.go | 98 ++++++++++++++++--- core/src/main/golang/server/server.go | 2 + .../java/com/github/kr328/clash/core/Clash.kt | 32 +++++- .../kr328/clash/core/model/UrlTestPacket.kt | 11 +++ .../clash/callback/IUrlTestCallback.aidl | 5 + .../kr328/clash/service/IClashService.aidl | 2 + .../kr328/clash/service/ClashEventService.kt | 4 +- .../kr328/clash/service/ClashService.kt | 23 ++++- 16 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt create mode 100644 core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt create mode 100644 service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 14c5bb9..0736332 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -1,6 +1,7 @@ package com.github.kr328.clash import android.app.Application +import android.content.Context import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp @@ -9,6 +10,14 @@ class MainApplication : Application() { const val KEY_PROXY_MODE = "key_proxy_mode" const val PROXY_MODE_VPN = "vpn" const val PROXY_MODE_PROXY_ONLY = "proxy_only" + + lateinit var instance: MainApplication + } + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + + instance = this } override fun onCreate() { diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt index 747d9f0..f060cc1 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt @@ -2,7 +2,9 @@ package com.github.kr328.clash import android.os.Bundle import com.github.kr328.clash.adapter.ProxyAdapter +import com.github.kr328.clash.callback.IUrlTestCallback import com.github.kr328.clash.core.model.ProxyPacket +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.model.ListProxy import kotlinx.android.synthetic.main.activity_proxies.* @@ -14,11 +16,7 @@ class ProxyActivity : BaseActivity() { setSupportActionBar(activity_proxies_toolbar) activity_proxies_list.also { - it.adapter = ProxyAdapter(this) { p, s -> - runClash { clash -> - clash.setSelectProxy(p, s) - } - } + it.adapter = ProxyAdapter(this, this::setProxySelected, this::urlTest) it.layoutManager = (it.adapter!! as ProxyAdapter).getLayoutManager() } @@ -29,6 +27,51 @@ class ProxyActivity : BaseActivity() { refreshList() } + private fun setProxySelected(name: String, selected: String) { + runClash { + it.setSelectProxy(name, selected) + } + } + + private fun urlTest(position: Int, size: Int) { + val adapter = (activity_proxies_list.adapter as ProxyAdapter) + + runClash { + val proxies = adapter.elements.subList(position, position+size) + .filterIsInstance() + .mapIndexed { index, proxy -> proxy.name to IndexedValue(index, proxy)} + .toMap() + + for ((_, p) in proxies) { + Log.i("$p -> ${p.index}") + + p.value.delay = -1 + } + + it.startUrlTest(proxies.keys.toTypedArray(), object: IUrlTestCallback.Stub() { + override fun onResult(proxy: String?, delay: Long) { + if ( proxy == null ) { + (adapter.elements[position] as ListProxy.ListProxyHeader).urlTest = false + + runOnUiThread { + adapter.notifyItemRangeChanged(position, size) + } + + return + } + + val p = proxies[proxy] ?: return + + p.value.delay = delay + + runOnUiThread { + adapter.notifyItemChanged(position + p.index + 1) + } + } + }) + } + } + private fun refreshList() { if (!activity_proxies_swipe.isRefreshing) activity_proxies_swipe.isRefreshing = true diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index 3494ea6..dadca9e 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -13,7 +13,9 @@ import com.github.kr328.clash.core.model.ProxyPacket import com.github.kr328.clash.model.ListProxy import com.google.android.material.card.MaterialCardView -class ProxyAdapter(private val context: Context, val listener: (String, String) -> Unit) : +class ProxyAdapter(private val context: Context, + private val onSelect: (String, String) -> Unit, + private val onUrlTest: (Int, Int) -> Unit) : RecyclerView.Adapter() { var elements: List = emptyList() var clickable: Boolean = false @@ -108,7 +110,7 @@ class ProxyAdapter(private val context: Context, val listener: (String, String) notifyItemChanged(old) notifyItemChanged(position) - listener(element.header.name, element.name) + onSelect(element.header.name, element.name) } } else { holder.card.setOnClickListener(null) @@ -119,20 +121,45 @@ class ProxyAdapter(private val context: Context, val listener: (String, String) holder.name.text = current.name holder.type.text = current.type holder.delay.text = - if (current.delay > 0) { - current.delay.toString() - } else { - if (current.header.type != ProxyPacket.Type.SELECT) - "N/A" - else - "" + when { + current.header.urlTest && current.delay < 0 -> "..." + current.delay < 0 -> "N/A" + current.delay > 0 -> { + current.delay.toString() + } + else -> { + if (current.header.type != ProxyPacket.Type.SELECT) + "N/A" + else + "" + } } - } private fun bindHeaderView(holder: HeaderHolder, current: ListProxy.ListProxyHeader) { holder.name.text = current.name holder.test.visibility = if (current.type == ProxyPacket.Type.SELECT) View.VISIBLE else View.GONE + holder.test.setOnClickListener { + if ( current.urlTest ) + return@setOnClickListener + + val indexed = elements.withIndex() + .filter { it.value is ListProxy.ListProxyHeader } + .filterIsInstance>() + + val headerIndex = indexed.indexOfFirst { + it.value === current + }.takeIf { it >= 0 } ?: return@setOnClickListener + val header = indexed[headerIndex] + + indexed[headerIndex].value.urlTest = true + + val position = header.index + val size = (indexed.getOrNull(headerIndex + 1)?.index ?: elements.size) - header.index + + notifyItemRangeChanged(position, size) + onUrlTest(position, size) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt index b86a8f2..50dd35d 100644 --- a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt +++ b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt @@ -3,13 +3,13 @@ package com.github.kr328.clash.model import com.github.kr328.clash.core.model.ProxyPacket interface ListProxy { - data class ListProxyHeader(val name: String, val type: ProxyPacket.Type, var now: Int) : + data class ListProxyHeader(val name: String, val type: ProxyPacket.Type, var now: Int, var urlTest: Boolean = false) : ListProxy data class ListProxyItem( val name: String, val type: String, - val delay: Long, + var delay: Long, val header: ListProxyHeader ) : ListProxy } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt new file mode 100644 index 0000000..8d80ef0 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt @@ -0,0 +1,13 @@ +package com.github.kr328.clash.utils + +import android.util.TypedValue +import com.github.kr328.clash.MainApplication + +val Int.dp: Float + get() { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + MainApplication.instance.resources.displayMetrics + ) + } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5e9e221..f21d4e7 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.3.60' + kotlin_version = '1.3.61' room_version = '2.2.2' } repositories { @@ -14,7 +14,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath 'com.google.gms:google-services:4.3.3' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 052c514..64f32da 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' apply plugin: 'kotlin' buildscript { - ext.kotlin_version = '1.3.60' + ext.kotlin_version = '1.3.61' repositories { mavenCentral() } diff --git a/core/src/main/golang/server/profile.go b/core/src/main/golang/server/profile.go index 172ab13..7ec6cf6 100644 --- a/core/src/main/golang/server/profile.go +++ b/core/src/main/golang/server/profile.go @@ -4,9 +4,7 @@ import ( "encoding/json" "net" - A "github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" "github.com/kr328/cfa/profile" ) @@ -54,31 +52,8 @@ func handleProfileReload(client *net.UnixConn) { return } - proxies := tunnel.Instance().Proxies() - for k, v := range payload.Selected { - p := proxies[k] - - if p == nil { - response.InvalidSelected = append(response.InvalidSelected, k) - continue - } - - proxy, ok := p.(*A.Proxy) - if !ok { - response.InvalidSelected = append(response.InvalidSelected, k) - continue - } - - selector, ok := proxy.ProxyAdapter.(*A.Selector) - if !ok { - response.InvalidSelected = append(response.InvalidSelected, k) - continue - } - - log.Infoln("Set selector " + k + " -> " + v) - - selector.Set(v) + setProxySelect(k, v) } log.Infoln("Profile " + payload.Path + " loaded") diff --git a/core/src/main/golang/server/proxies.go b/core/src/main/golang/server/proxies.go index 1ab161d..1497290 100644 --- a/core/src/main/golang/server/proxies.go +++ b/core/src/main/golang/server/proxies.go @@ -1,9 +1,12 @@ package server import ( + "context" "encoding/json" + "errors" "net" "strconv" + "time" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/proxy" @@ -95,8 +98,6 @@ func handleQueryProxies(client *net.UnixConn) { } func handleSetProxy(client *net.UnixConn) { - proxies := tunnel.Instance().Proxies() - var Request struct { Key string `json:"key"` Value string `json:"value"` @@ -119,28 +120,101 @@ func handleSetProxy(client *net.UnixConn) { json.Unmarshal(buf, &Request) - p := proxies[Request.Key] + if err := setProxySelect(Request.Key, Request.Value); err != nil { + Response.Error = err.Error() + } +} + +func handleUrlTest(client *net.UnixConn) { + var request struct { + URL string `json:"url"` + Timeout int `json:"timeout"` + Proxies []string `json:"proxies"` + } + + request.Proxies = make([]string, 0) + + buf, err := readCommandPacket(client) + if err != nil { + return + } + + if json.Unmarshal(buf, &request) != nil { + return + } + + type Response struct { + Name string `json:"name"` + Delay int `json:"delay"` + } + + proxies := tunnel.Instance().Proxies() + channel := make(chan *Response, len(request.Proxies)) + + for _, p := range request.Proxies { + go func(p string) { + proxy := proxies[p] + if proxy == nil { + channel <- nil + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(request.Timeout)*time.Millisecond) + + defer cancel() + + delay, err := proxies[p].URLTest(ctx, request.URL) + if err != nil { + channel <- nil + return + } + + channel <- &Response{ + Name: p, + Delay: int(delay), + } + }(p) + } + + for range request.Proxies { + response := <-channel + + if response != nil { + buf, _ := json.Marshal(&response) + + writeCommandPacket(client, buf) + } + } + + writeCommandPacket(client, nil) + + log.Infoln("URLTest exited") +} + +func setProxySelect(name, selected string) error { + proxies := tunnel.Instance().Proxies() + + p := proxies[name] if p == nil { - Response.Error = "Unknown proxy " + Request.Key - return + return errors.New("Unknown proxy " + name) } proxy, ok := p.(*A.Proxy) if !ok { - Response.Error = "Invalid proxy " + Request.Key - return + return errors.New("Invalid proxy " + name) } selector, ok := proxy.ProxyAdapter.(*A.Selector) if !ok { - Response.Error = "Not selector" - return + return errors.New("Not selector") } - log.Infoln("Set selector " + Request.Key + " -> " + Request.Value) + log.Infoln("Set selector " + name + " -> " + selected) - if err := selector.Set(Request.Value); err != nil { - Response.Error = err.Error() + if err := selector.Set(selected); err != nil { + return err } + + return nil } diff --git a/core/src/main/golang/server/server.go b/core/src/main/golang/server/server.go index 0790f4a..4214e3a 100644 --- a/core/src/main/golang/server/server.go +++ b/core/src/main/golang/server/server.go @@ -19,6 +19,7 @@ const ( commandPullBandwidth uint32 = 8 commandSetProxy uint32 = 9 commandQueryGeneral uint32 = 10 + commandUrlTest uint32 = 11 ) var handlers map[uint32]func(*net.UnixConn) = make(map[uint32]func(*net.UnixConn)) @@ -35,6 +36,7 @@ func init() { handlers[commandPullBandwidth] = handlePullBandwidthEvent // event.go handlers[commandSetProxy] = handleSetProxy // proxies.go handlers[commandQueryGeneral] = handleQueryGeneral // proxies.go + handlers[commandUrlTest] = handleUrlTest // proxies.go } // Start local control server diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index b89ef92..4b017b7 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -6,12 +6,11 @@ import com.github.kr328.clash.core.event.BandwidthEvent import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.event.ProcessEvent import com.github.kr328.clash.core.event.SpeedEvent -import com.github.kr328.clash.core.model.GeneralPacket -import com.github.kr328.clash.core.model.LoadProfilePacket -import com.github.kr328.clash.core.model.RawProxyPacket -import com.github.kr328.clash.core.model.SetProxyPacket +import com.github.kr328.clash.core.model.* +import com.github.kr328.clash.core.utils.Log import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration +import kotlinx.serialization.stringify import java.io.File import java.io.FileDescriptor import java.io.IOException @@ -33,8 +32,12 @@ class Clash( const val COMMAND_PULL_BANDWIDTH = 8 const val COMMAND_SET_PROXY = 9 const val COMMAND_QUERY_GENERAL = 10 + const val COMMAND_URL_TEST = 11 const val PING_REPLY = 233 + + const val DEFAULT_URL_TEST_TIMEOUT = 5000 + const val DEFAULT_URL_TEST_URL = "https://www.gstatic.com/generate_204" } val process = ClashProcess(context, clashDir, controllerPath, listener) @@ -153,6 +156,27 @@ class Clash( } } + fun startUrlTest(proxies: List, callback: (String, Long) -> Unit) { + runControl(COMMAND_URL_TEST) { _, input, output -> + output.writeString(Json(JsonConfiguration.Stable) + .stringify(UrlTestPacket.Request.serializer(), + UrlTestPacket.Request(proxies, DEFAULT_URL_TEST_TIMEOUT, DEFAULT_URL_TEST_URL))) + + while ( true ) { + val data = input.readString() + if ( data.isEmpty() ) + return@runControl + + val response = Json(JsonConfiguration.Stable) + .parse(UrlTestPacket.Response.serializer(), data) + + callback(response.name, response.delay) + } + } + + Log.i("Url test exited") + } + fun startTunDevice(fd: FileDescriptor, mtu: Int) { runControl(COMMAND_TUN_START) { socket, _, output -> socket.setFileDescriptorsForSend(arrayOf(fd)) diff --git a/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt new file mode 100644 index 0000000..91ea122 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt @@ -0,0 +1,11 @@ +package com.github.kr328.clash.core.model + +import kotlinx.serialization.Serializable + +class UrlTestPacket { + @Serializable + data class Request(val proxies: List, val timeout: Int, val url: String) + @Serializable + data class Response(val name: String, val delay: Long) +} + diff --git a/service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl b/service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl new file mode 100644 index 0000000..5e6b8fb --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl @@ -0,0 +1,5 @@ +package com.github.kr328.clash.callback; + +interface IUrlTestCallback { + void onResult(String proxy, long delay); +} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl index 6652662..83712a0 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl @@ -4,6 +4,7 @@ import com.github.kr328.clash.service.IClashEventObserver; import com.github.kr328.clash.service.IClashEventService; import com.github.kr328.clash.service.IClashProfileService; import com.github.kr328.clash.service.IClashSettingService; +import com.github.kr328.clash.callback.IUrlTestCallback; import com.github.kr328.clash.core.event.Event; import com.github.kr328.clash.core.model.Packet; @@ -26,4 +27,5 @@ interface IClashService { // Query ProxyPacket queryAllProxies(); GeneralPacket queryGeneral(); + void startUrlTest(in String[] proxies, IUrlTestCallback callback); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt index e0516be..87dd73c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt @@ -5,7 +5,7 @@ import java.util.concurrent.Executors class ClashEventService(private val master: Master) : IClashEventService.Stub() { interface Master { - fun requireEvent(event: Int) + fun acquireEvent(event: Int) fun releaseEvent(event: Int) } @@ -130,7 +130,7 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub() }.toSet() val rel = EVENT_SET - req - req.forEach(master::requireEvent) + req.forEach(master::acquireEvent) rel.forEach(master::releaseEvent) } } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt index 3398d8a..fda410d 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashService.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.* +import com.github.kr328.clash.callback.IUrlTestCallback import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.ClashProcess import com.github.kr328.clash.core.event.* @@ -15,6 +16,7 @@ import com.github.kr328.clash.core.utils.Log import java.io.File import java.io.IOException import java.util.concurrent.Executors +import kotlin.concurrent.thread class ClashService : Service(), IClashEventObserver, ClashEventService.Master, ClashProfileService.Master, ClashEventPuller.Master { @@ -66,6 +68,24 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } } + override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { + require(proxies != null && callback != null) + + thread { + try { + clash.startUrlTest(proxies.toList()) { name, delay -> + callback.onResult(name, delay) + } + } + catch (e: Exception) { + Log.w("Url test failure", e) + } + + callback.onResult(null, -1) + // Ignore exceptions + } + } + override fun start() { try { clash.process.start() @@ -134,7 +154,7 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } } - override fun requireEvent(event: Int) { + override fun acquireEvent(event: Int) { if (clash.process.getProcessStatus() == ProcessEvent.STOPPED) return @@ -146,7 +166,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, Event.EVENT_BANDWIDTH -> puller.startBandwidthPull() } - } override fun releaseEvent(event: Int) {