update share link

This commit is contained in:
arm64v8a 2023-03-21 16:11:29 +09:00
parent 6051efd5aa
commit 810058ccd4
10 changed files with 146 additions and 167 deletions

View File

@ -97,6 +97,7 @@ class TrafficLooper
if (proxy.config.selectorGroupId >= 0L) {
itemMain = TrafficUpdater.TrafficLooperData(tag = TAG_PROXY)
itemMainBase = TrafficUpdater.TrafficLooperData(tag = TAG_PROXY)
tags.add(TAG_PROXY)
}
//
trafficUpdater = TrafficUpdater(

View File

@ -20,7 +20,6 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
import io.nekohasekai.sagernet.fmt.socks.toUri
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
import io.nekohasekai.sagernet.fmt.trojan.toUri
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
import io.nekohasekai.sagernet.fmt.trojan_go.toUri
@ -236,23 +235,18 @@ data class ProxyEntity(
}
}
fun toLink(compact: Boolean = false): String? = with(requireBean()) {
fun toStdLink(compact: Boolean = false): String = with(requireBean()) {
when (this) {
is SOCKSBean -> toUri()
is HttpBean -> toUri()
is ShadowsocksBean -> toUri()
is VMessBean -> if (compact) toV2rayN() else toUri()
is TrojanBean -> toUri()
is VMessBean -> toUriVMessVLESSTrojan(false)
is TrojanBean -> toUriVMessVLESSTrojan(true)
is TrojanGoBean -> toUri()
is NaiveBean -> toUri()
is HysteriaBean -> toUri()
is SSHBean -> toUniversalLink()
is WireGuardBean -> toUniversalLink()
is TuicBean -> toUniversalLink()
is ShadowTLSBean -> toUniversalLink()
is ConfigBean -> toUniversalLink()
is NekoBean -> shareLink()
else -> null
else -> toUniversalLink()
}
}

View File

@ -1,7 +1,6 @@
package io.nekohasekai.sagernet.fmt.trojan
import io.nekohasekai.sagernet.fmt.v2ray.parseDuckSoft
import io.nekohasekai.sagernet.fmt.v2ray.toUri
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun parseTrojan(server: String): TrojanBean {
@ -17,7 +16,3 @@ fun parseTrojan(server: String): TrojanBean {
}
}
fun TrojanBean.toUri(): String {
return toUri(true).replace("vmess://", "trojan://")
}

View File

@ -1,5 +1,6 @@
package io.nekohasekai.sagernet.fmt.v2ray
import android.text.TextUtils
import com.google.gson.Gson
import io.nekohasekai.sagernet.fmt.http.HttpBean
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
@ -10,6 +11,24 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.json.JSONObject
data class VmessQRCode(
var v: String = "",
var ps: String = "",
var add: String = "",
var port: String = "",
var id: String = "",
var aid: String = "0",
var scy: String = "",
var net: String = "",
var type: String = "",
var host: String = "",
var path: String = "",
var tls: String = "",
var sni: String = "",
var alpn: String = "",
var fp: String = "",
)
fun StandardV2RayBean.isTLS(): Boolean {
return security == "tls"
}
@ -104,7 +123,6 @@ fun parseV2Ray(link: String): StandardV2RayBean {
}
// https://github.com/XTLS/Xray-core/issues/91
// NO allowInsecure
fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
serverAddress = url.host
serverPort = url.port
@ -116,27 +134,26 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
uuid = url.username
}
// not ducksoft fmt path
if (url.pathSegments.size > 1 || url.pathSegments[0].isNotBlank()) {
path = url.pathSegments.joinToString("/")
}
type = url.queryParameter("type") ?: "tcp"
if (type == "h2") type = "http"
security = url.queryParameter("security")
if (security == null) {
security = if (this is TrojanBean) {
"tls"
} else {
"none"
}
security = if (this is TrojanBean) "tls" else "none"
}
when (security) {
"tls" -> {
"tls", "reality" -> {
url.queryParameter("sni")?.let {
sni = it
}
url.queryParameter("alpn")?.let {
alpn = it
alpn = it.replace(",", "\n")
}
url.queryParameter("cert")?.let {
certificates = it
@ -144,6 +161,15 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
}
}
when (type) {
"tcp" -> {
// v2rayNG
if (url.queryParameter("headerType") == "http") {
url.queryParameter("host")?.let {
type = "http"
host = it
}
}
}
"http" -> {
url.queryParameter("host")?.let {
host = it
@ -251,63 +277,43 @@ fun parseV2RayN(link: String): VMessBean {
return parseCsvVMess(result)
}
val bean = VMessBean()
val json = JSONObject(result)
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
bean.serverAddress = json.getStr("add") ?: ""
bean.serverPort = json.getIntNya("port") ?: 1080
bean.encryption = json.getStr("scy") ?: ""
bean.uuid = json.getStr("id") ?: ""
bean.alterId = json.getIntNya("aid") ?: 0
bean.type = json.getStr("net") ?: ""
bean.host = json.getStr("host") ?: ""
bean.path = json.getStr("path") ?: ""
// 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)
) {
throw Exception("invalid VmessQRCode")
}
bean.serverAddress = vmessQRCode.add
bean.serverPort = vmessQRCode.port.toIntOrNull()
bean.encryption = vmessQRCode.scy
bean.uuid = vmessQRCode.id
bean.alterId = vmessQRCode.aid.toIntOrNull()
bean.type = vmessQRCode.type
bean.host = vmessQRCode.host
bean.path = vmessQRCode.path
val headerType = vmessQRCode.type
when (bean.type) {
"grpc" -> {
bean.path = bean.path
"tcp" -> {
if (headerType == "http") {
bean.type = "http"
}
}
}
bean.name = json.getStr("ps") ?: ""
bean.sni = json.getStr("sni") ?: bean.host
bean.security = json.getStr("tls") ?: "none"
bean.utlsFingerprint = json.getStr("fp") ?: ""
if (json.optInt("v", 2) < 2) {
when (bean.type) {
"ws" -> {
var path = ""
var host = ""
val lstParameter = bean.host.split(";")
if (lstParameter.isNotEmpty()) {
path = lstParameter[0].trim()
}
if (lstParameter.size > 1) {
path = lstParameter[0].trim()
host = lstParameter[1].trim()
}
bean.path = path
bean.host = host
}
"h2" -> {
var path = ""
var host = ""
val lstParameter = bean.host.split(";")
if (lstParameter.isNotEmpty()) {
path = lstParameter[0].trim()
}
if (lstParameter.size > 1) {
path = lstParameter[0].trim()
host = lstParameter[1].trim()
}
bean.path = path
bean.host = host
}
}
}
bean.name = vmessQRCode.ps
bean.security = vmessQRCode.tls
if (bean.security == "reality") bean.security = "tls"
bean.sni = vmessQRCode.sni
bean.alpn = vmessQRCode.alpn.replace(",", "\n")
bean.utlsFingerprint = vmessQRCode.fp
return bean
}
private fun parseCsvVMess(csv: String): VMessBean {
@ -345,61 +351,61 @@ private fun parseCsvVMess(csv: String): VMessBean {
}
data class VmessQRCode(
var v: String = "",
var ps: String = "",
var add: String = "",
var port: String = "",
var id: String = "",
var aid: String = "0",
var scy: String = "",
var net: String = "",
var type: String = "",
var host: String = "",
var path: String = "",
var tls: String = "",
var sni: String = "",
var alpn: String = "",
var fp: String = "",
)
fun VMessBean.toV2rayN(): String {
if (isVLESS) return toUri(true)
val bean = this
return "vmess://" + VmessQRCode().apply {
v = "2"
ps = this@toV2rayN.name
add = this@toV2rayN.serverAddress
port = this@toV2rayN.serverPort.toString()
id = this@toV2rayN.uuid
aid = this@toV2rayN.alterId.toString()
net = this@toV2rayN.type
host = this@toV2rayN.host
path = this@toV2rayN.path
ps = bean.name
add = bean.serverAddress
port = bean.serverPort.toString()
id = bean.uuid
aid = bean.alterId.toString()
net = bean.type
host = bean.host
path = bean.path
when (net) {
"grpc" -> {
path = this@toV2rayN.path
"http" -> {
if (!isTLS()) {
type = "http"
net = "tcp"
}
}
}
tls = if (this@toV2rayN.security == "tls") "tls" else ""
sni = this@toV2rayN.sni
scy = this@toV2rayN.encryption
fp = this@toV2rayN.utlsFingerprint
if (isTLS()) {
tls = "tls"
if (bean.realityPubKey.isNotBlank()) {
tls = "reality"
}
}
scy = bean.encryption
sni = bean.sni
alpn = bean.alpn.replace("\n", ",")
fp = bean.utlsFingerprint
}.let {
NGUtil.encode(Gson().toJson(it))
}
}
fun StandardV2RayBean.toUri(standard: Boolean = true): String {
if (this is VMessBean && alterId > 0) return toV2rayN()
fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String {
// VMess
if (this is VMessBean && !isVLESS) {
return toV2rayN()
}
// VLESS & Trojan (ducksoft fmt)
val builder = linkBuilder()
.username(if (this is TrojanBean) password else uuid)
.host(serverAddress)
.port(serverPort)
.addQueryParameter("type", type)
if (this !is TrojanBean) builder.addQueryParameter("encryption", encryption)
if (isVLESS) {
builder.addQueryParameter("encryption", "none")
if (encryption != "auto") builder.addQueryParameter("flow", encryption)
}
when (type) {
"tcp" -> {}
@ -408,11 +414,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
builder.addQueryParameter("host", host)
}
if (path.isNotBlank()) {
if (standard) {
builder.addQueryParameter("path", path)
} else {
builder.encodedPath(path.pathSafe())
}
}
if (type == "ws") {
if (wsMaxEarlyData > 0) {
@ -422,12 +424,13 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
}
}
} else if (type == "http" && !isTLS()) {
return "" // no fmt?
builder.setQueryParameter("type", "tcp")
builder.addQueryParameter("headerType", "http")
}
}
"grpc" -> {
if (path.isNotBlank()) {
builder.addQueryParameter("serviceName", path)
builder.setQueryParameter("serviceName", path)
}
}
}
@ -440,7 +443,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
builder.addQueryParameter("sni", sni)
}
if (alpn.isNotBlank()) {
builder.addQueryParameter("alpn", alpn)
builder.addQueryParameter("alpn", alpn.replace("\n", ","))
}
if (certificates.isNotBlank()) {
builder.addQueryParameter("cert", certificates)
@ -468,10 +471,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
builder.encodedFragment(name.urlSafe())
}
// TODO vless flow: bean.encryption != "auto"
return builder.toLink(if (isVLESS) "vless" else "vmess")
return builder.toLink(if (isTrojan) "trojan" else "vless")
}
fun buildSingBoxOutboundStreamSettings(bean: StandardV2RayBean): V2RayTransportOptions? {

View File

@ -43,7 +43,6 @@ import io.nekohasekai.sagernet.databinding.LayoutProfileListBinding
import io.nekohasekai.sagernet.databinding.LayoutProgressListBinding
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.fmt.toUniversalLink
import io.nekohasekai.sagernet.fmt.v2ray.toV2rayN
import io.nekohasekai.sagernet.group.RawUpdater
import io.nekohasekai.sagernet.ktx.*
import io.nekohasekai.sagernet.plugin.PluginManager
@ -1470,11 +1469,6 @@ class ConfigurationFragment @JvmOverloads constructor(
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.profile_share_menu, popup.menu)
if (proxyEntity.vmessBean == null || proxyEntity.vmessBean!!.isVLESS) {
popup.menu.findItem(R.id.action_group_qr).subMenu?.removeItem(R.id.action_v2rayn_qr)
popup.menu.findItem(R.id.action_group_clipboard).subMenu?.removeItem(R.id.action_v2rayn_clipboard)
}
when {
!proxyEntity.haveStandardLink() -> {
popup.menu.findItem(R.id.action_group_qr).subMenu?.removeItem(R.id.action_standard_qr)
@ -1527,14 +1521,12 @@ class ConfigurationFragment @JvmOverloads constructor(
try {
currentName = entity.displayName()!!
when (item.itemId) {
R.id.action_standard_qr -> showCode(entity.toLink()!!)
R.id.action_standard_clipboard -> export(entity.toLink()!!)
R.id.action_standard_qr -> showCode(entity.toStdLink()!!)
R.id.action_standard_clipboard -> export(entity.toStdLink()!!)
R.id.action_universal_qr -> showCode(entity.requireBean().toUniversalLink())
R.id.action_universal_clipboard -> export(
entity.requireBean().toUniversalLink()
)
R.id.action_v2rayn_qr -> showCode(entity.vmessBean!!.toV2rayN())
R.id.action_v2rayn_clipboard -> export(entity.vmessBean!!.toV2rayN())
R.id.action_config_export_clipboard -> export(entity.exportConfig().first)
R.id.action_config_export_file -> {
val cfg = entity.exportConfig()

View File

@ -130,11 +130,12 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
private lateinit var selectedGroup: ProxyGroup
private val exportProfiles = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
private val exportProfiles =
registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
if (data != null) {
runOnDefaultDispatcher {
val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id)
val links = profiles.mapNotNull { it.toLink(compact = true) }.joinToString("\n")
val links = profiles.joinToString("\n") { it.toStdLink(compact = true) }
try {
(requireActivity() as MainActivity).contentResolver.openOutputStream(
data
@ -310,7 +311,8 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
undoManager.flush()
}
inner class GroupHolder(binding: LayoutGroupItemBinding) : RecyclerView.ViewHolder(binding.root),
inner class GroupHolder(binding: LayoutGroupItemBinding) :
RecyclerView.ViewHolder(binding.root),
PopupMenu.OnMenuItemClickListener {
lateinit var proxyGroup: ProxyGroup
@ -343,8 +345,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
R.id.action_export_clipboard -> {
runOnDefaultDispatcher {
val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id)
val links = profiles.mapNotNull { it.toLink(compact = true) }
.joinToString("\n")
val links = profiles.joinToString("\n") { it.toStdLink(compact = true) }
onMainDispatcher {
SagerNet.trySetPrimaryClip(links)
snackbar(getString(R.string.copy_toast_msg)).show()
@ -397,7 +398,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
popup.menuInflater.inflate(R.menu.group_action_menu, popup.menu)
if (proxyGroup.type != GroupType.SUBSCRIPTION) {
popup.menu.removeItem(R.id.action_share)
popup.menu.removeItem(R.id.action_share_subscription)
}
popup.setOnMenuItemClickListener(this)
popup.show()
@ -451,12 +452,12 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
}
groupStatus.setPadding(0)
} else if (subscription != null && !subscription.subscriptionUserinfo.isNullOrBlank()) { // Raw
var text = "";
var text = ""
fun get(regex: String): String? {
return regex.toRegex().findAll(subscription.subscriptionUserinfo).mapNotNull {
if (it.groupValues.size > 1) it.groupValues[1] else null
}.firstOrNull();
}.firstOrNull()
}
var used: Long = 0
@ -484,7 +485,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
if (text.isNotEmpty()) {
groupTraffic.isVisible = true
groupTraffic.text = text;
groupTraffic.text = text
groupStatus.setPadding(0)
}
} else {

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_share"
android:title="@string/share">
android:id="@+id/action_share_subscription"
android:title="@string/share_subscription">
<menu>
<item
android:id="@+id/action_universal_clipboard"

View File

@ -9,10 +9,7 @@
android:title="@string/standard" />
<item
android:id="@+id/action_universal_qr"
android:title="@string/app_name" />
<item
android:id="@+id/action_v2rayn_qr"
android:title="@string/v2rayn" />
android:title="SN Link" />
</menu>
</item>
<item
@ -24,10 +21,7 @@
android:title="@string/standard" />
<item
android:id="@+id/action_universal_clipboard"
android:title="@string/app_name" />
<item
android:id="@+id/action_v2rayn_clipboard"
android:title="@string/v2rayn" />
android:title="SN Link" />
</menu>
</item>
<item

View File

@ -466,4 +466,5 @@
<string name="front_proxy">前置代理</string>
<string name="landing_proxy">落地代理</string>
<string name="shadowtls_version">ShadowTLS 版本</string>
<string name="share_subscription">分享订阅</string>
</resources>

View File

@ -507,5 +507,6 @@ Anyone can write advanced plugins, which can control NekoBox. please download an
<string name="landing_proxy">Landing Proxy</string>
<string name="action_shadowtls" translatable="false">ShadowTLS</string>
<string name="shadowtls_version">ShadowTLS Version</string>
<string name="share_subscription">Share Subscription</string>
</resources>