11package features.proxy.server.usecase
22
3- import features.logs.AndroidAppLogger
4- import features.proxy.server.model.ProxyServer
5- import features.proxy.server.model.ProxyServerConstants
6- import kotlin.io.encoding.Base64
7-
8- internal data class ProxyServerImportResult (
9- val urlCount : Int ,
10- val servers : List <ProxyServer <* >>,
11- )
12-
13- internal enum class ProxyServerImportSource (
14- val logName : String ,
15- val decodeBase64 : Boolean ,
16- ) {
17- Clipboard (logName = " clipboard" , decodeBase64 = false ),
18- File (logName = " file" , decodeBase64 = true ),
19- QrCode (logName = " qr_code" , decodeBase64 = false ),
20- SubscriptionUrl (logName = " subscription_url" , decodeBase64 = true ),
21- }
3+ import features.proxy.server.usecase.importer.importPayloads
4+ import features.proxy.server.usecase.importer.parseProxyServersFromJsonConfig
5+ import features.proxy.server.usecase.importer.parseProxyServersFromMihomoYamlConfig
6+ import features.proxy.server.usecase.importer.parseProxyServersFromUrls
227
238internal fun importProxyServersFromText (
249 text : String ,
@@ -34,142 +19,23 @@ private fun parseProxyServersFromPayloads(
3419 payloads : List <String >,
3520 source : ProxyServerImportSource ,
3621): ProxyServerImportResult {
37- var lastAttempt = ProxyServerImportResult (urlCount = 0 , servers = emptyList())
38- payloads.forEach { payload ->
39- val jsonResult = parseProxyServersFromJsonConfig(
40- text = payload,
41- source = source,
42- )
43- if (jsonResult.servers.isNotEmpty()) {
44- return jsonResult
45- }
46- if (jsonResult.urlCount > 0 ) {
47- lastAttempt = jsonResult
48- }
49-
50- val lineResult = parseProxyServersFromLines(
51- lines = payload.lineSequence(),
52- source = source,
53- )
54- if (lineResult.servers.isNotEmpty()) {
55- return lineResult
56- }
57- if (lineResult.urlCount > 0 || lastAttempt.urlCount == 0 ) {
58- lastAttempt = lineResult
22+ var lastAttempt = EmptyProxyServerImportResult
23+ for (payload in payloads) {
24+ for (parser in ProxyServerImportParsers ) {
25+ val result = parser(payload, source)
26+ if (result.servers.isNotEmpty()) {
27+ return result
28+ }
29+ if (result.urlCount > 0 || lastAttempt.urlCount == 0 ) {
30+ lastAttempt = result
31+ }
5932 }
6033 }
6134 return lastAttempt
6235}
6336
64- private fun parseProxyServersFromLines (
65- lines : Sequence <String >,
66- source : ProxyServerImportSource ,
67- ): ProxyServerImportResult {
68- val urls = lines.proxyServerUrlCandidates(distinct = true )
69- val servers = urls.mapIndexedNotNull { index, url ->
70- parseProxyServerUrlOrNull(
71- url = url,
72- index = index,
73- source = source,
74- )
75- }
76- return ProxyServerImportResult (
77- urlCount = urls.size,
78- servers = servers,
79- )
80- }
81-
82- private fun parseProxyServerUrlOrNull (
83- url : String ,
84- index : Int ,
85- source : ProxyServerImportSource ,
86- ): ProxyServer <* >? {
87- return runCatching { ProxyServer .parse(url) }
88- .onFailure { error ->
89- AndroidAppLogger .warn(
90- ProxyServerImportLogTag ,
91- url.importFailureMessage(index = index, source = source),
92- error,
93- )
94- }
95- .getOrNull()
96- }
97-
98- private fun String.importFailureMessage (
99- index : Int ,
100- source : ProxyServerImportSource ,
101- ): String {
102- val protocol = substringBefore(" ://" , missingDelimiterValue = " " ).ifBlank { " <blank>" }
103- return " Failed to import proxy server URL source=${source.logName} index=$index protocol=$protocol length=$length "
104- }
105-
106- private fun String.importPayloads (source : ProxyServerImportSource ): List <String > {
107- return if (source.decodeBase64) {
108- listOfNotNull(decodeImportBase64(), this ).distinct()
109- } else {
110- listOf (this )
111- }
112- }
113-
114- private fun String.decodeImportBase64 (): String? {
115- val normalized = trimStart(ImportByteOrderMark ).filterNot(Char ::isWhitespace)
116- if (normalized.isBlank()) return null
117- return ImportBase64Decoders .firstNotNullOfOrNull { decoder ->
118- runCatching { decoder.decode(normalized).decodeToString() }.getOrNull()
119- } ? : normalized.trimEnd(' =' ).takeIf { it.length != normalized.length }?.let { trimmed ->
120- ImportBase64Decoders .firstNotNullOfOrNull { decoder ->
121- runCatching { decoder.decode(trimmed).decodeToString() }.getOrNull()
122- }
123- }
124- }
125-
126- private fun Sequence<String>.proxyServerUrlCandidates (distinct : Boolean ): List <String > {
127- val urls = flatMap { line -> line.proxyServerUrlCandidates() }
128- .let { sequence -> if (distinct) sequence.distinct() else sequence }
129- return urls.toList()
130- }
131-
132- private fun String.proxyServerUrlCandidates (): Sequence <String > {
133- val line = trim().trimStart(ImportByteOrderMark )
134- if (line.isBlank()) return emptySequence()
135- val embeddedUrls = ProxyServerUrlRegex .findAll(line)
136- .map { match -> match.value.trimEnd(' ,' , ' ;' ) }
137- .filterNot { url -> url == line }
138- .toList()
139- return if (line.startsWithProxyServerScheme()) {
140- (listOf (line) + embeddedUrls).asSequence()
141- } else {
142- embeddedUrls.asSequence()
143- }
144- }
145-
146- private fun String.startsWithProxyServerScheme (): Boolean {
147- val lower = lowercase()
148- return ProxyServerUrlPrefixes .any { prefix -> lower.startsWith(prefix) }
149- }
150-
151- private val ImportBase64Decoders = listOf (
152- Base64 .Default ,
153- Base64 .Default .withPadding(Base64 .PaddingOption .ABSENT_OPTIONAL ),
154- Base64 .UrlSafe ,
155- Base64 .UrlSafe .withPadding(Base64 .PaddingOption .ABSENT_OPTIONAL ),
37+ private val ProxyServerImportParsers : List <ProxyServerPayloadParser > = listOf (
38+ ::parseProxyServersFromJsonConfig,
39+ ::parseProxyServersFromMihomoYamlConfig,
40+ ::parseProxyServersFromUrls,
15641)
157-
158- private val ProxyServerUrlPrefixes = listOf (
159- " ${ProxyServerConstants .PROTOCOL_HTTP } ://" ,
160- " ${ProxyServerConstants .PROTOCOL_SOCKS } ://" ,
161- " ${ProxyServerConstants .PROTOCOL_SS } ://" ,
162- " ${ProxyServerConstants .PROTOCOL_VMESS } ://" ,
163- " ${ProxyServerConstants .PROTOCOL_VLESS } ://" ,
164- " ${ProxyServerConstants .PROTOCOL_TROJAN } ://" ,
165- " ${ProxyServerConstants .PROTOCOL_HY2 } ://" ,
166- " ${ProxyServerConstants .PROTOCOL_HYSTERIA2 } ://" ,
167- " ${ProxyServerConstants .PROTOCOL_WIREGUARD } ://" ,
168- )
169-
170- private val ProxyServerUrlRegex = Regex (
171- " (?i)\\ b(?:http|socks|ss|vmess|vless|trojan|hy2|hysteria2|wireguard)://[^\\ s<>\" ']+" ,
172- )
173-
174- private const val ProxyServerImportLogTag = " ProxyServerImport"
175- private const val ImportByteOrderMark = ' \uFEFF '
0 commit comments