Skip to content

Commit

Permalink
feat(wip): 支持自定义 share token
Browse files Browse the repository at this point in the history
  • Loading branch information
xream committed Oct 31, 2024
1 parent d12ccad commit 902d078
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 196 deletions.
9 changes: 4 additions & 5 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.14.410",
"version": "2.14.411",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
Expand Down Expand Up @@ -28,13 +28,12 @@
"http-proxy-middleware": "^2.0.6",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2",
"jsonwebtoken": "^9.0.2",
"jsrsasign": "^11.1.0",
"lodash": "^4.17.21",
"ms": "^2.1.3",
"nanoid": "^3.3.3",
"request": "^2.88.2",
"semver": "^7.3.7",
"static-js-yaml": "^1.0.0",
"uuid": "^8.3.2"
"static-js-yaml": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.0",
Expand Down
157 changes: 22 additions & 135 deletions backend/pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules';
export const TOKENS_KEY = 'tokens';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
Expand Down
3 changes: 3 additions & 0 deletions backend/src/restful/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { getISO } from '@/utils/geo';
import env from '@/utils/env';

export default function register($app) {
$app.get('/share/col/:name', downloadCollection);
$app.get('/share/sub/:name', downloadSubscription);

$app.get('/download/collection/:name', downloadCollection);
$app.get('/download/:name', downloadSubscription);
$app.get(
Expand Down
2 changes: 2 additions & 0 deletions backend/src/restful/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { produceArtifact } from '@/restful/sync';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);

$app.get('/share/file/:name', getFile);

$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
Expand Down
36 changes: 10 additions & 26 deletions backend/src/restful/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import migrate from '@/utils/migration';
import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants';

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

const jwt = eval(`require("jsonwebtoken")`);

app.use(
be_share_rewrite,
createProxyMiddleware({
Expand All @@ -186,31 +185,16 @@ export default function serve() {
pathRewrite: (path, req) => {
if (req.method.toLowerCase() !== 'get')
throw new Error('Method not allowed');
const payload = jwt.verify(
req.query.token,
fe_be_path,
const tokens = $.read(TOKENS_KEY) || [];
const token = tokens.find(
(t) =>
t.token === req.query.token &&
t.type === req.params.type &&
t.name === req.params.name &&
(t.exp == null || t.exp > Date.now()),
);
if (
payload.type !== req.params.type ||
payload.name !== req.params.name
)
throw new Error('Forbbiden');
if (payload.type === 'sub')
return path.replace(
'/share/sub/',
'/download/',
);
if (payload.type === 'col')
return path.replace(
'/share/col/',
'/download/collection/',
);
if (payload.type === 'file')
return path.replace(
'/share/file/',
'/api/file/',
);
throw new Error('Not Found');
if (!token) throw new Error('Forbbiden');
return path;
},
}),
);
Expand Down
174 changes: 144 additions & 30 deletions backend/src/restful/miscs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import {
GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY,
SETTINGS_KEY,
TOKENS_KEY,
FILES_KEY,
COLLECTIONS_KEY,
SUBS_KEY,
} from '@/constants';
import { InternalServerError, RequestInvalidError } from '@/restful/errors';
import Gist from '@/utils/gist';
Expand All @@ -20,36 +24,7 @@ export default function register($app) {
$app.get('/api/utils/env', getEnv); // get runtime environment
$app.get('/api/utils/backup', gistBackup); // gist backup actions
$app.get('/api/utils/refresh', refresh);
$app.post('/api/jwt', (req, res) => {
if (!ENV().isNode) {
return failed(
res,
new RequestInvalidError(
'INVALID_ENV',
`This endpoint is only available in Node.js environment`,
),
);
}
try {
const { payload, options } = req.body;
const jwt = eval(`require("jsonwebtoken")`);
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const token = jwt.sign(payload, secret, options);
return success(res, {
token,
secret,
});
} catch (e) {
return failed(
res,
new InternalServerError(
'JWT_SIGN_FAILED',
`Failed to sign JWT token`,
`Reason: ${e.message ?? e}`,
),
);
}
});
$app.post('/api/token', signToken);

// Storage management
$app.route('/api/storage')
Expand Down Expand Up @@ -98,6 +73,145 @@ function getEnv(req, res) {
success(res, env);
}

async function signToken(req, res) {
if (!ENV().isNode) {
return failed(
res,
new RequestInvalidError(
'INVALID_ENV',
`This endpoint is only available in Node.js environment`,
),
);
}
try {
const { payload, options } = req.body;
const ms = eval(`require("ms")`);
let token = payload?.token;
if (token != null) {
if (typeof token !== 'string' || token.length < 1) {
return failed(
res,
new RequestInvalidError(
'INVALID_CUSTOM_TOKEN',
`Invalid custom token: ${token}`,
),
);
}
const tokens = $.read(TOKENS_KEY) || [];
if (tokens.find((t) => t.token === token)) {
return failed(
res,
new RequestInvalidError(
'DUPLICATE_TOKEN',
`Token ${token} already exists`,
),
);
}
}
const type = payload?.type;
const name = payload?.name;
if (!type || !name)
return failed(
res,
new RequestInvalidError(
'INVALID_PAYLOAD',
`payload type and name are required`,
),
);
if (type === 'col') {
const collections = $.read(COLLECTIONS_KEY) || [];
const collection = collections.find((c) => c.name === name);
if (!collection)
return failed(
res,
new RequestInvalidError(
'INVALID_COLLECTION',
`collection ${name} not found`,
),
);
} else if (type === 'file') {
const files = $.read(FILES_KEY) || [];
const file = files.find((f) => f.name === name);
if (!file)
return failed(
res,
new RequestInvalidError(
'INVALID_FILE',
`file ${name} not found`,
),
);
} else if (type === 'sub') {
const subs = $.read(SUBS_KEY) || [];
const sub = subs.find((s) => s.name === name);
if (!sub)
return failed(
res,
new RequestInvalidError(
'INVALID_SUB',
`sub ${name} not found`,
),
);
} else {
return failed(
res,
new RequestInvalidError(
'INVALID_TYPE',
`type ${name} not supported`,
),
);
}
let expiresIn = options?.expiresIn;
if (options?.expiresIn != null) {
expiresIn = ms(options.expiresIn);
if (expiresIn == null || isNaN(expiresIn) || expiresIn <= 0) {
return failed(
res,
new RequestInvalidError(
'INVALID_EXPIRES_IN',
`Invalid expiresIn option: ${options.expiresIn}`,
),
);
}
}
const secret = eval('process.env.SUB_STORE_FRONTEND_BACKEND_PATH');
const nanoid = eval(`require("nanoid")`);
const tokens = $.read(TOKENS_KEY) || [];
// const now = Date.now();
// for (const key in tokens) {
// const token = tokens[key];
// if (token.exp != null || token.exp < now) {
// delete tokens[key];
// }
// }
if (!token) {
do {
token = nanoid.customAlphabet(nanoid.urlAlphabet)();
} while (tokens.find((t) => t.token === token));
}

tokens.push({
...payload,
token,
expiresIn: expiresIn > 0 ? ms(expiresIn) : undefined,
exp: expiresIn > 0 ? Date.now() + expiresIn : undefined,
});

$.write(tokens, TOKENS_KEY);
return success(res, {
token,
secret,
});
} catch (e) {
return failed(
res,
new InternalServerError(
'TOKEN_SIGN_FAILED',
`Failed to sign token`,
`Reason: ${e.message ?? e}`,
),
);
}
}
async function refresh(_, res) {
// 1. get GitHub avatar and artifact store
await updateAvatar();
Expand Down

0 comments on commit 902d078

Please sign in to comment.