Refactor the parser
This commit is contained in:
@@ -3,7 +3,6 @@ package com.v2ray.ang.util
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonPrimitive
|
||||
@@ -11,20 +10,18 @@ import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.PROTOCOL_HTTP
|
||||
import com.v2ray.ang.AppConfig.PROTOCOL_HTTPS
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
|
||||
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.dto.*
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY
|
||||
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.removeWhiteSpace
|
||||
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
|
||||
import com.v2ray.ang.util.fmt.ShadowsocksFmt
|
||||
import com.v2ray.ang.util.fmt.SocksFmt
|
||||
import com.v2ray.ang.util.fmt.TrojanFmt
|
||||
import com.v2ray.ang.util.fmt.VlessFmt
|
||||
import com.v2ray.ang.util.fmt.VmessFmt
|
||||
import com.v2ray.ang.util.fmt.WireguardFmt
|
||||
import java.lang.reflect.Type
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
|
||||
object AngConfigManager {
|
||||
@@ -219,250 +216,28 @@ object AngConfigManager {
|
||||
|
||||
//maybe Subscription
|
||||
if (TextUtils.isEmpty(subid)
|
||||
&& (str.startsWith(PROTOCOL_HTTP) || str.startsWith(PROTOCOL_HTTPS))) {
|
||||
&& (str.startsWith(PROTOCOL_HTTP) || str.startsWith(PROTOCOL_HTTPS))
|
||||
) {
|
||||
MmkvManager.importUrlAsSubscription(str)
|
||||
return 0
|
||||
}
|
||||
|
||||
var config: ServerConfig? = null
|
||||
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||
if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
||||
config = ServerConfig.create(EConfigType.VMESS)
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return -1
|
||||
|
||||
|
||||
if (!tryParseNewVmess(str, config, allowInsecure)) {
|
||||
if (str.indexOf("?") > 0) {
|
||||
if (!tryResolveVmess4Kitsunebi(str, config)) {
|
||||
return R.string.toast_incorrect_protocol
|
||||
}
|
||||
} else {
|
||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||
result = Utils.decode(result)
|
||||
if (TextUtils.isEmpty(result)) {
|
||||
return R.string.toast_decoding_failed
|
||||
}
|
||||
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
|
||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
||||
if (TextUtils.isEmpty(vmessQRCode.add)
|
||||
|| TextUtils.isEmpty(vmessQRCode.port)
|
||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||
) {
|
||||
return R.string.toast_incorrect_protocol
|
||||
}
|
||||
|
||||
config.remarks = vmessQRCode.ps
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = vmessQRCode.add
|
||||
vnext.port = Utils.parseInt(vmessQRCode.port)
|
||||
vnext.users[0].id = vmessQRCode.id
|
||||
vnext.users[0].security =
|
||||
if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
|
||||
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
|
||||
}
|
||||
val sni = streamSetting.populateTransportSettings(
|
||||
vmessQRCode.net,
|
||||
vmessQRCode.type,
|
||||
vmessQRCode.host,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.host,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.type,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.host
|
||||
)
|
||||
|
||||
val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint
|
||||
streamSetting.populateTlsSettings(
|
||||
vmessQRCode.tls, allowInsecure,
|
||||
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
|
||||
fingerprint, vmessQRCode.alpn, null, null, null
|
||||
)
|
||||
}
|
||||
}
|
||||
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
|
||||
VmessFmt.parseVmess(str)
|
||||
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
|
||||
config = ServerConfig.create(EConfigType.SHADOWSOCKS)
|
||||
if (!tryResolveResolveSip002(str, config)) {
|
||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("#")
|
||||
if (indexSplit > 0) {
|
||||
try {
|
||||
config.remarks =
|
||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
|
||||
//part decode
|
||||
val indexS = result.indexOf("@")
|
||||
result = if (indexS > 0) {
|
||||
Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||
indexS,
|
||||
result.length
|
||||
)
|
||||
} else {
|
||||
Utils.decode(result)
|
||||
}
|
||||
|
||||
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
||||
val match = legacyPattern.matchEntire(result)
|
||||
?: return R.string.toast_incorrect_protocol
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
||||
server.port = match.groupValues[4].toInt()
|
||||
server.password = match.groupValues[2]
|
||||
server.method = match.groupValues[1].lowercase()
|
||||
}
|
||||
}
|
||||
ShadowsocksFmt.parseShadowsocks(str)
|
||||
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
|
||||
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("#")
|
||||
config = ServerConfig.create(EConfigType.SOCKS)
|
||||
if (indexSplit > 0) {
|
||||
try {
|
||||
config.remarks =
|
||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
|
||||
//part decode
|
||||
val indexS = result.indexOf("@")
|
||||
if (indexS > 0) {
|
||||
result = Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||
indexS,
|
||||
result.length
|
||||
)
|
||||
} else {
|
||||
result = Utils.decode(result)
|
||||
}
|
||||
|
||||
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
|
||||
val match =
|
||||
legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
||||
server.port = match.groupValues[4].toInt()
|
||||
val socksUsersBean =
|
||||
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = match.groupValues[1]
|
||||
socksUsersBean.pass = match.groupValues[2]
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
SocksFmt.parseSocks(str)
|
||||
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config = ServerConfig.create(EConfigType.TROJAN)
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
|
||||
var flow = ""
|
||||
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
|
||||
if (uri.rawQuery != null) {
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||
queryParam["type"] ?: "tcp",
|
||||
queryParam["headerType"],
|
||||
queryParam["host"],
|
||||
queryParam["path"],
|
||||
queryParam["seed"],
|
||||
queryParam["quicSecurity"],
|
||||
queryParam["key"],
|
||||
queryParam["mode"],
|
||||
queryParam["serviceName"],
|
||||
queryParam["authority"]
|
||||
)
|
||||
fingerprint = queryParam["fp"] ?: ""
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
queryParam["security"] ?: TLS,
|
||||
allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"],
|
||||
null, null, null
|
||||
)
|
||||
flow = queryParam["flow"] ?: ""
|
||||
} else {
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
TLS, allowInsecure, "",
|
||||
fingerprint, null, null, null, null
|
||||
)
|
||||
}
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = uri.idnHost
|
||||
server.port = uri.port
|
||||
server.password = uri.userInfo
|
||||
server.flow = flow
|
||||
}
|
||||
TrojanFmt.parseTrojan(str)
|
||||
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
config = ServerConfig.create(EConfigType.VLESS)
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return -1
|
||||
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = uri.idnHost
|
||||
vnext.port = uri.port
|
||||
vnext.users[0].id = uri.userInfo
|
||||
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
|
||||
vnext.users[0].flow = queryParam["flow"] ?: ""
|
||||
}
|
||||
|
||||
val sni = streamSetting.populateTransportSettings(
|
||||
queryParam["type"] ?: "tcp",
|
||||
queryParam["headerType"],
|
||||
queryParam["host"],
|
||||
queryParam["path"],
|
||||
queryParam["seed"],
|
||||
queryParam["quicSecurity"],
|
||||
queryParam["key"],
|
||||
queryParam["mode"],
|
||||
queryParam["serviceName"],
|
||||
queryParam["authority"]
|
||||
)
|
||||
streamSetting.populateTlsSettings(
|
||||
queryParam["security"] ?: "",
|
||||
allowInsecure,
|
||||
queryParam["sni"] ?: sni,
|
||||
queryParam["fp"] ?: "",
|
||||
queryParam["alpn"],
|
||||
queryParam["pbk"] ?: "",
|
||||
queryParam["sid"] ?: "",
|
||||
queryParam["spx"] ?: ""
|
||||
)
|
||||
VlessFmt.parseVless(str)
|
||||
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config = ServerConfig.create(EConfigType.WIREGUARD)
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
|
||||
if (uri.rawQuery != null) {
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
config.outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = uri.userInfo
|
||||
wireguard.address =
|
||||
(queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
|
||||
.split(",")
|
||||
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
|
||||
wireguard.peers?.get(0)?.endpoint = "${uri.idnHost}:${uri.port}"
|
||||
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: WIREGUARD_LOCAL_MTU)
|
||||
wireguard.reserved =
|
||||
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
|
||||
.map { it.toInt() }
|
||||
}
|
||||
}
|
||||
WireguardFmt.parseWireguard(str)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
return R.string.toast_incorrect_protocol
|
||||
}
|
||||
@@ -485,171 +260,6 @@ object AngConfigManager {
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun tryParseNewVmess(
|
||||
uriString: String,
|
||||
config: ServerConfig,
|
||||
allowInsecure: Boolean
|
||||
): Boolean {
|
||||
return runCatching {
|
||||
val uri = URI(Utils.fixIllegalUrl(uriString))
|
||||
check(uri.scheme == "vmess")
|
||||
val (_, protocol, tlsStr, uuid, alterId) =
|
||||
Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
|
||||
.matchEntire(uri.userInfo)?.groupValues
|
||||
?: error("parse user info fail.")
|
||||
val tls = tlsStr.isNotBlank()
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return false
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = uri.idnHost
|
||||
vnext.port = uri.port
|
||||
vnext.users[0].id = uuid
|
||||
vnext.users[0].security = DEFAULT_SECURITY
|
||||
vnext.users[0].alterId = alterId.toInt()
|
||||
}
|
||||
var fingerprint = streamSetting.tlsSettings?.fingerprint
|
||||
val sni = streamSetting.populateTransportSettings(protocol,
|
||||
queryParam["type"],
|
||||
queryParam["host"]?.split("|")?.get(0) ?: "",
|
||||
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "",
|
||||
queryParam["seed"],
|
||||
queryParam["security"],
|
||||
queryParam["key"],
|
||||
queryParam["mode"],
|
||||
queryParam["serviceName"],
|
||||
queryParam["authority"])
|
||||
streamSetting.populateTlsSettings(
|
||||
if (tls) TLS else "", allowInsecure, sni, fingerprint, null,
|
||||
null, null, null
|
||||
)
|
||||
true
|
||||
}.getOrElse { false }
|
||||
}
|
||||
|
||||
private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean {
|
||||
|
||||
var result = server.replace(EConfigType.VMESS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("?")
|
||||
if (indexSplit > 0) {
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
result = Utils.decode(result)
|
||||
|
||||
val arr1 = result.split('@')
|
||||
if (arr1.count() != 2) {
|
||||
return false
|
||||
}
|
||||
val arr21 = arr1[0].split(':')
|
||||
val arr22 = arr1[1].split(':')
|
||||
if (arr21.count() != 2) {
|
||||
return false
|
||||
}
|
||||
|
||||
config.remarks = "Alien"
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = arr22[0]
|
||||
vnext.port = Utils.parseInt(arr22[1])
|
||||
vnext.users[0].id = arr21[1]
|
||||
vnext.users[0].security = arr21[0]
|
||||
vnext.users[0].alterId = 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
|
||||
try {
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
|
||||
val method: String
|
||||
val password: String
|
||||
if (uri.userInfo.contains(":")) {
|
||||
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
|
||||
if (arrUserInfo.count() != 2) {
|
||||
return false
|
||||
}
|
||||
method = arrUserInfo[0]
|
||||
password = Utils.urlDecode(arrUserInfo[1])
|
||||
} else {
|
||||
val base64Decode = Utils.decode(uri.userInfo)
|
||||
val arrUserInfo = base64Decode.split(":").map { it.trim() }
|
||||
if (arrUserInfo.count() < 2) {
|
||||
return false
|
||||
}
|
||||
method = arrUserInfo[0]
|
||||
password = base64Decode.substringAfter(":")
|
||||
}
|
||||
|
||||
val query = Utils.urlDecode(uri.query ?: "")
|
||||
if (query != "") {
|
||||
val queryPairs = HashMap<String, String>()
|
||||
val pairs = query.split(";")
|
||||
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
|
||||
for (pair in pairs) {
|
||||
val idx = pair.indexOf("=")
|
||||
if (idx == -1) {
|
||||
queryPairs[Utils.urlDecode(pair)] = "";
|
||||
} else {
|
||||
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
|
||||
Utils.urlDecode(pair.substring(idx + 1))
|
||||
}
|
||||
}
|
||||
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
|
||||
var sni: String? = ""
|
||||
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
|
||||
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||
"tcp",
|
||||
"http",
|
||||
queryPairs["obfs-host"],
|
||||
queryPairs["path"],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else if (queryPairs["plugin"] == "v2ray-plugin") {
|
||||
var network = "ws";
|
||||
if (queryPairs["mode"] == "quic") {
|
||||
network = "quic";
|
||||
}
|
||||
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||
network,
|
||||
null,
|
||||
queryPairs["host"],
|
||||
queryPairs["path"],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
if ("tls" in queryPairs) {
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
"tls", false, sni ?: "", null, null, null, null, null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = uri.idnHost
|
||||
server.port = uri.port
|
||||
server.password = password
|
||||
server.method = method
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, e.toString())
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* share config
|
||||
@@ -657,211 +267,15 @@ object AngConfigManager {
|
||||
private fun shareConfig(guid: String): String {
|
||||
try {
|
||||
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
val streamSetting =
|
||||
outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||
if (config.configType != EConfigType.WIREGUARD) {
|
||||
if (outbound.streamSettings == null) return ""
|
||||
}
|
||||
|
||||
return config.configType.protocolScheme + when (config.configType) {
|
||||
EConfigType.VMESS -> {
|
||||
val vmessQRCode = VmessQRCode()
|
||||
vmessQRCode.v = "2"
|
||||
vmessQRCode.ps = config.remarks
|
||||
vmessQRCode.add = outbound.getServerAddress().orEmpty()
|
||||
vmessQRCode.port = outbound.getServerPort().toString()
|
||||
vmessQRCode.id = outbound.getPassword().orEmpty()
|
||||
vmessQRCode.aid =
|
||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
|
||||
vmessQRCode.scy =
|
||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
|
||||
vmessQRCode.net = streamSetting.network
|
||||
vmessQRCode.tls = streamSetting.security
|
||||
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
|
||||
vmessQRCode.alpn =
|
||||
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString())
|
||||
.orEmpty()
|
||||
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
|
||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||
vmessQRCode.type = transportDetails[0]
|
||||
vmessQRCode.host = transportDetails[1]
|
||||
vmessQRCode.path = transportDetails[2]
|
||||
}
|
||||
val json = Gson().toJson(vmessQRCode)
|
||||
Utils.encode(json)
|
||||
}
|
||||
|
||||
EConfigType.VMESS -> VmessFmt.toUri(config)
|
||||
EConfigType.CUSTOM -> ""
|
||||
|
||||
EConfigType.SHADOWSOCKS -> {
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val pw =
|
||||
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
pw,
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
url + remark
|
||||
}
|
||||
|
||||
EConfigType.SOCKS -> {
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val pw =
|
||||
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
|
||||
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
|
||||
else
|
||||
":"
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
Utils.encode(pw),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
url + remark
|
||||
}
|
||||
|
||||
EConfigType.VLESS,
|
||||
EConfigType.TROJAN -> {
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
|
||||
val dicQuery = HashMap<String, String>()
|
||||
if (config.configType == EConfigType.VLESS) {
|
||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
|
||||
if (!TextUtils.isEmpty(it)) {
|
||||
dicQuery["flow"] = it
|
||||
}
|
||||
}
|
||||
dicQuery["encryption"] =
|
||||
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
|
||||
else outbound.getSecurityEncryption().orEmpty()
|
||||
} else if (config.configType == EConfigType.TROJAN) {
|
||||
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
|
||||
if (!TextUtils.isEmpty(it)) {
|
||||
dicQuery["flow"] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||
(streamSetting.tlsSettings
|
||||
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
||||
dicQuery["sni"] = tlsSetting.serverName
|
||||
}
|
||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||
dicQuery["alpn"] =
|
||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||
dicQuery["fp"] = tlsSetting.fingerprint!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||
dicQuery["pbk"] = tlsSetting.publicKey!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||
dicQuery["sid"] = tlsSetting.shortId!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!)
|
||||
}
|
||||
}
|
||||
dicQuery["type"] =
|
||||
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
||||
|
||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||
when (streamSetting.network) {
|
||||
"tcp" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"ws", "httpupgrade" -> {
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"http", "h2" -> {
|
||||
dicQuery["type"] = "http"
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
||||
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
dicQuery["mode"] = transportDetails[0]
|
||||
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
||||
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
val query = "?" + dicQuery.toList().joinToString(
|
||||
separator = "&",
|
||||
transform = { it.first + "=" + it.second })
|
||||
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
outbound.getPassword(),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
url + query + remark
|
||||
}
|
||||
|
||||
EConfigType.WIREGUARD -> {
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
|
||||
val dicQuery = HashMap<String, String>()
|
||||
dicQuery["publickey"] =
|
||||
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
|
||||
if (outbound.settings?.reserved != null) {
|
||||
dicQuery["reserved"] = Utils.urlEncode(
|
||||
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
|
||||
.toString()
|
||||
)
|
||||
}
|
||||
dicQuery["address"] = Utils.urlEncode(
|
||||
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
|
||||
.toString()
|
||||
)
|
||||
if (outbound.settings?.mtu != null) {
|
||||
dicQuery["mtu"] = outbound.settings?.mtu.toString()
|
||||
}
|
||||
val query = "?" + dicQuery.toList().joinToString(
|
||||
separator = "&",
|
||||
transform = { it.first + "=" + it.second })
|
||||
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
Utils.urlEncode(outbound.getPassword().toString()),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
url + query + remark
|
||||
}
|
||||
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
|
||||
EConfigType.SOCKS -> SocksFmt.toUri(config)
|
||||
EConfigType.VLESS-> VlessFmt.toUri(config)
|
||||
EConfigType.TROJAN-> TrojanFmt.toUri(config)
|
||||
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -1023,7 +437,7 @@ object AngConfigManager {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun appendCustomConfigServer(server: String?, subid: String): Int {
|
||||
fun appendCustomConfigServer(server: String?, subid: String): Int {
|
||||
if (server == null) {
|
||||
return 0
|
||||
}
|
||||
@@ -1052,7 +466,8 @@ object AngConfigManager {
|
||||
var count = 0
|
||||
for (srv in serverList) {
|
||||
val config = ServerConfig.create(EConfigType.CUSTOM)
|
||||
config.fullConfig = Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
|
||||
config.fullConfig =
|
||||
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
|
||||
config.remarks = config.fullConfig?.remarks
|
||||
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
|
||||
.toString())
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.v2ray.ang.util.fmt
|
||||
|
||||
import android.util.Log
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object ShadowsocksFmt {
|
||||
fun parseShadowsocks(str: String): ServerConfig? {
|
||||
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
|
||||
if (!tryResolveResolveSip002(str, config)) {
|
||||
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("#")
|
||||
if (indexSplit > 0) {
|
||||
try {
|
||||
config.remarks =
|
||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
|
||||
//part decode
|
||||
val indexS = result.indexOf("@")
|
||||
result = if (indexS > 0) {
|
||||
Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||
indexS,
|
||||
result.length
|
||||
)
|
||||
} else {
|
||||
Utils.decode(result)
|
||||
}
|
||||
|
||||
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
|
||||
val match = legacyPattern.matchEntire(result)
|
||||
?: return null
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
||||
server.port = match.groupValues[4].toInt()
|
||||
server.password = match.groupValues[2]
|
||||
server.method = match.groupValues[1].lowercase()
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ServerConfig): String {
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val pw =
|
||||
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
pw,
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
return url + remark
|
||||
}
|
||||
|
||||
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
|
||||
try {
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
|
||||
val method: String
|
||||
val password: String
|
||||
if (uri.userInfo.contains(":")) {
|
||||
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
|
||||
if (arrUserInfo.count() != 2) {
|
||||
return false
|
||||
}
|
||||
method = arrUserInfo[0]
|
||||
password = Utils.urlDecode(arrUserInfo[1])
|
||||
} else {
|
||||
val base64Decode = Utils.decode(uri.userInfo)
|
||||
val arrUserInfo = base64Decode.split(":").map { it.trim() }
|
||||
if (arrUserInfo.count() < 2) {
|
||||
return false
|
||||
}
|
||||
method = arrUserInfo[0]
|
||||
password = base64Decode.substringAfter(":")
|
||||
}
|
||||
|
||||
val query = Utils.urlDecode(uri.query ?: "")
|
||||
if (query != "") {
|
||||
val queryPairs = HashMap<String, String>()
|
||||
val pairs = query.split(";")
|
||||
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
|
||||
for (pair in pairs) {
|
||||
val idx = pair.indexOf("=")
|
||||
if (idx == -1) {
|
||||
queryPairs[Utils.urlDecode(pair)] = "";
|
||||
} else {
|
||||
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
|
||||
Utils.urlDecode(pair.substring(idx + 1))
|
||||
}
|
||||
}
|
||||
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
|
||||
var sni: String? = ""
|
||||
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
|
||||
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||
"tcp",
|
||||
"http",
|
||||
queryPairs["obfs-host"],
|
||||
queryPairs["path"],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else if (queryPairs["plugin"] == "v2ray-plugin") {
|
||||
var network = "ws";
|
||||
if (queryPairs["mode"] == "quic") {
|
||||
network = "quic";
|
||||
}
|
||||
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||
network,
|
||||
null,
|
||||
queryPairs["host"],
|
||||
queryPairs["path"],
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
if ("tls" in queryPairs) {
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
"tls", false, sni ?: "", null, null, null, null, null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = uri.idnHost
|
||||
server.port = uri.port
|
||||
server.password = password
|
||||
server.method = method
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, e.toString())
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.v2ray.ang.util.fmt
|
||||
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.util.Utils
|
||||
|
||||
object SocksFmt {
|
||||
fun parseSocks(str: String): ServerConfig? {
|
||||
val config = ServerConfig.create(EConfigType.SOCKS)
|
||||
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("#")
|
||||
|
||||
if (indexSplit > 0) {
|
||||
try {
|
||||
config.remarks =
|
||||
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
|
||||
//part decode
|
||||
val indexS = result.indexOf("@")
|
||||
if (indexS > 0) {
|
||||
result = Utils.decode(result.substring(0, indexS)) + result.substring(
|
||||
indexS,
|
||||
result.length
|
||||
)
|
||||
} else {
|
||||
result = Utils.decode(result)
|
||||
}
|
||||
|
||||
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
|
||||
val match =
|
||||
legacyPattern.matchEntire(result) ?: return null
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = match.groupValues[3].removeSurrounding("[", "]")
|
||||
server.port = match.groupValues[4].toInt()
|
||||
val socksUsersBean =
|
||||
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
|
||||
socksUsersBean.user = match.groupValues[1]
|
||||
socksUsersBean.pass = match.groupValues[2]
|
||||
server.users = listOf(socksUsersBean)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ServerConfig): String {
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val pw =
|
||||
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
|
||||
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
|
||||
else
|
||||
":"
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
Utils.encode(pw),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
return url + remark
|
||||
}
|
||||
}
|
||||
170
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt
Normal file
170
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/TrojanFmt.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
package com.v2ray.ang.util.fmt
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object TrojanFmt {
|
||||
private val settingsStorage by lazy {
|
||||
MMKV.mmkvWithID(
|
||||
MmkvManager.ID_SETTING,
|
||||
MMKV.MULTI_PROCESS_MODE
|
||||
)
|
||||
}
|
||||
|
||||
fun parseTrojan(str: String): ServerConfig? {
|
||||
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||
val config = ServerConfig.create(EConfigType.TROJAN)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
|
||||
var flow = ""
|
||||
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
|
||||
if (uri.rawQuery.isNullOrEmpty()) {
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
V2rayConfig.TLS, allowInsecure, "",
|
||||
fingerprint, null, null, null, null
|
||||
)
|
||||
} else {
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
|
||||
queryParam["type"] ?: "tcp",
|
||||
queryParam["headerType"],
|
||||
queryParam["host"],
|
||||
queryParam["path"],
|
||||
queryParam["seed"],
|
||||
queryParam["quicSecurity"],
|
||||
queryParam["key"],
|
||||
queryParam["mode"],
|
||||
queryParam["serviceName"],
|
||||
queryParam["authority"]
|
||||
)
|
||||
fingerprint = queryParam["fp"] ?: ""
|
||||
config.outboundBean?.streamSettings?.populateTlsSettings(
|
||||
queryParam["security"] ?: V2rayConfig.TLS,
|
||||
allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"],
|
||||
null, null, null
|
||||
)
|
||||
flow = queryParam["flow"] ?: ""
|
||||
}
|
||||
|
||||
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
|
||||
server.address = uri.idnHost
|
||||
server.port = uri.port
|
||||
server.password = uri.userInfo
|
||||
server.flow = flow
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ServerConfig): String {
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val dicQuery = HashMap<String, String>()
|
||||
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
|
||||
if (!TextUtils.isEmpty(it)) {
|
||||
dicQuery["flow"] = it
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||
(streamSetting.tlsSettings
|
||||
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
||||
dicQuery["sni"] = tlsSetting.serverName
|
||||
}
|
||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||
dicQuery["alpn"] =
|
||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||
dicQuery["fp"] = tlsSetting.fingerprint!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||
dicQuery["pbk"] = tlsSetting.publicKey!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||
dicQuery["sid"] = tlsSetting.shortId!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!)
|
||||
}
|
||||
}
|
||||
dicQuery["type"] =
|
||||
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
||||
|
||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||
when (streamSetting.network) {
|
||||
"tcp" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"ws", "httpupgrade" -> {
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"http", "h2" -> {
|
||||
dicQuery["type"] = "http"
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
||||
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
dicQuery["mode"] = transportDetails[0]
|
||||
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
||||
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
val query = "?" + dicQuery.toList().joinToString(
|
||||
separator = "&",
|
||||
transform = { it.first + "=" + it.second })
|
||||
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
outbound.getPassword(),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
return url + query + remark
|
||||
}
|
||||
}
|
||||
170
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt
Normal file
170
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VlessFmt.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
package com.v2ray.ang.util.fmt
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object VlessFmt {
|
||||
private val settingsStorage by lazy {
|
||||
MMKV.mmkvWithID(
|
||||
MmkvManager.ID_SETTING,
|
||||
MMKV.MULTI_PROCESS_MODE
|
||||
)
|
||||
}
|
||||
|
||||
fun parseVless(str: String): ServerConfig? {
|
||||
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||
val config = ServerConfig.create(EConfigType.VLESS)
|
||||
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
if (uri.rawQuery.isNullOrEmpty()) return null
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = uri.idnHost
|
||||
vnext.port = uri.port
|
||||
vnext.users[0].id = uri.userInfo
|
||||
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
|
||||
vnext.users[0].flow = queryParam["flow"] ?: ""
|
||||
}
|
||||
|
||||
val sni = streamSetting.populateTransportSettings(
|
||||
queryParam["type"] ?: "tcp",
|
||||
queryParam["headerType"],
|
||||
queryParam["host"],
|
||||
queryParam["path"],
|
||||
queryParam["seed"],
|
||||
queryParam["quicSecurity"],
|
||||
queryParam["key"],
|
||||
queryParam["mode"],
|
||||
queryParam["serviceName"],
|
||||
queryParam["authority"]
|
||||
)
|
||||
streamSetting.populateTlsSettings(
|
||||
queryParam["security"] ?: "",
|
||||
allowInsecure,
|
||||
queryParam["sni"] ?: sni,
|
||||
queryParam["fp"] ?: "",
|
||||
queryParam["alpn"],
|
||||
queryParam["pbk"] ?: "",
|
||||
queryParam["sid"] ?: "",
|
||||
queryParam["spx"] ?: ""
|
||||
)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ServerConfig): String {
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val dicQuery = HashMap<String, String>()
|
||||
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
|
||||
if (!TextUtils.isEmpty(it)) {
|
||||
dicQuery["flow"] = it
|
||||
}
|
||||
}
|
||||
dicQuery["encryption"] =
|
||||
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
|
||||
else outbound.getSecurityEncryption().orEmpty()
|
||||
|
||||
|
||||
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
|
||||
(streamSetting.tlsSettings
|
||||
?: streamSetting.realitySettings)?.let { tlsSetting ->
|
||||
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
|
||||
dicQuery["sni"] = tlsSetting.serverName
|
||||
}
|
||||
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
|
||||
dicQuery["alpn"] =
|
||||
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
|
||||
dicQuery["fp"] = tlsSetting.fingerprint!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
|
||||
dicQuery["pbk"] = tlsSetting.publicKey!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
|
||||
dicQuery["sid"] = tlsSetting.shortId!!
|
||||
}
|
||||
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
|
||||
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!)
|
||||
}
|
||||
}
|
||||
dicQuery["type"] =
|
||||
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
|
||||
|
||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||
when (streamSetting.network) {
|
||||
"tcp" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
}
|
||||
|
||||
"kcp" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"ws", "httpupgrade" -> {
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"http", "h2" -> {
|
||||
dicQuery["type"] = "http"
|
||||
if (!TextUtils.isEmpty(transportDetails[1])) {
|
||||
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
|
||||
}
|
||||
if (!TextUtils.isEmpty(transportDetails[2])) {
|
||||
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
|
||||
"quic" -> {
|
||||
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
|
||||
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
|
||||
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
|
||||
"grpc" -> {
|
||||
dicQuery["mode"] = transportDetails[0]
|
||||
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
|
||||
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
val query = "?" + dicQuery.toList().joinToString(
|
||||
separator = "&",
|
||||
transform = { it.first + "=" + it.second })
|
||||
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
outbound.getPassword(),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
return url + query + remark
|
||||
}
|
||||
}
|
||||
193
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt
Normal file
193
V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/fmt/VmessFmt.kt
Normal file
@@ -0,0 +1,193 @@
|
||||
package com.v2ray.ang.util.fmt
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import com.tencent.mmkv.MMKV
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.dto.V2rayConfig
|
||||
import com.v2ray.ang.dto.VmessQRCode
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.util.MmkvManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object VmessFmt {
|
||||
private val settingsStorage by lazy {
|
||||
MMKV.mmkvWithID(
|
||||
MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE
|
||||
)
|
||||
}
|
||||
|
||||
fun parseVmess(str: String): ServerConfig? {
|
||||
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
|
||||
val config = ServerConfig.create(EConfigType.VMESS)
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return null
|
||||
|
||||
if (!tryParseNewVmess(str, config, allowInsecure)) {
|
||||
if (str.indexOf("?") > 0) {
|
||||
if (!tryResolveVmess4Kitsunebi(str, config)) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
|
||||
result = Utils.decode(result)
|
||||
if (TextUtils.isEmpty(result)) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
|
||||
return null
|
||||
}
|
||||
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
|
||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
||||
if (TextUtils.isEmpty(vmessQRCode.add) || TextUtils.isEmpty(vmessQRCode.port) || TextUtils.isEmpty(
|
||||
vmessQRCode.id
|
||||
) || TextUtils.isEmpty(vmessQRCode.net)
|
||||
) {
|
||||
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
|
||||
return null
|
||||
}
|
||||
|
||||
config.remarks = vmessQRCode.ps
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = vmessQRCode.add
|
||||
vnext.port = Utils.parseInt(vmessQRCode.port)
|
||||
vnext.users[0].id = vmessQRCode.id
|
||||
vnext.users[0].security =
|
||||
if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy
|
||||
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
|
||||
}
|
||||
val sni = streamSetting.populateTransportSettings(
|
||||
vmessQRCode.net,
|
||||
vmessQRCode.type,
|
||||
vmessQRCode.host,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.host,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.type,
|
||||
vmessQRCode.path,
|
||||
vmessQRCode.host
|
||||
)
|
||||
|
||||
val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint
|
||||
streamSetting.populateTlsSettings(
|
||||
vmessQRCode.tls,
|
||||
allowInsecure,
|
||||
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
|
||||
fingerprint,
|
||||
vmessQRCode.alpn,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ServerConfig): String {
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
|
||||
|
||||
val vmessQRCode = VmessQRCode()
|
||||
vmessQRCode.v = "2"
|
||||
vmessQRCode.ps = config.remarks
|
||||
vmessQRCode.add = outbound.getServerAddress().orEmpty()
|
||||
vmessQRCode.port = outbound.getServerPort().toString()
|
||||
vmessQRCode.id = outbound.getPassword().orEmpty()
|
||||
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
|
||||
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
|
||||
vmessQRCode.net = streamSetting.network
|
||||
vmessQRCode.tls = streamSetting.security
|
||||
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
|
||||
vmessQRCode.alpn =
|
||||
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
|
||||
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
|
||||
outbound.getTransportSettingDetails()?.let { transportDetails ->
|
||||
vmessQRCode.type = transportDetails[0]
|
||||
vmessQRCode.host = transportDetails[1]
|
||||
vmessQRCode.path = transportDetails[2]
|
||||
}
|
||||
val json = Gson().toJson(vmessQRCode)
|
||||
return Utils.encode(json)
|
||||
}
|
||||
|
||||
private fun tryParseNewVmess(
|
||||
uriString: String, config: ServerConfig, allowInsecure: Boolean
|
||||
): Boolean {
|
||||
return runCatching {
|
||||
val uri = URI(Utils.fixIllegalUrl(uriString))
|
||||
check(uri.scheme == "vmess")
|
||||
val (_, protocol, tlsStr, uuid, alterId) = Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})").matchEntire(
|
||||
uri.userInfo
|
||||
)?.groupValues ?: error("parse user info fail.")
|
||||
val tls = tlsStr.isNotBlank()
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
val streamSetting = config.outboundBean?.streamSettings ?: return false
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = uri.idnHost
|
||||
vnext.port = uri.port
|
||||
vnext.users[0].id = uuid
|
||||
vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY
|
||||
vnext.users[0].alterId = alterId.toInt()
|
||||
}
|
||||
var fingerprint = streamSetting.tlsSettings?.fingerprint
|
||||
val sni = streamSetting.populateTransportSettings(protocol,
|
||||
queryParam["type"],
|
||||
queryParam["host"]?.split("|")?.get(0) ?: "",
|
||||
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "",
|
||||
queryParam["seed"],
|
||||
queryParam["security"],
|
||||
queryParam["key"],
|
||||
queryParam["mode"],
|
||||
queryParam["serviceName"],
|
||||
queryParam["authority"])
|
||||
streamSetting.populateTlsSettings(
|
||||
if (tls) V2rayConfig.TLS else "",
|
||||
allowInsecure,
|
||||
sni,
|
||||
fingerprint,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
true
|
||||
}.getOrElse { false }
|
||||
}
|
||||
|
||||
private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean {
|
||||
|
||||
var result = server.replace(EConfigType.VMESS.protocolScheme, "")
|
||||
val indexSplit = result.indexOf("?")
|
||||
if (indexSplit > 0) {
|
||||
result = result.substring(0, indexSplit)
|
||||
}
|
||||
result = Utils.decode(result)
|
||||
|
||||
val arr1 = result.split('@')
|
||||
if (arr1.count() != 2) {
|
||||
return false
|
||||
}
|
||||
val arr21 = arr1[0].split(':')
|
||||
val arr22 = arr1[1].split(':')
|
||||
if (arr21.count() != 2) {
|
||||
return false
|
||||
}
|
||||
|
||||
config.remarks = "Alien"
|
||||
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
|
||||
vnext.address = arr22[0]
|
||||
vnext.port = Utils.parseInt(arr22[1])
|
||||
vnext.users[0].id = arr21[1]
|
||||
vnext.users[0].security = arr21[0]
|
||||
vnext.users[0].alterId = 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.v2ray.ang.util.fmt
|
||||
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.dto.EConfigType
|
||||
import com.v2ray.ang.dto.ServerConfig
|
||||
import com.v2ray.ang.extension.idnHost
|
||||
import com.v2ray.ang.extension.removeWhiteSpace
|
||||
import com.v2ray.ang.util.Utils
|
||||
import java.net.URI
|
||||
|
||||
object WireguardFmt {
|
||||
fun parseWireguard(str: String): ServerConfig? {
|
||||
val config = ServerConfig.create(EConfigType.WIREGUARD)
|
||||
val uri = URI(Utils.fixIllegalUrl(str))
|
||||
config.remarks = Utils.urlDecode(uri.fragment ?: "")
|
||||
|
||||
if (uri.rawQuery != null) {
|
||||
val queryParam = uri.rawQuery.split("&")
|
||||
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
|
||||
|
||||
config.outboundBean?.settings?.let { wireguard ->
|
||||
wireguard.secretKey = uri.userInfo
|
||||
wireguard.address =
|
||||
(queryParam["address"]
|
||||
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
|
||||
.split(",")
|
||||
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
|
||||
wireguard.peers?.get(0)?.endpoint = "${uri.idnHost}:${uri.port}"
|
||||
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
|
||||
wireguard.reserved =
|
||||
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
|
||||
.map { it.toInt() }
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
fun toUri(config: ServerConfig): String {
|
||||
val outbound = config.getProxyOutbound() ?: return ""
|
||||
|
||||
val remark = "#" + Utils.urlEncode(config.remarks)
|
||||
val dicQuery = HashMap<String, String>()
|
||||
dicQuery["publickey"] =
|
||||
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
|
||||
if (outbound.settings?.reserved != null) {
|
||||
dicQuery["reserved"] = Utils.urlEncode(
|
||||
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
|
||||
.toString()
|
||||
)
|
||||
}
|
||||
dicQuery["address"] = Utils.urlEncode(
|
||||
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
|
||||
.toString()
|
||||
)
|
||||
if (outbound.settings?.mtu != null) {
|
||||
dicQuery["mtu"] = outbound.settings?.mtu.toString()
|
||||
}
|
||||
val query = "?" + dicQuery.toList().joinToString(
|
||||
separator = "&",
|
||||
transform = { it.first + "=" + it.second })
|
||||
|
||||
val url = String.format(
|
||||
"%s@%s:%s",
|
||||
Utils.urlEncode(outbound.getPassword().toString()),
|
||||
Utils.getIpv6Address(outbound.getServerAddress()!!),
|
||||
outbound.getServerPort()
|
||||
)
|
||||
return url + query + remark
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user