diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index f761466..5e480fd 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.os.Bundle import android.view.View import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.model.GeneralPacket +import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.utils.ByteFormatter import com.github.kr328.clash.service.TunService import com.github.kr328.clash.utils.ServiceUtils @@ -113,13 +113,13 @@ class MainActivity : BaseActivity() { runOnUiThread { when ( general.mode ) { - GeneralPacket.Mode.DIRECT -> + General.Mode.DIRECT -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_direct) - GeneralPacket.Mode.GLOBAL -> + General.Mode.GLOBAL -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_global) - GeneralPacket.Mode.RULE -> + General.Mode.RULE -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_rule) } diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index b6654b6..6aaf2bd 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -173,11 +173,4 @@ open class GolangBindTask : DefaultTask() { if (process.waitFor() != 0) throw GradleException("Run command $this failure") } - - private fun String.exe(): String { - return if (Os.isFamily(Os.FAMILY_WINDOWS)) - "$this.exe" - else - this - } } \ No newline at end of file diff --git a/buildSrc/src/main/java/GolangBuildOptions.kt b/buildSrc/src/main/java/GolangBuildOptions.kt deleted file mode 100644 index a3df273..0000000 --- a/buildSrc/src/main/java/GolangBuildOptions.kt +++ /dev/null @@ -1,3 +0,0 @@ -import java.io.File - -data class GolangBuildOptions(val sourceDir: File, val outputDir: File, val platform: String) diff --git a/buildSrc/src/main/java/GolangBuildTask.kt b/buildSrc/src/main/java/GolangBuildTask.kt deleted file mode 100644 index 68a2d3d..0000000 --- a/buildSrc/src/main/java/GolangBuildTask.kt +++ /dev/null @@ -1,92 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.api.GradleException -import org.gradle.api.tasks.Exec -import java.io.File -import java.io.FileReader - -import java.util.Properties - -open class GolangBuildTask : Exec() { - private lateinit var options: GolangBuildOptions - private lateinit var abi: String - - override fun exec() { - val toolchainRoot = findAndroidNdkPath().resolve("toolchains/llvm/prebuilt/${detectOsType()}/bin") - val linkerPrefix = toolChainPrefix() - val compilerPrefix = linkerPrefix + options.platform - - if ( !toolchainRoot.exists() ) - throw GradleException("Compiler not found") - - workingDir = options.sourceDir - - environment.put("GOARCH", golangArch()) - environment.put("GOOS", "android") - environment.put("CGO_ENABLED", "1") - environment.put("GOPATH", project.buildDir.resolve("intermediates/gopath").absolutePath) - environment.put("CXX", toolchainRoot.resolve(compilerPrefix + "-clang++".cmd()).absolutePath) - environment.put("CC", toolchainRoot.resolve(compilerPrefix + "-clang".cmd()).absolutePath) - environment.put("LD", toolchainRoot.resolve(linkerPrefix + "ld".exe()).absolutePath) - - commandLine = listOf("go".exe(), "build", "-o", options.outputDir.resolve("$abi/libclash.so").absolutePath) - - super.exec() - } - - fun setOptions(options: GolangBuildOptions, abi: String) { - this.options = options - this.abi = abi - } - - private fun findAndroidNdkPath(): File { - val properties = FileReader(project.rootProject.file("local.properties")).use { - Properties().apply { load(it) } - } - - return properties.getProperty("ndk.dir")?.let { File(it) }?.takeIf { it.exists() } - ?: throw GradleException("Android NDK not found.") - } - - private fun detectOsType(): String { - return when { - Os.isFamily(Os.FAMILY_WINDOWS) -> "windows-x86_64" - Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" - Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" - else -> throw GradleException("Unsupported Build OS ${System.getenv("os.name")}") - } - } - - private fun golangArch(): String { - return when (abi) { - "armeabi-v7a" -> "arm" - "arm64-v8a" -> "arm64" - "x86" -> "i686" - "x86_64" -> "amd64" - else -> throw GradleException("Unsupported arch $abi") - } - } - - private fun toolChainPrefix(): String { - return when (abi) { - "armeabi-v7a" -> "arm-linux-androideabi" - "arm64-v8a" -> "aarch64-linux-android" - "x86" -> "i686-linux-android" - "x86_64" -> "x86_64-linux-android" - else -> throw GradleException("Unsupported arch $abi") - } - } - - private fun String.exe(): String { - return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) - "$this.exe" - else - this - } - - private fun String.cmd(): String { - return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) - "$this.cmd" - else - this - } -} diff --git a/core/src/main/golang/bridge/general.go b/core/src/main/golang/bridge/general.go new file mode 100644 index 0000000..ac422e3 --- /dev/null +++ b/core/src/main/golang/bridge/general.go @@ -0,0 +1,27 @@ +package bridge + +import ( + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/tunnel" +) + +type TunnelGeneral struct { + Mode string + HTTPPort int + SocksPort int + RedirectPort int +} + +func QueryGeneral() *TunnelGeneral { + result := &TunnelGeneral{} + + g := executor.GetGeneral() + t := tunnel.Instance() + + result.Mode = t.Mode().String() + result.HTTPPort = g.Port + result.SocksPort = g.SocksPort + result.RedirectPort = g.RedirPort + + return result +} diff --git a/core/src/main/golang/bridge/proxies.go b/core/src/main/golang/bridge/proxies.go new file mode 100644 index 0000000..7693d4b --- /dev/null +++ b/core/src/main/golang/bridge/proxies.go @@ -0,0 +1,63 @@ +package bridge + +import ( + "encoding/json" + + "github.com/Dreamacro/clash/tunnel" +) + +type Proxy struct { + Name string + Type string `json:"type"` + All []string `json:"all"` + Now string `json:"now"` + Delay int +} + +type ProxyList struct { + proxies []*Proxy +} + +func (p Proxy) GetAllLength() int { + return len(p.All) +} + +func (p Proxy) GetAllElement(index int) string { + return p.All[index] +} + +func (pl *ProxyList) GetProxiesLength() int { + return len(pl.proxies) +} + +func (pl *ProxyList) GetProxiesElement(index int) *Proxy { + return pl.proxies[index] +} + +func QueryAllProxies() (*ProxyList, error) { + ps := tunnel.Instance().Proxies() + result := make([]*Proxy, len(ps)) + currentIndex := 0 + + for k, v := range ps { + current := &Proxy{ + Name: k, + Type: v.Type().String(), + All: []string{}, + Delay: int(v.LastDelay()), + } + + data, err := v.MarshalJSON() + if err != nil { + return nil, err + } + + json.Unmarshal(data, current) + + result[currentIndex] = current + + currentIndex++ + } + + return &ProxyList{proxies: result}, nil +} 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 2e68cc1..43b11c7 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 @@ -1,194 +1,118 @@ package com.github.kr328.clash.core import android.content.Context -import android.net.LocalSocket -import com.github.kr328.clash.core.event.BandwidthEvent -import com.github.kr328.clash.core.event.LogEvent +import bridge.Bridge import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.core.event.SpeedEvent -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 +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.Proxy +import java.lang.IllegalStateException class Clash( context: Context, - clashDir: File, - controllerPath: File, - listener: (ProcessEvent) -> Unit -) : BaseClash(controllerPath) { + private val listener: (ProcessEvent) -> Unit +) { companion object { - const val COMMAND_PING = 0 - const val COMMAND_TUN_START = 1 - const val COMMAND_TUN_STOP = 2 - const val COMMAND_PROFILE_RELOAD = 4 - const val COMMAND_QUERY_PROXIES = 5 - const val COMMAND_PULL_SPEED = 6 - const val COMMAND_PULL_LOG = 7 - 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 CLASH_DIR = "clash" 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) + private var currentProcess = ProcessEvent.STOPPED - fun ping(): Boolean { - return runControlNoException(COMMAND_PING) { _, input, _ -> - input.readInt() == PING_REPLY - } ?: false + init { + val home = context.filesDir.resolve(CLASH_DIR) + + home.mkdirs() + + Bridge.init(home.absolutePath) } - fun loadProfile(file: File, selected: Map): List { - return runControl(COMMAND_PROFILE_RELOAD) { _, input, output -> - output.writeString( - Json(JsonConfiguration.Stable) - .stringify( - LoadProfilePacket.Request.serializer(), - LoadProfilePacket.Request(file.absolutePath, selected) - ) - ) - - val result = Json(JsonConfiguration.Stable) - .parse(LoadProfilePacket.Response.serializer(), input.readString()) - - if (result.error.isNotEmpty()) { - throw IOException(result.error) - } - - return@runControl result.invalidSelected - } + fun getCurrentProcessStatus(): ProcessEvent { + return currentProcess } - fun queryGeneral(): GeneralPacket { - return runCatching { - runControl(COMMAND_QUERY_GENERAL) { _, input, _ -> - Json(JsonConfiguration.Stable).parse(GeneralPacket.serializer(), input.readString()) - } - }.getOrDefault( - GeneralPacket( - GeneralPacket.Ports(0, 0, 0, 0), - GeneralPacket.Mode.DIRECT - ) - ) + fun start() { + if ( currentProcess == ProcessEvent.STARTED ) + return + + currentProcess = ProcessEvent.STARTED + + listener(currentProcess) + + loadDefault() } - fun queryProxies(): RawProxyPacket { - return runControl(COMMAND_QUERY_PROXIES) { _, input, _ -> - val data = input.readString() + fun stop() { + if ( currentProcess == ProcessEvent.STOPPED ) + return - Json(JsonConfiguration.Stable.copy(strictMode = false)) - .parse(RawProxyPacket.serializer(), data) - } + currentProcess = ProcessEvent.STOPPED + + listener(currentProcess) + + loadDefault() } - fun setSelectProxy(key: String, value: String) { - runControl(COMMAND_SET_PROXY) { _, input, output -> - output.writeString( - Json(JsonConfiguration.Stable) - .stringify( - SetProxyPacket.Request.serializer(), - SetProxyPacket.Request(key, value) - ) - ) + fun startTunDevice(fd: Int, mtu: Int, gateway: String, dns: String, onSocket: (Int) -> Unit) { + enforceStarted() - val response = Json(JsonConfiguration.Stable).parse( - SetProxyPacket.Response.serializer(), - input.readString() - ) - - if (response.error.isNotEmpty()) - throw IOException(response.error) - } - } - - fun pullSpeedEvent(initial: (LocalSocket) -> Unit, callback: (SpeedEvent) -> Unit) { - runControlNoException(COMMAND_PULL_SPEED) { socket, input, _ -> - initial(socket) - - while (!Thread.currentThread().isInterrupted) { - callback( - Json(JsonConfiguration.Stable).parse( - SpeedEvent.serializer(), - input.readString() - ) - ) - } - } - } - - fun pullLogsEvent(initial: (LocalSocket) -> Unit, callback: (LogEvent) -> Unit) { - runControlNoException(COMMAND_PULL_LOG) { socket, input, _ -> - initial(socket) - - while (!Thread.currentThread().isInterrupted) { - callback( - Json(JsonConfiguration.Stable).parse( - LogEvent.serializer(), - input.readString() - ) - ) - } - } - } - - fun pullBandwidthEvent(initial: (LocalSocket) -> Unit, callback: (BandwidthEvent) -> Unit) { - runControlNoException(COMMAND_PULL_BANDWIDTH) { socket, input, _ -> - initial(socket) - - while (!Thread.currentThread().isInterrupted) { - callback( - Json(JsonConfiguration.Stable).parse( - BandwidthEvent.serializer(), - input.readString() - ) - ) - } - } - } - - 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, dnsHijacking: Boolean) { - runControl(COMMAND_TUN_START) { socket, _, output -> - socket.setFileDescriptorsForSend(arrayOf(fd)) - socket.outputStream.write(0) - socket.outputStream.flush() - output.writeInt(mtu) - output.writeInt(if ( dnsHijacking ) 1 else 0) - output.writeInt(0x243) + Bridge.startTunDevice(fd.toLong(), mtu.toLong(), gateway, dns) { + onSocket(it.toInt()) } } fun stopTunDevice() { - runControlNoException(COMMAND_TUN_STOP) + Bridge.stopTunDevice() + } + + fun loadProfile(path: String) { + enforceStarted() + + Bridge.loadProfileFile(path) + } + + fun queryProxies(): List { + enforceStarted() + + val list = Bridge.queryAllProxies() + val result = mutableListOf() + + for (i in 0..list.proxiesLength) { + val p = list.getProxiesElement(i) + val all = mutableListOf() + + for (index in 0..p.allLength) { + all.add(p.getAllElement(index)) + } + + result.add( + Proxy( + p.name, + Proxy.Type.fromString(p.type), + p.now, + all, + p.delay + ) + ) + } + + return result + } + + fun queryGeneral(): General { + val t = Bridge.queryGeneral() + + return General(General.Mode.fromString(t.mode), + t.httpPort.toInt(), t.socksPort.toInt(), t.redirectPort.toInt()) + } + + private fun loadDefault() { + Bridge.loadProfileDefault() + } + + private fun enforceStarted() { + if ( currentProcess == ProcessEvent.STOPPED ) + throw IllegalStateException("Clash Stopped") } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt new file mode 100644 index 0000000..b411b7b --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt @@ -0,0 +1,65 @@ +package com.github.kr328.clash.core.model + +import android.os.Parcel +import android.os.Parcelable +import com.github.kr328.clash.core.serialization.Parcels +import kotlinx.serialization.Serializable +import java.lang.IllegalStateException + +fun List.compress(): CompressedProxyList { + val nameMap = this.toList().mapIndexed { index, proxy -> + proxy.name to index + }.toMap() + + val elements = this.map { + CompressedProxyList.Element( + name = nameMap[it.name] ?: throw IllegalStateException("Unknown proxy $it"), + type = it.type, + now = nameMap[it.now] ?: throw IllegalStateException("Unknown proxy $it"), + all = it.all.mapNotNull { name -> nameMap[name] }, + delay = it.delay + ) + } + + return CompressedProxyList(nameMap.entries.associate { (k, v) -> v to k }, elements) +} + +@Serializable +data class CompressedProxyList(val proxyName: Map, val elements: List): Parcelable { + @Serializable + data class Element(val name: Int, + val type: Proxy.Type, + val now: Int, + val all: List, + val delay: Long) + + fun uncompress(): List { + return elements.map { + Proxy( + name = proxyName[it.name] ?: throw IllegalStateException("Unknown proxy $it"), + type = it.type, + now = proxyName[it.now] ?: throw IllegalStateException("Unknown proxy $it"), + all = it.all.mapNotNull { index -> proxyName[index] }, + delay = it.delay + ) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + Parcels.dump(serializer(), this, parcel) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CompressedProxyList { + return Parcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/GeneralPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/General.kt similarity index 54% rename from core/src/main/java/com/github/kr328/clash/core/model/GeneralPacket.kt rename to core/src/main/java/com/github/kr328/clash/core/model/General.kt index cad1778..f310e93 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/GeneralPacket.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/General.kt @@ -5,25 +5,35 @@ import android.os.Parcelable import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.* import kotlinx.serialization.internal.StringDescriptor -import java.lang.IllegalArgumentException +import kotlin.IllegalArgumentException @Serializable -data class GeneralPacket(val ports: Ports, val mode: Mode) : Parcelable { +data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: Int) : Parcelable { @Serializable(with = ModeSerializer::class) enum class Mode { - DIRECT, GLOBAL, RULE - } + DIRECT, GLOBAL, RULE; - @Serializable - data class Ports(val http: Int, val socks: Int, val redirect: Int, val randomHttp: Int) - - class ModeSerializer : KSerializer { - companion object { - const val MODE_DIRECT = 1 - const val MODE_GLOBAL = 2 - const val MODE_RULE = 3 + override fun toString(): String { + return when ( this ) { + DIRECT -> "Direct" + GLOBAL -> "Global" + RULE -> "Rule" + } } + companion object { + fun fromString(mode: String): Mode { + return when ( mode ) { + "Direct" -> DIRECT + "Global" -> GLOBAL + "Rule" -> RULE + else -> throw IllegalArgumentException("Invalid mode $mode") + } + } + } + } + + class ModeSerializer : KSerializer { override val descriptor: SerialDescriptor get() = StringDescriptor @@ -53,13 +63,20 @@ data class GeneralPacket(val ports: Ports, val mode: Mode) : Parcelable { return 0 } - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): GeneralPacket { - return Parcels.load(serializer(), parcel) - } + companion object { + const val MODE_DIRECT = 1 + const val MODE_GLOBAL = 2 + const val MODE_RULE = 3 - override fun newArray(size: Int): Array { - return arrayOfNulls(size) + @JvmField + val CREATOR = object: Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): General { + return Parcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt deleted file mode 100644 index 77adf3e..0000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -class LoadProfilePacket { - @Serializable - data class Request(val path: String, val selected: Map) - @Serializable - data class Response(val error: String, val invalidSelected: List) -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt new file mode 100644 index 0000000..19cf097 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt @@ -0,0 +1,80 @@ +package com.github.kr328.clash.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Proxy( + val name: String, + val type: Type, + val now: String, + val all: List, + val delay: Long +) { + enum class Type { + SELECT, + URL_TEST, + FALLBACK, + DIRECT, + REJECT, + SHADOWSOCKS, + SNELL, + SOCKS5, + HTTP, + VMESS, + LOAD_BALANCE, + UNKNOWN; + + override fun toString(): String { + return when (this) { + SELECT -> TYPE_SELECT + URL_TEST -> TYPE_URL_TEST + FALLBACK -> TYPE_FALLBACK + DIRECT -> TYPE_DIRECT + REJECT -> TYPE_REJECT + SHADOWSOCKS -> TYPE_SHADOWSOCKS + SNELL -> TYPE_SNELL + SOCKS5 -> TYPE_SOCKS5 + HTTP -> TYPE_HTTP + VMESS -> TYPE_VMESS + LOAD_BALANCE -> TYPE_LOAD_BALANCE + UNKNOWN -> TYPE_UNKNOWN + } + } + + companion object { + fun fromString(type: String): Type { + return when (type) { + TYPE_SELECT -> SELECT + TYPE_URL_TEST -> URL_TEST + TYPE_FALLBACK -> FALLBACK + TYPE_DIRECT -> DIRECT + TYPE_REJECT -> REJECT + TYPE_SHADOWSOCKS -> SHADOWSOCKS + TYPE_SNELL -> SNELL + TYPE_SOCKS5 -> SOCKS5 + TYPE_HTTP -> HTTP + TYPE_VMESS -> VMESS + TYPE_LOAD_BALANCE -> LOAD_BALANCE + TYPE_UNKNOWN -> UNKNOWN + else -> UNKNOWN + } + } + } + } + + companion object { + private const val TYPE_SELECT = "Selector" + private const val TYPE_URL_TEST = "URLTest" + private const val TYPE_FALLBACK = "Fallback" + private const val TYPE_DIRECT = "Direct" + private const val TYPE_REJECT = "Reject" + private const val TYPE_SHADOWSOCKS = "Shadowsocks" + private const val TYPE_SNELL = "Snell" + private const val TYPE_SOCKS5 = "Socks5" + private const val TYPE_HTTP = "Http" + private const val TYPE_VMESS = "Vmess" + private const val TYPE_LOAD_BALANCE = "LoadBalance" + private const val TYPE_UNKNOWN = "Unknown" + + } +} diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt deleted file mode 100644 index 651297d..0000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.kr328.clash.core.model - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -data class ProxyPacket(val mode: String, val proxies: Map): Parcelable { - @Serializable - data class Proxy(val name: String, val type: Type, val now: Int, val all: List, val delay: Long) - - enum class Type { - SELECT, - URL_TEST, - FALLBACK, - DIRECT, - REJECT, - SHADOWSOCKS, - SNELL, - SOCKS5, - HTTP, - VMESS, - LOAD_BALANCE, - UNKNOWN; - - override fun toString(): String { - return when ( this ) { - SELECT -> "Select" - URL_TEST -> "UrlTest" - FALLBACK -> "Fallback" - DIRECT -> "Direct" - REJECT -> "Reject" - SHADOWSOCKS -> "Shadowsocks" - SNELL -> "Snell" - SOCKS5 -> "Socks5" - HTTP -> "HTTP" - VMESS -> "Vmess" - LOAD_BALANCE -> "LoadBalance" - UNKNOWN -> "Unknown" - } - } - } - - companion object { - private const val TYPE_SELECT = "Selector" - private const val TYPE_URL_TEST = "URLTest" - private const val TYPE_FALLBACK = "Fallback" - private const val TYPE_DIRECT = "Direct" - private const val TYPE_REJECT = "Reject" - private const val TYPE_SHADOWSOCKS = "Shadowsocks" - private const val TYPE_SNELL = "Snell" - private const val TYPE_SOCKS5 = "Socks5" - private const val TYPE_HTTP = "Http" - private const val TYPE_VMESS = "Vmess" - private const val TYPE_LOAD_BALANCE = "LoadBalance" - private const val TYPE_UNKNOWN = "Unknown" - - fun fromRawProxy(rawProxy: RawProxyPacket): ProxyPacket { - val hashed = rawProxy.proxies - .map { it.key to (it.key.hashCode() to it.value) }.toMap() - - val proxies = hashed.map { entry -> - val type = when ( entry.value.second.type ) { - TYPE_SELECT -> Type.SELECT - TYPE_URL_TEST -> Type.URL_TEST - TYPE_FALLBACK -> Type.FALLBACK - TYPE_DIRECT -> Type.DIRECT - TYPE_REJECT -> Type.REJECT - TYPE_SHADOWSOCKS -> Type.SHADOWSOCKS - TYPE_SNELL -> Type.SNELL - TYPE_SOCKS5 -> Type.SOCKS5 - TYPE_HTTP -> Type.HTTP - TYPE_VMESS -> Type.VMESS - TYPE_LOAD_BALANCE -> Type.LOAD_BALANCE - TYPE_UNKNOWN -> Type.UNKNOWN - else -> Type.UNKNOWN - } - - val now = hashed[entry.value.second.now]?.first ?: 0 - val all = entry.value.second.all.mapNotNull { hashed[it]?.first } - val delay = entry.value.second.history.firstOrNull()?.delay ?: 0 - - entry.value.first to Proxy(entry.key, type, now, all, delay) - } - - return ProxyPacket(rawProxy.mode, proxies.toMap()) - } - - @JvmField - val CREATOR = object: Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProxyPacket { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt deleted file mode 100644 index ac581ca..0000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -@Serializable -data class RawProxyPacket(val mode: String, val proxies: Map) { - @Serializable - data class RawProxy(val type: String, - val all: List = emptyList(), - val now: String = "", - val history: List = emptyList()) { - @Serializable - data class History(val delay: Long) - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt deleted file mode 100644 index ac8157e..0000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -class SetProxyPacket { - @Serializable - data class Request(val key: String, val value: String) - @Serializable - data class Response(val error: String) -} \ No newline at end of file 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 deleted file mode 100644 index 91ea122..0000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt +++ /dev/null @@ -1,11 +0,0 @@ -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/core/model/Packet.aidl b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl index 515a8d4..ba5c225 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl @@ -1,4 +1,4 @@ package com.github.kr328.clash.core.model; -parcelable ProxyPacket; -parcelable GeneralPacket; \ No newline at end of file +parcelable CompressedProxyList; +parcelable General; \ No newline at end of file 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 8b2c4ba..98166b3 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 @@ -23,7 +23,7 @@ interface IClashService { void stop(); // Query - ProxyPacket queryAllProxies(); - GeneralPacket queryGeneral(); + CompressedProxyList queryAllProxies(); + General queryGeneral(); void startUrlTest(in String[] proxies, IUrlTestCallback callback); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt new file mode 100644 index 0000000..b9fd4ba --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt @@ -0,0 +1,37 @@ +package com.github.kr328.clash.service + +import android.content.Context +import com.github.kr328.clash.core.event.* + +class ClashEventBridge(val service: ClashService): ClashProfileService.Master, + ClashEventService.Master, ClashEventPuller.Master { + val eventService: ClashEventService = ClashEventService(this) + + override fun preformProfileChanged() { + eventService.performProfileChangedEvent(ProfileChangedEvent()) + } + + override fun acquireEvent(event: Int) { + service.acquireEvent(event) + } + + override fun releaseEvent(event: Int) { + service.releaseEvent(event) + } + + fun onProcessChanged(event: ProcessEvent) { + eventService.performProcessEvent(event) + } + + override fun onLogPulled(event: LogEvent) { + eventService.performLogEvent(event) + } + + override fun onSpeedPulled(event: SpeedEvent) { + eventService.performSpeedEvent(event) + } + + override fun onBandwidthPulled(event: BandwidthEvent) { + eventService.performBandwidthEvent(event) + } +} \ No newline at end of file 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 7ff9d13..3b45749 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 @@ -5,106 +5,40 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.* -import bridge.Bridge -import com.github.kr328.clash.callback.IUrlTestCallback +import android.os.Binder +import android.os.IBinder +import android.os.IInterface import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.model.GeneralPacket -import com.github.kr328.clash.core.model.ProxyPacket import com.github.kr328.clash.core.utils.Log -import java.io.File -import java.io.IOException import java.util.concurrent.Executors -import javax.xml.transform.SourceLocator -import kotlin.concurrent.thread -class ClashService : Service(), IClashEventObserver, ClashEventService.Master, - ClashProfileService.Master, ClashEventPuller.Master { +class ClashService : Service(), IClashEventObserver { private val executor = Executors.newSingleThreadExecutor() - private val eventService = ClashEventService(this) - private val profileService = ClashProfileService(this, this) - private val settingService = ClashSettingService(this) + private val instance: ClashServiceImpl by lazy { + ClashServiceImpl(this) + } - private var processStatus = ProcessEvent.STOPPED + val events: ClashEventService + get() = instance.eventService + val clash: Clash + get() = instance.clash //private lateinit var puller: ClashEventPuller private lateinit var notification: ClashNotification - private val clashService = object : IClashService.Stub() { - override fun setSelectProxy(proxy: String?, selected: String?) { - require(proxy != null && selected != null) - - try { - this@ClashService.profileService.setCurrentProfileProxy(proxy, selected) - } catch (e: IOException) { - Log.w("Set proxy failure", e) - - this@ClashService.eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, e.toString()) - ) - } - } - - override fun queryGeneral(): GeneralPacket { - return GeneralPacket(GeneralPacket.Ports(0, 0, 0, 0), GeneralPacket.Mode.DIRECT) - } - - override fun queryAllProxies(): ProxyPacket { - return ProxyPacket("Direct", emptyMap()) - } - - override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { - require(proxies != null && callback != null) - } - - override fun start() { - if ( processStatus == ProcessEvent.STARTED ) - return - - processStatus = ProcessEvent.STARTED - - this@ClashService.eventService.performProcessEvent(processStatus) - } - - override fun stop() { - if ( processStatus == ProcessEvent.STOPPED ) - return - - processStatus = ProcessEvent.STOPPED - - this@ClashService.eventService.performProcessEvent(processStatus) - } - - override fun getEventService(): IClashEventService { - return this@ClashService.eventService - } - - override fun getProfileService(): IClashProfileService { - return this@ClashService.profileService - } - - override fun getSettingService(): IClashSettingService { - return this@ClashService.settingService - } - - override fun getCurrentProcessStatus(): ProcessEvent { - return processStatus - } - } - private val screenReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intent.ACTION_SCREEN_ON -> - eventService.registerEventObserver( + instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, intArrayOf(Event.EVENT_SPEED) ) Intent.ACTION_SCREEN_OFF -> - eventService.registerEventObserver( + instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, intArrayOf() @@ -113,27 +47,32 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } } - override fun acquireEvent(event: Int) { - if (processStatus == ProcessEvent.STOPPED) + private fun onClashProcessChanged(event: ProcessEvent) { + instance.eventService.performProcessEvent(event) + } + + fun acquireEvent(event: Int) { + if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) return + } - override fun releaseEvent(event: Int) { - if (processStatus == ProcessEvent.STOPPED) + fun releaseEvent(event: Int) { + if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) return + + } override fun onCreate() { super.onCreate() - Bridge.init(filesDir.resolve("clash").absolutePath) - //puller = ClashEventPuller(clash, this) notification = ClashNotification(this) - eventService.registerEventObserver( + instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, intArrayOf(Event.EVENT_SPEED) @@ -148,20 +87,19 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - clashService.start() + instance.clash.start() return START_NOT_STICKY } override fun onBind(intent: Intent?): IBinder? { - return clashService + return instance } override fun onDestroy() { - clashService.stop() + instance.clash.stop() executor.shutdown() - eventService.shutdown() unregisterReceiver(screenReceiver) @@ -179,17 +117,15 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, notification.show() - eventService.recastEventRequirement() + instance.eventService.recastEventRequirement() } ProcessEvent.STOPPED -> { - eventService.performSpeedEvent(SpeedEvent(0, 0)) - eventService.performBandwidthEvent(BandwidthEvent(0)) + instance.eventService.performSpeedEvent(SpeedEvent(0, 0)) + instance.eventService.performBandwidthEvent(BandwidthEvent(0)) notification.cancel() stopSelf() - - Bridge.loadProfileDefault() } } @@ -198,28 +134,28 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, private fun reloadProfile() { executor.submit { - if ( processStatus != ProcessEvent.STARTED) + if ( clash.getCurrentProcessStatus() != ProcessEvent.STARTED) return@submit - val active = profileService.queryActiveProfile() + val active = instance.profileService.queryActiveProfile() if (active == null) { - eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, "No profile activated")) - clashService.stop() + sendError(ErrorEvent.Type.PROFILE_LOAD, "No profile activated") + clash.stop() return@submit } Log.i("Loading profile ${active.file}") try { - Bridge.loadProfileFile(active.file) + clash.loadProfile(active.file) notification.setProfile(active.name) - eventService.performProfileReloadEvent(ProfileReloadEvent()) + events.performProfileReloadEvent(ProfileReloadEvent()) } catch (e: Exception) { - clashService.stop() - eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, e.message ?: "Unknown")) + clash.stop() + sendError(ErrorEvent.Type.PROFILE_LOAD, e.message) Log.w("Load profile failure", e) } } @@ -233,22 +169,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, notification.setSpeed(event?.up ?: 0, event?.down ?: 0) } - override fun preformProfileChanged() { - eventService.performProfileChangedEvent(ProfileChangedEvent()) - } - - override fun onLogPulled(event: LogEvent) { - eventService.performLogEvent(event) - } - - override fun onSpeedPulled(event: SpeedEvent) { - eventService.performSpeedEvent(event) - } - - override fun onBandwidthPulled(event: BandwidthEvent) { - eventService.performBandwidthEvent(event) - } - override fun onBandwidthEvent(event: BandwidthEvent?) {} override fun onLogEvent(event: LogEvent?) {} override fun onErrorEvent(event: ErrorEvent?) {} @@ -257,4 +177,8 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, return this@ClashService } } + + private fun sendError(type: ErrorEvent.Type, message: String?) { + events.performErrorEvent(ErrorEvent(type, message ?: "Unknown")) + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt new file mode 100644 index 0000000..33e7569 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt @@ -0,0 +1,88 @@ +package com.github.kr328.clash.service + +import com.github.kr328.clash.callback.IUrlTestCallback +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.event.ErrorEvent +import com.github.kr328.clash.core.event.ProcessEvent +import com.github.kr328.clash.core.model.CompressedProxyList +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.compress +import com.github.kr328.clash.core.utils.Log +import java.io.IOException + +class ClashServiceImpl(val clashService: ClashService) : IClashService.Stub() { + private val bridge: ClashEventBridge = ClashEventBridge(clashService) + + val clash: Clash = Clash(clashService, bridge::onProcessChanged) + val profileService = ClashProfileService(clashService, bridge) + val settingService = ClashSettingService(clashService) + val eventService: ClashEventService + get() = bridge.eventService + + override fun setSelectProxy(proxy: String?, selected: String?) { + require(proxy != null && selected != null) + + try { + profileService.setCurrentProfileProxy(proxy, selected) + } catch (e: IOException) { + Log.w("Set proxy failure", e) + + eventService.performErrorEvent( + ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, e.toString()) + ) + } + } + + override fun queryGeneral(): General { + return clash.queryGeneral() + } + + override fun queryAllProxies(): CompressedProxyList { + return try { + clash.queryProxies().compress() + } + catch (e: Exception) { + Log.w("Query proxies", e) + + eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.QUERY_PROXY_FAILURE, e.message ?: "Unknown")) + + CompressedProxyList(emptyMap(), emptyList()) + } + } + + override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { + require(proxies != null && callback != null) + } + + override fun start() { + clash.start() + } + + override fun stop() { + clash.stop() + } + + override fun getEventService(): IClashEventService { + return eventService + } + + override fun getProfileService(): IClashProfileService { + return profileService + } + + override fun getSettingService(): IClashSettingService { + return settingService + } + + override fun getCurrentProcessStatus(): ProcessEvent { + return clash.getCurrentProcessStatus() + } + + fun getClashInstance(): Clash { + return clash + } + + fun shutdown() { + eventService.shutdown() + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index d3c6063..f8fbc50 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -143,6 +143,8 @@ class TunService : VpnService(), IClashEventObserver { private fun Builder.addBypassPrivateRoute(): Builder { // IPv4 if ( settings.isBypassPrivateNetwork ) { + Log.i("Bypass Private Network") + resources.getStringArray(R.array.bypass_private_route).forEach { val address = it.split("/") addRoute(address[0], address[1].toInt())