一般机场提供的订阅链接会直接包含全部节点信息,为了实现按照区域、倍率分组后提供流量分发,就需要借助 Sub-Store1 解析订阅链接后通过正则表达式筛选出特定的节点信息。但是由于我并不使用额外的很多特性,所以想要借助 Cloudflare Workers2 实现一套简单的解决方案。
目标拆解
因为当前我使用的机场提供了 Surge 订阅链接,具体内容就是如下图的可读文本,所以在实现上就不考虑多种订阅链接的解析功能,只需要实现文本筛选就可以满足需要。
最终确定应该包含的特性如下:
- 将订阅链接(一个或多个)、正则表达式作为两个参数传给 Workers 进行处理。
- Workers 在云端下载订阅内容并根据正则表达式过滤出目标条目构成最终的节点信息集。
- Surge 获取 Workers 处理后的节点信息构成分组。
整体方案通过正则表达式这个参数保证了分组的灵活性,又通过订阅链接传入参数保证了一定的安全性,同时整个处理过程在云端便不会再出现 Surge 内存占用的相关问题。另一方面,劣势便是 Cloudflare Workers 免费用户的限制问题了。
代码实现
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
const targetUrls = decodeURIComponent(url.searchParams.get('url')).split('|')
const regexPattern = decodeURIComponent(url.searchParams.get('RegExp'))
const regex = new RegExp(regexPattern)
const fetchOptions = {
headers: request.headers,
}
const lineFilter = new LineFilter(regex)
const errors = []
const results = await Promise.allSettled(
targetUrls.map((targetUrl) =>
fetch(targetUrl, fetchOptions).then((response) => {
const reader = response.body.getReader()
const decoder = new TextDecoder()
return lineFilter.filterStream(reader, decoder)
}),
),
)
results.forEach((result, idx) => {
if (result.status === 'rejected') {
errors.push(`Error fetching ${targetUrls[idx]}: ${result.reason}`)
}
})
if (errors.length > 0) {
return new Response(errors.join('\n'), { status: 500 })
}
return new Response(lineFilter.getFilteredLines().join('\n'), { status: 200 })
}
class LineFilter {
constructor(regex) {
this.regex = regex
this.filteredLines = []
this.partialLine = ''
}
getFilteredLines() {
return this.filteredLines
}
async filterStream(reader, decoder) {
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const chunk = decoder.decode(value, { stream: true })
const lines = chunk.split('\n')
if (lines.length > 1) {
lines[0] = this.partialLine + lines[0]
this.partialLine = lines.pop()
} else {
this.partialLine += lines[0]
}
this.handleLines(lines)
}
if (this.partialLine !== '') {
this.handleLines([this.partialLine])
}
}
filterLines(lines) {
return lines.filter((line) => {
const trimmed = line.trim()
return trimmed !== '' && !trimmed.startsWith('#') && this.regex.test(line)
})
}
findProxyGroupIndices(lines) {
let proxyIndex = -1
let groupIndex = -1
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
if (line === '[Proxy]') {
proxyIndex = i
} else if (line === '[Proxy Group]') {
groupIndex = i
break
}
}
return [proxyIndex, groupIndex]
}
handleLines(lines) {
const [proxyIndex, groupIndex] = this.findProxyGroupIndices(lines)
if (proxyIndex !== -1 && groupIndex !== -1) {
this.filteredLines.push(...this.filterLines(lines.slice(proxyIndex + 1, groupIndex)))
} else if (proxyIndex === -1 && groupIndex !== -1) {
this.filteredLines.push(...this.filterLines(lines.slice(0, groupIndex)))
} else {
this.filteredLines.push(...this.filterLines(lines))
}
}
}
最终使用可借助 https://your-worker.your-account.workers.dev?url=https%3A%2F%2Fexample.com%2Ffile.txt%7Chttps%3A%2F%2Fexample.net%2Ffile.txt&RegExp=%5E%5BA-Z%5D%2B%3A%20.*$
进行分组后的节点信息获取。
其中 url 支持 “|” 分割多个订阅链接;RegExp 填写编写好的正则表达式。
Ps:为了方便在 Surge 中调用,两个参数都需要进行转码。
Surge 中调用
在 Surge 中使用也很方便,比如我们要创建一个香港节点的分组,就可以直接在 [Proxy Group]
中增加对应的条目,对于不同的分组规则,只需要调整为相应的正则表达式。
[Proxy Group]
🇭🇰 = url-test, policy-path= https://your-worker.your-account.workers.dev?url= https%3A%2F%2Fexample.com%2Ffile.txt&RegExp=%5E.*%E9%A6%99%E6%B8%AF.*%24