mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 18:34:51 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57fb5ec606 |
No files matched your search
@@ -24,11 +24,7 @@ import { computeNetworkFingerprint } from '../../core/http/middleware/rateLimit.
|
||||
import type { PuterRouter } from '../../core/http/PuterRouter.js';
|
||||
import { PuterController } from '../types.js';
|
||||
import { PEER_COSTS } from './costs.js';
|
||||
import {
|
||||
readClaimedGrantIdentifier,
|
||||
signGuestGrant,
|
||||
verifyGuestGrant,
|
||||
} from './guestGrant.js';
|
||||
import { signGuestGrant, verifyGuestGrant } from './guestGrant.js';
|
||||
import {
|
||||
DEFAULT_FREE_SUBSCRIPTION,
|
||||
DEFAULT_TEMP_SUBSCRIPTION,
|
||||
@@ -191,19 +187,30 @@ export class PeerController extends PuterController {
|
||||
// guests share one bucket and no host can be relayed for by
|
||||
// more guests per minute than this — the only ceiling on guest
|
||||
// spend we can apply before the bytes are already spent.
|
||||
// A grant that doesn't parse can't name a bucket, so those
|
||||
// requests fall back to the caller's own network.
|
||||
// The limiter counts a request when it admits it, so only a
|
||||
// grant that would redeem names a host; otherwise anyone
|
||||
// holding the host's identifier could drain the bucket and
|
||||
// lock out every real guest. Anything else - forged, expired,
|
||||
// malformed, or no secret configured - is charged to the
|
||||
// caller's own network.
|
||||
rateLimit: {
|
||||
scope: 'peer-guest-turn',
|
||||
limit: 60,
|
||||
window: 60_000,
|
||||
key: (req: Request) => {
|
||||
const claimed = readClaimedGrantIdentifier(
|
||||
(req.body as { grant?: unknown } | undefined)
|
||||
?.grant,
|
||||
);
|
||||
return claimed
|
||||
? `host:${claimed}`
|
||||
const secret =
|
||||
this.config.peers?.guest_turn?.grant_secret;
|
||||
const verified = secret
|
||||
? verifyGuestGrant({
|
||||
grant: (
|
||||
req.body as
|
||||
{ grant?: unknown } | undefined
|
||||
)?.grant,
|
||||
secret,
|
||||
})
|
||||
: null;
|
||||
return verified?.status === 'ok'
|
||||
? `host:${verified.customIdentifier}`
|
||||
: `net:${computeNetworkFingerprint(req)}`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { toMicroCents } from '../../services/metering/utils';
|
||||
|
||||
// Microcents per byte of TURN egress ($0.05/GB).
|
||||
// Microcents per byte of TURN egress ($0.10/GB).
|
||||
export const PEER_COSTS = {
|
||||
'turn:egress-bytes': toMicroCents(0.1 / 1000 ** 3),
|
||||
} as const;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createHmac } from 'node:crypto';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
readClaimedGrantIdentifier,
|
||||
signGuestGrant,
|
||||
verifyGuestGrant,
|
||||
} from './guestGrant.js';
|
||||
@@ -243,59 +242,3 @@ describe('signGuestGrant / verifyGuestGrant', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('readClaimedGrantIdentifier', () => {
|
||||
it('reads the identifier from a valid grant', () => {
|
||||
const { grant } = signGuestGrant({
|
||||
customIdentifier: USER_ID,
|
||||
ttlSeconds: 60,
|
||||
secret: SECRET,
|
||||
});
|
||||
|
||||
expect(readClaimedGrantIdentifier(grant)).toBe(USER_ID);
|
||||
});
|
||||
|
||||
it('reads the claimed identifier without checking the signature', () => {
|
||||
// Bucketing runs before verification, so this is expected: a forged
|
||||
// grant still names a bucket, and the handler still rejects it.
|
||||
const grant = forgeGrant(
|
||||
{ id: USER_ID, exp: 9_999_999_999, n: 'x' },
|
||||
{ secret: 'wrong-secret' },
|
||||
);
|
||||
|
||||
expect(readClaimedGrantIdentifier(grant)).toBe(USER_ID);
|
||||
});
|
||||
|
||||
it('reads an expired grant, which the handler then rejects', () => {
|
||||
const { grant } = signGuestGrant({
|
||||
customIdentifier: USER_ID,
|
||||
ttlSeconds: 60,
|
||||
secret: SECRET,
|
||||
now: 1_000_000_000_000,
|
||||
});
|
||||
|
||||
expect(readClaimedGrantIdentifier(grant)).toBe(USER_ID);
|
||||
expect(verifyGuestGrant({ grant, secret: SECRET })).toEqual({
|
||||
status: 'expired',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a non-string', 42],
|
||||
['undefined', undefined],
|
||||
['a wrong-shaped string', 'not-a-grant'],
|
||||
['an unknown version', 'pg2.abc.def'],
|
||||
['an over-long string', `pg1.${'a'.repeat(600)}.sig`],
|
||||
[
|
||||
'a non-JSON payload',
|
||||
`pg1.${Buffer.from('x').toString('base64url')}.sig`,
|
||||
],
|
||||
])('returns null for %s', (_label, grant) => {
|
||||
expect(readClaimedGrantIdentifier(grant)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for an identifier of the wrong shape', () => {
|
||||
const grant = forgeGrant({ id: 'nope', exp: 9_999_999_999 });
|
||||
expect(readClaimedGrantIdentifier(grant)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -171,37 +171,3 @@ export const verifyGuestGrant = ({
|
||||
expiresAt: payload.exp,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Read the claimed identifier out of a grant _without_ verifying it, for
|
||||
* rate-limit bucketing only.
|
||||
*
|
||||
* Bucketing has to happen before the handler runs, and a forged grant is
|
||||
* rejected there without reaching the upstream API — so an unverified read is
|
||||
* enough to put a host's guests in one bucket, and the worst a forger achieves
|
||||
* is choosing which bucket their own rejections land in. Never use this to
|
||||
* decide attribution.
|
||||
*
|
||||
* @returns The claimed identifier, or null if the grant doesn't parse.
|
||||
*/
|
||||
export const readClaimedGrantIdentifier = (grant: unknown): string | null => {
|
||||
if (typeof grant !== 'string' || grant.length > MAX_GRANT_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
const parts = grant.split('.');
|
||||
if (parts.length !== 3 || parts[0] !== GRANT_VERSION) return null;
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(parts[1]!, 'base64url').toString('utf8'),
|
||||
) as { id?: unknown };
|
||||
if (
|
||||
typeof payload?.id !== 'string' ||
|
||||
!IDENTIFIER_RE.test(payload.id)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return payload.id;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user