[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/JavaScriptSolidServer/plugins/gh-pages/turn/plugin.js [Back]  [Original]

// TURN REST credentials (draft-uberti-behave-turn-rest-00) as a #206 loader
// plugin  the JSS half of a coturn deployment. coturn relays media; this
// endpoint mints the time-limited credentials browsers use to reach it, so
// no static TURN password ever ships in a public page (a leaked credential
// expires by itself).
//
//   plugins: [{ module: 'turn/plugin.js', prefix: '/.turn',
//               config: { uris: ['turn:pod.example:3478?transport=udp',
//                                'turns:pod.example:5349'],
//                         ttl: 3600 } }]
//
// Serves `GET /credentials[?user=]` 
//   { username, password, ttl, uris,               // the draft's shape
//     iceServers: [{ urls, username, credential }] // drop-in for RTCPeerConnection
//   }
// where username = "[:]" and
// password = base64(HMAC-SHA1(secret, username))  exactly what coturn
// verifies under `use-auth-secret` / `static-auth-secret`.
//
// The secret comes from config.secret, or the environment variable named by
// config.secretEnv (default TURN_STATIC_AUTH_SECRET)  the env path exists
// because a CLI `--plugin module@prefix` entry carries no config object.
// `uris` may likewise come from a comma-separated TURN_URIS. Activation
// fails loudly when either is missing: a server that would mint credentials
// nobody can use (or sign them with '') must not boot quietly.
//
// Minting is anonymous by default  the point of expiry is that handing
// creds to any visitor is survivable, and the mesh pages that need them run
// without accounts. Set config.requireAuth to gate on api.auth.getAgent
// (401 for anonymous) when the relay should serve only known agents.

import crypto from 'node:crypto';

const DEFAULT_TTL = 3600; // seconds; short  clients re-mint per session
const USER_TAG = /^[A-Za-z0-9._-]{1,64}$/;

export async function activate(api) {
  const cfg = api.config || {};
  const secret = cfg.secret
    || process.env[cfg.secretEnv || 'TURN_STATIC_AUTH_SECRET']
    || '';
  if (!secret) {
    throw new Error('turn plugin: no secret  set config.secret or the '
      + `${cfg.secretEnv || 'TURN_STATIC_AUTH_SECRET'} environment variable`);
  }
  const uris = (Array.isArray(cfg.uris) && cfg.uris.length ? cfg.uris
    : String(process.env.TURN_URIS || '').split(',').map((s) => s.trim()).filter(Boolean));
  if (!uris.length) {
    throw new Error('turn plugin: no TURN uris  set config.uris or TURN_URIS');
  }
  const ttl = Number.isInteger(cfg.ttl) && cfg.ttl > 0 ? cfg.ttl : DEFAULT_TTL;
  const requireAuth = !!cfg.requireAuth;

  api.fastify.get(`${api.prefix}/credentials`, async (request, reply) => {
    if (requireAuth) {
      const agent = await api.auth.getAgent(request);
      if (!agent) return reply.code(401).send({ error: 'Unauthorized' });
    }
    const tag = request.query?.user;
    if (tag !== undefined && !USER_TAG.test(String(tag))) {
      return reply.code(400).send({ error: 'BadRequest', message: 'user must match [A-Za-z0-9._-]{1,64}' });
    }
    const username = `${Math.floor(Date.now() / 1000) + ttl}${tag ? ':' + tag : ''}`;
    const password = crypto.createHmac('sha1', secret).update(username).digest('base64');
    reply.header('cache-control', 'no-store'); // every response is a fresh, expiring grant
    return {
      username, password, ttl, uris,
      iceServers: [{ urls: uris, username, credential: password }],
    };
  });

  api.log.info(`turn: minting ${ttl}s credentials at ${api.prefix}/credentials for ${uris.length} uri(s)`);
}

Web Proxy Viewer  |  New URL  |  Original Page