| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- import crypto from 'node:crypto';
- import { safeEqual } from '../libs/auth.js';
- const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;
- const usedNonces = new Map();
- const normalizePath = (path) => {
- if (!path || path === '/') {
- return '/';
- }
- return path.endsWith('/') ? path.slice(0, -1) : path;
- };
- const stableStringify = (value) => {
- if (Array.isArray(value)) {
- return `[${value.map(stableStringify).join(',')}]`;
- }
- if (value && typeof value === 'object') {
- return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
- }
- return JSON.stringify(value);
- };
- const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
- const getUsers = () => {
- return process.env.PINNACLE_PROXY_USERS?.split(';')
- .map(item => item.trim())
- .filter(Boolean)
- .map(item => {
- const [username, secret, permissions=''] = item.split(':');
- return {
- permissions: permissions.split(',').map(permission => normalizePath(permission.trim())).filter(Boolean),
- secret,
- username,
- };
- })
- .filter(user => user.username && user.secret) ?? [];
- };
- const getUser = (username) => getUsers().find(user => safeEqual(user.username, username));
- const cleanupUsedNonces = () => {
- const now = Date.now();
- usedNonces.forEach((expiresAt, key) => {
- if (expiresAt <= now) {
- usedNonces.delete(key);
- }
- });
- };
- const checkNonce = (username, nonce) => {
- cleanupUsedNonces();
- const key = `${username}:${nonce}`;
- if (usedNonces.has(key)) {
- return false;
- }
- usedNonces.set(key, Date.now() + MAX_CLOCK_SKEW_MS);
- return true;
- };
- const hasPermission = (user, path) => {
- const targetPath = normalizePath(path);
- return user.permissions.includes('*') || user.permissions.includes(targetPath);
- };
- const getBodyHash = (body) => {
- if (body === undefined || body === null || Object.keys(body).length === 0) {
- return sha256('');
- }
- return sha256(stableStringify(body));
- };
- const buildSigningPayload = (req, timestamp, nonce) => [
- req.method.toUpperCase(),
- normalizePath(req.path),
- stableStringify(req.query ?? {}),
- getBodyHash(req.body),
- timestamp,
- nonce,
- ].join('\n');
- const requirePinnacleProxyAuth = (req, res, next) => {
- const username = req.get('x-api-user');
- const timestamp = req.get('x-api-timestamp');
- const nonce = req.get('x-api-nonce');
- const signature = req.get('x-api-signature');
- if (!username || !timestamp || !nonce || !signature) {
- return res.unauthorized('Missing API signature headers');
- }
- const requestTime = Number(timestamp);
- if (!Number.isFinite(requestTime) || Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
- return res.unauthorized('Invalid API timestamp');
- }
- const user = getUser(username);
- if (!user) {
- return res.unauthorized('Invalid API user');
- }
- if (!hasPermission(user, req.path)) {
- return res.status(403).json({ statusCode: 403, code: -1, message: 'Forbidden' });
- }
- const payload = buildSigningPayload(req, timestamp, nonce);
- const expectedSignature = crypto.createHmac('sha256', user.secret).update(payload).digest('hex');
- if (!safeEqual(signature, expectedSignature)) {
- return res.unauthorized('Invalid API signature');
- }
- if (!checkNonce(username, nonce)) {
- return res.unauthorized('API nonce has already been used');
- }
- req.pinnacleProxyUser = { username };
- return next();
- };
- export default requirePinnacleProxyAuth;
|