Skip to content

Commit

Permalink
feat: 新增判断 UserAgent 的工具方法
Browse files Browse the repository at this point in the history
  • Loading branch information
geekdada committed Oct 28, 2023
1 parent fc245ca commit 9a8d0f0
Show file tree
Hide file tree
Showing 8 changed files with 501 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default {
children: [
'/guide/advance/surge-advance',
'/guide/advance/custom-filter',
'/guide/advance/advanced-provider',
'/guide/advance/automation',
'/guide/advance/api-gateway',
'/guide/advance/redis-cache',
Expand Down
197 changes: 197 additions & 0 deletions docs/guide/advance/advanced-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---
title: 编写更复杂的自定义 Provider
sidebarDepth: 2
---

# 编写更复杂的自定义 Provider

[[toc]]

## 介绍

我们在 Provider 的指南中已经介绍了如何编写一个简单的自定义 Provider,并且提到了可以利用异步函数获取节点信息。异步函数可以极大地多样化我们编排节点的方式,这篇文章会例举几个我认为比较有用的例子。需要注意的是,这篇文章都是配合面板使用的。

## 准备工作

在开始之前,我想先介绍一些我们在 Surgio v3 中新增的一些功能,这些功能可能会在下面的例子中用到。

1. 请求订阅的客户端 UserAgent 会暴露在异步函数的 `customParams` 中,你可以通过 `customParams.requestUserAgent` 来获取
2. 请求订阅的 URL 参数会暴露在异步函数的 `customParams` 中,你可以通过 `customParams.xxx` 来获取(它们的值都是字符串)
3. Surgio 内置了 `httpClient` 工具方法,`httpClient` 是一个 [Got](https://github.com/sindresorhus/got) 实例,你可以使用它来发起 HTTP 请求
4. Surgio 内置了一些判断客户端 UserAgent 的工具方法(v3.2.0 新增)

## 例子 🌰

### 动态上下线节点

**情境:** 我有两台国内用于转发的小鸡,它们的流量不多,每个月我都要人工修改节点的域名和端口来切换不同的转发小鸡,我想用一个更简单的方式来动态切换他们。

**思路:** [Flagsmith](https://www.flagsmith.com/) 是一个免费的 Feature Flag 服务,我们可以在 Flagsmith 上创建一个名为 `china` 的 Feature Flag,然后使用不同的值来对应不同的小鸡,例如 `china=1` 对应小鸡 A,`china=2` 对应小鸡 B。然后我们在 Provider 中使用异步函数来获取节点列表,根据 `china` 的值来切换节点的域名和端口。

**实现:**

我们先来看一下 Provider 的配置:

```js
const { utils, defineCustomProvider } = require('surgio');
const Flagsmith = require('flagsmith-nodejs');

const flagsmith = new Flagsmith({
environmentKey: 'put_your_environment_key_here',
});

module.exports = defineCustomProvider({
nodeList: async () => {
const flags = await flagsmith.getEnvironmentFlags();
const china = flags.getFeatureValue('china');

if (china === '1') {
return [
{
nodeName: '香港节点',
type: 'shadowsocks',
hostname: 'a.com',
port: 443,
method: 'chacha20-ietf-poly1305',
password: 'put_your_password_here',
},
];
} else {
// 默认返回 b.com
return [
{
nodeName: '香港节点',
type: 'shadowsocks',
hostname: 'b.com',
port: 443,
method: 'chacha20-ietf-poly1305',
password: 'put_your_password_here',
},
];
}
},
})
```

因为节点的名称没有变化,所以客户端自动更新订阅之后不会因为名称不一致而选中别的节点。 今后,我只需要在 Flagsmith 上修改 `china` 的值,就能够动态切换节点了。

### 根据客户端 UserAgent 动态切换节点

**情境:** 我同时部署了 Hysteria 和 Shadowsocks,我想在 TF 版本的 Surge 中使用 Hysteria,而在其他版本的 Surge 中使用 Shadowsocks。

**思路:** 我们可以利用客户端 UserAgent 来判断客户端的 Surge 版本,然后根据 Surge 版本来切换节点。

**实现:**

我们先来看一下 Provider 的配置:

```js
const { utils, defineCustomProvider } = require('surgio');

module.exports = defineCustomProvider({
nodeList: async (customParams) => {
const useragent = customParams.requestUserAgent;
const isHysteriaSupported = utils.isSurgeIOS(useragent, '>=2920')

return [
isHysteriaSupported ? {
nodeName: '香港节点',
type: 'hysteria2',
hostname: 'a.com',
port: 443,
password: 'put_your_password_here',
} : {
nodeName: '香港节点',
type: 'shadowsocks',
hostname: 'a.com',
port: 8443,
method: 'chacha20-ietf-poly1305',
password: 'put_your_password_here',
},
{
nodeName: '美国节点',
type: 'shadowsocks',
hostname: 'b.com',
port: 8443,
method: 'chacha20-ietf-poly1305',
password: 'put_your_password_here',
}
]
}
})
```

这样写的 Provider 在本地生成时没有 `requestUserAgent``isHysteriaSupported``false` 所以不会报错。

以下是所有用于判断客户端 UserAgent 的工具方法:

```js
utils.isSurgeIOS(useragent)
utils.isSurgeMac(useragent)
utils.isClash(useragent)
utils.isStash(useragent)
utils.isQuantumultX(useragent)
utils.isShadowrocket(useragent)
utils.isLoon(useragent)
```

这些方法都支持第二个参数来判断版本号,例如 `utils.isSurgeIOS(useragent, '>=2920')`。正确的判断语法有:

- `>=2920`
- `>2920`
- `<=2920`
- `<2920`
- `=2920`

需要注意的是,有的客户端实际在 UserAgent 中使用的版本号并非形如 `1.2.3` 的格式,而是形如 `2490` 这样的格式。请在软件的设置页查看真实的版本号。下面是一些常见客户端的版本号格式:

- Surge: 1000
- Stash: 1.2.3
- Clash: 1.2.3(原版 Clash 不传版本号)
- Loon: 1000
- Quantumult X: 1.2.3
- Shadowrocket: 1000

### 根据 URL 参数动态切换节点

**情境:** 我分享了我的订阅地址给朋友一起用,但是我不想把我用来打游戏的节点也分享给他们。

**思路:** 我不想弄得很复杂,只需要在 URL 中增加一个参数来开启游戏的节点。

**实现:**

```js
const { utils, defineCustomProvider } = require('surgio');

module.exports = defineCustomProvider({
nodeList: async (customParams) => {
const isGame = customParams.game === '1';

const nodeList = [
isGame ? {
nodeName: '香港节点',
type: 'hysteria2',
hostname: 'a.com',
port: 443,
password: 'put_your_password_here',
} : undefined,
{
nodeName: '美国节点',
type: 'shadowsocks',
hostname: 'b.com',
port: 8443,
method: 'chacha20-ietf-poly1305',
password: 'put_your_password_here',
}
]

return nodeList.filter(Boolean); // 不要忘了这一行过滤 undefined
}
})
```

下面的两个订阅地址分别对应开启和关闭游戏节点:

- https://surgioapi.com/get-artifact/my-provider?game=1 - 有游戏节点
- https://surgioapi.com/get-artifact/my-provider - 没有游戏节点
- 本地生成 - 没有游戏节点
10 changes: 7 additions & 3 deletions docs/guide/custom-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ module.exports = defineCustomProvider({

`customParams` 默认会包含 `requestUserAgent`,方便你根据不同的客户端返回不同的节点列表。

:::tip 提示
如果你想了解如何编写更复杂的 Provider 请看 [这里](/guide/advance/advanced-provider.md)
:::

```js
const { defineCustomProvider } = require('surgio');

Expand Down Expand Up @@ -369,11 +373,11 @@ Clash 需要在配置中开启 `clashConfig.enableHysteria2`。
nodeName: 'Hysteria',
hostname: 'hysteria.example.com',
port: 443,
password: 'password',
password: 'password',
downloadBandwidth: 40, // 可选, Mbps
uploadBandwidth: 40, // 可选, Mbps
sni: 'sni.example.com', // 可选
skipCertVerify: true, // 可选
alpn: ['h3'], // 可选,Stash 不支持空值
udpRelay: false, // 可选, 仅 Clash 支持更改,Surge 默认开启
}
```

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"chalk": "^4.1.2",
"change-case": "^4.1.2",
"check-node-version": "^4.2.1",
"compare-versions": "^6.1.0",
"date-fns": "^2.30.0",
"detect-newline": "^3.1.0",
"dotenv": "^16.3.1",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isRailway,
isVercel,
} from './utils'
import * as useragentUtils from './utils/useragent'
import * as filters from './filters'
import { CATEGORIES } from './constant'

Expand All @@ -23,6 +24,7 @@ const { internalFilters, ...filtersUtils } = filters
export const utils = {
...internalFilters,
...filtersUtils,
...useragentUtils,
isHeroku,
isNow,
isVercel,
Expand Down
82 changes: 82 additions & 0 deletions src/utils/__tests__/useragent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import test from 'ava'

import {
isSurgeIOS,
isSurgeMac,
isClash,
isStash,
isQuantumultX,
isShadowrocket,
isLoon,
} from '../useragent'

test('isSurgeIOS', (t) => {
t.is(isSurgeIOS('Surge iOS/2920'), true)
t.is(isSurgeIOS('Surge iOS/2920', '>=300'), true)
t.is(isSurgeIOS('Surge iOS/2920 CFNetwork/1335.0.3.2', '>=300'), true)
t.is(isSurgeIOS('Surge iOS/2920', '>=3000'), false)
t.is(isSurgeIOS('Surge Mac/2408', '>3000'), false)
t.is(isSurgeIOS('Surge/1129 CFNetwork/1335.0.3.2 Darwin/21.6.0'), false)
t.is(isSurgeIOS('Surge iOS', '>=3000'), false)
})

test('isSurgeMac', (t) => {
t.is(isSurgeMac('Surge Mac/2920'), true)
t.is(isSurgeMac('Surge Mac/2920', '>=300'), true)
t.is(isSurgeMac('Surge Mac/2920 CFNetwork/1335.0.3.2', '>=300'), true)
t.is(isSurgeMac('Surge Mac/2920', '>=3000'), false)
t.is(isSurgeMac('Surge iOS/2408', '>3000'), false)
t.is(isSurgeMac('Surge/1129 CFNetwork/1335.0.3.2 Darwin/21.6.0'), false)
})

test('isClash', (t) => {
t.is(isClash('Surge iOS/2920'), false)
t.is(isClash('clash'), true)
t.is(isClash('Clash'), true)
t.is(isClash('Stash/2.4.7 Clash/1.9.0'), true)
t.is(isClash('Stash/2.4.7 Clash/1.9.0', '>=1.9.0'), true)
t.is(isClash('Stash/2.4.7 Clash/1.9.0', '>=2.0.0'), false)
})

test('isStash', (t) => {
t.is(isStash('Surge iOS/2920'), false)
t.is(isStash('clash'), false)
t.is(isStash('Stash/2.4.7 Clash/1.9.0'), true)
t.is(isStash('Stash/2.4.7 Clash/1.9.0', '>=1.9.0'), true)
t.is(isStash('Stash/2.4.7 Clash/1.9.0', '>=2.0.0'), true)
t.is(isStash('Stash/2.4.7 Clash/1.9.0', '>=3.0.0'), false)
})

test('isQuantumultX', (t) => {
t.is(isQuantumultX('Quantumult%20X/1.4.1 (iPhone15,2; iOS 17.0.3)'), true)
t.is(
isQuantumultX('Quantumult%20X/1.4.1 (iPhone15,2; iOS 17.0.3)', '>1.0.0'),
true,
)
t.is(
isQuantumultX('Quantumult%20X/1.4.1 (iPhone15,2; iOS 17.0.3)', '>2.0.0'),
false,
)
t.is(isQuantumultX('Quantumult/1.0.8 (iPhone15,2; iOS 17.0.3)'), false)
})

test('isShadowrocket', (t) => {
t.is(isShadowrocket('Shadowrocket/1982 CFNetwork/1474 Darwin/23.0.0'), true)
t.is(
isShadowrocket('Shadowrocket/1982 CFNetwork/1474 Darwin/23.0.0', '>=1900'),
true,
)
t.is(
isShadowrocket('Shadowrocket/1982 CFNetwork/1474 Darwin/23.0.0', '>=2000'),
false,
)
t.is(isShadowrocket('CFNetwork/1474 Darwin/23.0.0'), false)
})

test('isLoon', (t) => {
t.is(isLoon('Loon/622 CFNetwork/1485 Darwin/23.1.0'), true)
t.is(isLoon('Loon/622 CFNetwork/1485 Darwin/23.1.0', '>=600'), true)
t.is(isLoon('Loon/622 CFNetwork/1485 Darwin/23.1.0', '>=700'), false)
t.is(isLoon('CFNetwork/1485 Darwin/23.1.0', '>=700'), false)
t.is(isLoon('Loon CFNetwork/1485 Darwin/23.1.0', '>=700'), false)
})
Loading

0 comments on commit 9a8d0f0

Please sign in to comment.