Skip to content

Commit 5d29d25

Browse files
committed
feat(wip): 支持自定义 share token
1 parent d12ccad commit 5d29d25

File tree

8 files changed

+194
-202
lines changed

8 files changed

+194
-202
lines changed

backend/package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.14.410",
3+
"version": "2.14.411",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
55
"main": "src/main.js",
66
"scripts": {
@@ -28,13 +28,12 @@
2828
"http-proxy-middleware": "^2.0.6",
2929
"ip-address": "^9.0.5",
3030
"js-base64": "^3.7.2",
31-
"jsonwebtoken": "^9.0.2",
3231
"jsrsasign": "^11.1.0",
3332
"lodash": "^4.17.21",
33+
"ms": "^2.1.3",
34+
"nanoid": "^3.3.3",
3435
"request": "^2.88.2",
35-
"semver": "^7.3.7",
36-
"static-js-yaml": "^1.0.0",
37-
"uuid": "^8.3.2"
36+
"static-js-yaml": "^1.0.0"
3837
},
3938
"devDependencies": {
4039
"@babel/core": "^7.18.0",

backend/pnpm-lock.yaml

Lines changed: 22 additions & 135 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const FILES_KEY = 'files';
66
export const MODULES_KEY = 'modules';
77
export const ARTIFACTS_KEY = 'artifacts';
88
export const RULES_KEY = 'rules';
9+
export const TOKENS_KEY = 'tokens';
910
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
1011
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
1112
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

backend/src/restful/download.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import { getISO } from '@/utils/geo';
1313
import env from '@/utils/env';
1414

1515
export default function register($app) {
16+
$app.get('/share/col/:name', downloadCollection);
17+
$app.get('/share/sub/:name', downloadSubscription);
18+
1619
$app.get('/download/collection/:name', downloadCollection);
1720
$app.get('/download/:name', downloadSubscription);
1821
$app.get(

backend/src/restful/file.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { produceArtifact } from '@/restful/sync';
1313
export default function register($app) {
1414
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
1515

16+
$app.get('/share/file/:name', getFile);
17+
1618
$app.route('/api/file/:name')
1719
.get(getFile)
1820
.patch(updateFile)

backend/src/restful/index.js

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import migrate from '@/utils/migration';
44
import download from '@/utils/download';
55
import { syncArtifacts } from '@/restful/sync';
66
import { gistBackupAction } from '@/restful/miscs';
7+
import { TOKENS_KEY } from '@/constants';
78

89
import registerSubscriptionRoutes from './subscriptions';
910
import registerCollectionRoutes from './collections';
@@ -176,8 +177,6 @@ export default function serve() {
176177
fe_be_path === '/' ? '' : fe_be_path
177178
}${be_download}`;
178179

179-
const jwt = eval(`require("jsonwebtoken")`);
180-
181180
app.use(
182181
be_share_rewrite,
183182
createProxyMiddleware({
@@ -186,43 +185,30 @@ export default function serve() {
186185
pathRewrite: (path, req) => {
187186
if (req.method.toLowerCase() !== 'get')
188187
throw new Error('Method not allowed');
189-
const payload = jwt.verify(
190-
req.query.token,
191-
fe_be_path,
188+
const tokens = $.read(TOKENS_KEY) || [];
189+
const token = tokens.find(
190+
(t) =>
191+
t.token === req.query.token &&
192+
t.type === req.params.type &&
193+
t.name === req.params.name &&
194+
(t.exp == null || t.exp > Date.now()),
192195
);
193-
if (
194-
payload.type !== req.params.type ||
195-
payload.name !== req.params.name
196-
)
197-
throw new Error('Forbbiden');
198-
if (payload.type === 'sub')
199-
return path.replace(
200-
'/share/sub/',
201-
'/download/',
202-
);
203-
if (payload.type === 'col')
204-
return path.replace(
205-
'/share/col/',
206-
'/download/collection/',
207-
);
208-
if (payload.type === 'file')
209-
return path.replace(
210-
'/share/file/',
211-
'/api/file/',
212-
);
213-
throw new Error('Not Found');
196+
if (!token) throw new Error('Forbbiden');
197+
return path;
214198
},
215199
}),
216200
);
217201
app.use(
218202
be_api_rewrite,
219203
createProxyMiddleware({
220204
target: `http://127.0.0.1:${port}`,
221-
changeOrigin: true,
222205
pathRewrite: (path) => {
223-
return path.startsWith(be_api_rewrite)
206+
const newPath = path.startsWith(be_api_rewrite)
224207
? path.replace(be_api_rewrite, be_api)
225208
: path;
209+
return newPath.includes('?')
210+
? `${newPath}&share=true`
211+
: `${newPath}?share=true`;
226212
},
227213
}),
228214
);

backend/src/restful/miscs.js

Lines changed: 148 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
GIST_BACKUP_FILE_NAME,
1010
GIST_BACKUP_KEY,
1111
SETTINGS_KEY,
12+
TOKENS_KEY,
13+
FILES_KEY,
14+
COLLECTIONS_KEY,
15+
SUBS_KEY,
1216
} from '@/constants';
1317
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
1418
import Gist from '@/utils/gist';
@@ -20,36 +24,7 @@ export default function register($app) {
2024
$app.get('/api/utils/env', getEnv); // get runtime environment
2125
$app.get('/api/utils/backup', gistBackup); // gist backup actions
2226
$app.get('/api/utils/refresh', refresh);
23-
$app.post('/api/jwt', (req, res) => {
24-
if (!ENV().isNode) {
25-
return failed(
26-
res,
27-
new RequestInvalidError(
28-
'INVALID_ENV',
29-
`This endpoint is only available in Node.js environment`,
30-
),
31-
);
32-
}
33-
try {
34-
const { payload, options } = req.body;
35-
const jwt = eval(`require("jsonwebtoken")`);
36-
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
37-
const token = jwt.sign(payload, secret, options);
38-
return success(res, {
39-
token,
40-
secret,
41-
});
42-
} catch (e) {
43-
return failed(
44-
res,
45-
new InternalServerError(
46-
'JWT_SIGN_FAILED',
47-
`Failed to sign JWT token`,
48-
`Reason: ${e.message ?? e}`,
49-
),
50-
);
51-
}
52-
});
27+
$app.post('/api/token', signToken);
5328

5429
// Storage management
5530
$app.route('/api/storage')
@@ -95,9 +70,152 @@ export default function register($app) {
9570
}
9671

9772
function getEnv(req, res) {
73+
if (req.query.share) {
74+
env.feature.share = true;
75+
}
9876
success(res, env);
9977
}
10078

79+
async function signToken(req, res) {
80+
if (!ENV().isNode) {
81+
return failed(
82+
res,
83+
new RequestInvalidError(
84+
'INVALID_ENV',
85+
`This endpoint is only available in Node.js environment`,
86+
),
87+
);
88+
}
89+
try {
90+
const { payload, options } = req.body;
91+
const ms = eval(`require("ms")`);
92+
let token = payload?.token;
93+
if (token != null) {
94+
if (typeof token !== 'string' || token.length < 1) {
95+
return failed(
96+
res,
97+
new RequestInvalidError(
98+
'INVALID_CUSTOM_TOKEN',
99+
`Invalid custom token: ${token}`,
100+
),
101+
);
102+
}
103+
const tokens = $.read(TOKENS_KEY) || [];
104+
if (tokens.find((t) => t.token === token)) {
105+
return failed(
106+
res,
107+
new RequestInvalidError(
108+
'DUPLICATE_TOKEN',
109+
`Token ${token} already exists`,
110+
),
111+
);
112+
}
113+
}
114+
const type = payload?.type;
115+
const name = payload?.name;
116+
if (!type || !name)
117+
return failed(
118+
res,
119+
new RequestInvalidError(
120+
'INVALID_PAYLOAD',
121+
`payload type and name are required`,
122+
),
123+
);
124+
if (type === 'col') {
125+
const collections = $.read(COLLECTIONS_KEY) || [];
126+
const collection = collections.find((c) => c.name === name);
127+
if (!collection)
128+
return failed(
129+
res,
130+
new RequestInvalidError(
131+
'INVALID_COLLECTION',
132+
`collection ${name} not found`,
133+
),
134+
);
135+
} else if (type === 'file') {
136+
const files = $.read(FILES_KEY) || [];
137+
const file = files.find((f) => f.name === name);
138+
if (!file)
139+
return failed(
140+
res,
141+
new RequestInvalidError(
142+
'INVALID_FILE',
143+
`file ${name} not found`,
144+
),
145+
);
146+
} else if (type === 'sub') {
147+
const subs = $.read(SUBS_KEY) || [];
148+
const sub = subs.find((s) => s.name === name);
149+
if (!sub)
150+
return failed(
151+
res,
152+
new RequestInvalidError(
153+
'INVALID_SUB',
154+
`sub ${name} not found`,
155+
),
156+
);
157+
} else {
158+
return failed(
159+
res,
160+
new RequestInvalidError(
161+
'INVALID_TYPE',
162+
`type ${name} not supported`,
163+
),
164+
);
165+
}
166+
let expiresIn = options?.expiresIn;
167+
if (options?.expiresIn != null) {
168+
expiresIn = ms(options.expiresIn);
169+
if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
170+
return failed(
171+
res,
172+
new RequestInvalidError(
173+
'INVALID_EXPIRES_IN',
174+
`Invalid expiresIn option: ${options.expiresIn}`,
175+
),
176+
);
177+
}
178+
}
179+
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
180+
const nanoid = eval(`require("nanoid")`);
181+
const tokens = $.read(TOKENS_KEY) || [];
182+
// const now = Date.now();
183+
// for (const key in tokens) {
184+
// const token = tokens[key];
185+
// if (token.exp != null || token.exp < now) {
186+
// delete tokens[key];
187+
// }
188+
// }
189+
if (!token) {
190+
do {
191+
token = nanoid.customAlphabet(nanoid.urlAlphabet)();
192+
} while (tokens.find((t) => t.token === token));
193+
}
194+
195+
tokens.push({
196+
...payload,
197+
token,
198+
createdAt: Date.now(),
199+
expiresIn: expiresIn > 0 ? ms(expiresIn) : undefined,
200+
exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,
201+
});
202+
203+
$.write(tokens, TOKENS_KEY);
204+
return success(res, {
205+
token,
206+
secret,
207+
});
208+
} catch (e) {
209+
return failed(
210+
res,
211+
new InternalServerError(
212+
'TOKEN_SIGN_FAILED',
213+
`Failed to sign token`,
214+
`Reason: ${e.message ?? e}`,
215+
),
216+
);
217+
}
218+
}
101219
async function refresh(_, res) {
102220
// 1. get GitHub avatar and artifact store
103221
await updateAvatar();

backend/src/utils/env.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,6 @@ try {
4444
meta.plugin = $Plugin;
4545
}
4646
if (isNode) {
47-
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
48-
if (secret && eval('process.env.SUB_STORE_FRONTEND_PATH')) {
49-
feature.share = true;
50-
}
5147
meta.node = {
5248
version: eval('process.version'),
5349
argv: eval('process.argv'),

0 commit comments

Comments
 (0)