mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-15 19:57:07 +08:00
214 lines
6.5 KiB
JavaScript
214 lines
6.5 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const express = require('express');
|
|
const jwt = require('jsonwebtoken');
|
|
const load = require('../helpers/load-security-module.cjs');
|
|
|
|
test('HTTP authentication protects init, scopes, expired sessions and config secrets', async (t) => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ql-http-security-'));
|
|
for (const dir of ['config/grpc', 'scripts', 'upload', 'tmp'])
|
|
fs.mkdirSync(path.join(tmp, dir), { recursive: true });
|
|
fs.writeFileSync(path.join(tmp, 'config/grpc/client.key'), 'SENTINEL');
|
|
fs.writeFileSync(path.join(tmp, 'config/normal.txt'), 'normal');
|
|
const secret = 'http-security-test';
|
|
const valid = jwt.sign({}, secret, { algorithm: 'HS384', expiresIn: '1h' });
|
|
const expired = jwt.sign({}, secret, { algorithm: 'HS384', expiresIn: -1 });
|
|
const auth = {
|
|
username: 'owner',
|
|
password: 'configured',
|
|
token: valid,
|
|
tokens: { desktop: [{ value: expired }] },
|
|
};
|
|
const configSource = fs.readFileSync(
|
|
path.join(__dirname, '../../back/config/index.ts'),
|
|
'utf8',
|
|
);
|
|
const whitelistSource = configSource
|
|
.slice(configSource.indexOf('apiWhiteList:'))
|
|
.match(/apiWhiteList:\s*\[([\s\S]*?)\]/)[1];
|
|
const config = {
|
|
api: { prefix: '/api' },
|
|
apiWhiteList: [...whitelistSource.matchAll(/['"]([^'"]+)['"]/g)].map(
|
|
(x) => x[1],
|
|
),
|
|
jwt: { secret },
|
|
rootPath: tmp,
|
|
configPath: path.join(tmp, 'config/'),
|
|
scriptPath: path.join(tmp, 'scripts/'),
|
|
uploadPath: path.join(tmp, 'upload'),
|
|
tmpPath: path.join(tmp, 'tmp'),
|
|
blackFileList: ['auth.json', 'grpc'],
|
|
baseUrl: '/panel',
|
|
};
|
|
const apps = [
|
|
{
|
|
scopes: ['configs'],
|
|
tokens: [{ value: 'config-app', expiration: Date.now() / 1000 + 3600 }],
|
|
},
|
|
{
|
|
scopes: ['envs'],
|
|
tokens: [{ value: 'env-app', expiration: Date.now() / 1000 + 3600 }],
|
|
},
|
|
];
|
|
let initialized = 0;
|
|
const User = class {};
|
|
const user = {
|
|
initializeUser: async () => {
|
|
initialized++;
|
|
return { code: 200 };
|
|
},
|
|
getAuthInfo: async () => auth,
|
|
};
|
|
const mocks = {
|
|
'../config': config,
|
|
'../config/util': {
|
|
getToken: (r) => (r.headers.authorization || '').replace(/^Bearer /, ''),
|
|
getPlatform: () => 'desktop',
|
|
getFileContentByName: (p) => fs.promises.readFile(p, 'utf8'),
|
|
},
|
|
'../shared/i18n': { t: (x) => x },
|
|
'../shared/store': {
|
|
shareStore: { getAuthInfo: async () => auth, getApps: async () => apps },
|
|
},
|
|
'../config/serverEnv': { serveEnv: (_req, res) => res.end() },
|
|
'../services/user': User,
|
|
'../data/open': {},
|
|
'../data/system': {},
|
|
'../shared/utils': {
|
|
writeFileWithLock: (p, content) => fs.promises.writeFile(p, content),
|
|
},
|
|
};
|
|
const Config = load(path.join(__dirname, '../../back/services/config.ts'), {
|
|
...mocks,
|
|
typedi: { Service: () => (x) => x },
|
|
}).default;
|
|
const configService = new Config();
|
|
mocks['../services/config'] = Config;
|
|
mocks.typedi = {
|
|
Container: { get: (x) => (x === User ? user : configService) },
|
|
};
|
|
mocks['../api'] = () => {
|
|
const router = express.Router();
|
|
load(path.join(__dirname, '../../back/api/user.ts'), mocks).default(router);
|
|
load(path.join(__dirname, '../../back/api/config.ts'), mocks).default(
|
|
router,
|
|
);
|
|
router.get('/envs', (_req, res) => res.json({ code: 200 }));
|
|
return router;
|
|
};
|
|
const app = express();
|
|
load(path.join(__dirname, '../../back/loaders/express.ts'), mocks).default({
|
|
app,
|
|
});
|
|
const server = app.listen(0, '127.0.0.1');
|
|
t.after(async () => {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
server.on('listening', resolve);
|
|
server.on('error', reject);
|
|
});
|
|
const request = async (url, token, method = 'GET', body) => {
|
|
const response = await fetch(
|
|
`http://127.0.0.1:${server.address().port}${url}`,
|
|
{
|
|
method,
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
'Content-Type': 'application/json',
|
|
},
|
|
...(body ? { body: JSON.stringify(body) } : {}),
|
|
},
|
|
);
|
|
return { status: response.status, body: await response.json() };
|
|
};
|
|
for (const url of [
|
|
'/api/user/init',
|
|
'/open/user/init',
|
|
'/panel/api/user/init',
|
|
'/panel/open/user/init',
|
|
]) {
|
|
const response = await request(url, undefined, 'PUT', {
|
|
username: 'attacker',
|
|
password: 'changed',
|
|
});
|
|
assert.equal(response.body.code, 450, url);
|
|
}
|
|
assert.equal(initialized, 0);
|
|
assert.equal(
|
|
(
|
|
await request('/api/user/login', undefined, 'POST', {
|
|
username: 'x',
|
|
password: 'x'.repeat(20000),
|
|
})
|
|
).status,
|
|
413,
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmp, 'upload/legacy.html'),
|
|
'<script>window.test=1</script>',
|
|
);
|
|
const legacyUpload = await fetch(
|
|
`http://127.0.0.1:${server.address().port}/api/static/legacy.html`,
|
|
);
|
|
assert.equal(legacyUpload.headers.get('x-content-type-options'), 'nosniff');
|
|
assert.equal(
|
|
legacyUpload.headers.get('content-security-policy'),
|
|
"sandbox; default-src 'none'",
|
|
);
|
|
for (const prefix of ['/api', '/open', '/panel/api', '/panel/open']) {
|
|
assert.equal(
|
|
(await request(`${prefix}/envs`, expired)).status,
|
|
401,
|
|
prefix,
|
|
);
|
|
assert.equal(
|
|
(await request(`${prefix}/envs`, valid)).body.code,
|
|
200,
|
|
prefix,
|
|
);
|
|
assert.equal((await request(`${prefix}/envs`)).status, 401, prefix);
|
|
}
|
|
assert.equal((await request('/open/envs', 'env-app')).body.code, 200);
|
|
assert.equal(
|
|
(await request('/open/configs/detail?path=normal.txt', 'env-app')).status,
|
|
401,
|
|
);
|
|
assert.equal(
|
|
(await request('/open/configs/detail?path=normal.txt', 'config-app')).body
|
|
.data,
|
|
'normal',
|
|
);
|
|
assert.equal(
|
|
(await request('/open/configs/detail?path=grpc/client.key', 'config-app'))
|
|
.body.code,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request('/open/configs/save', 'config-app', 'POST', {
|
|
name: 'grpc/client.key',
|
|
content: 'changed',
|
|
})
|
|
).body.code,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
fs.readFileSync(path.join(tmp, 'config/grpc/client.key'), 'utf8'),
|
|
'SENTINEL',
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request('/Api/user/init', undefined, 'PUT', {
|
|
username: 'x',
|
|
password: 'y',
|
|
})
|
|
).status,
|
|
400,
|
|
);
|
|
});
|