Метки потоков (labels)
Как выдать доступ сразу группе потоков одной строкой в токене — и почему uuid с дефисами в JWT даёт 403.
Выдавать права по имени потока удобно, пока потоков десять. На тысяче камер токен, перечисляющий пути, становится неуправляемым: каждое добавление камеры требует перевыпуска всех токенов.
Метка решает это: непрозрачная строка, приклеенная к потоку. Право в токене выдаётся на метку, а не на путь, — и новая камера с той же меткой становится доступна без единого нового токена.
Ядро не приписывает метке никакого смысла: это не организация, не тег и не иерархия. Что она означает, решаете вы.
Как назначить метку потоку#
stream camA { input rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101; labels site-north tier-gold <UUID>; }
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
labels |
список строк через пробел; ≤ 40 меток на поток; каждая 1…64 байта; без пробелов и управляющих символов | [] — меток нет |
горячее |
Метки — единственная часть блока потока, которая не входит в его отпечаток перезапуска. Поэтому их правка применяется вживую: поток не переподключается, зрители не разрываются, а снятая метка перестаёт авторизовывать со следующего медиа-запроса.
Форма потока → вкладка Access → поле Authorization labels. Там же живёт allowed_referers.
Сохранение применяется горячо, ровно как правка файла.
Главная ловушка: uuid канонизируется в конфигурации, но не в токене#
Если метка выглядит как uuid, сервер приводит её к компактной 22-символьной форме base64url — чтобы два написания одного и того же uuid были одной идентичностью. Это преобразование применяется к меткам потоков и к меткам в конфигурации, но НЕ к меткам внутри JWT: их сравнивают байт в байт с тем, что уже лежит в памяти сервера.
в конфигурации: labels 3f2504e0-4f89-11d3-9a0c-0305e82c3301; в памяти: PyUE4E-JEdOaDAMF6CwzAQ в токене нужна: PyUE4E-JEdOaDAMF6CwzAQ ← а не форма с дефисами
Проверено на стенде, поток с меткой-uuid из примера выше:
| Где записана метка | Форма записи | Результат |
|---|---|---|
конфигурация: permission read label=3f2504e0-…-3301 |
с дефисами | 200 |
конфигурация: permission read label=PyUE4E-JEdOaDAMF6CwzAQ |
b64u22 | 200 |
JWT: "labels":["3f2504e0-…-3301"] |
с дефисами | 403 |
JWT: "labels":["PyUE4E-JEdOaDAMF6CwzAQ"] |
b64u22 | 200 |
Преобразование — это base64url без выравнивания от 16 байт uuid:
python3 -c " import base64, sys u = sys.argv[1].replace('-', '') print(base64.urlsafe_b64encode(bytes.fromhex(u)).rstrip(b'=').decode()) " 3f2504e0-4f89-11d3-9a0c-0305e82c3301
PyUE4E-JEdOaDAMF6CwzAQ
Токен подписан верно, срок не истёк, поток на месте — а ответ 403. Первым делом сверьте форму
метки в токене с той, что сервер держит в памяти. Готовая функция label_from_uuid() есть в обоих
скриптах на странице Генераторы токенов.
Если вы не используете uuid в метках, ловушки нет: site-north остаётся site-north везде.
Две формы права по метке#
Форма 1 — обычное разрешение с полем labels#
Выдаёт действие (read, playback, publish):
{"action":"read","labels":["site-north"],"protocols":["hls"]}
Совпадение — точное строковое равенство с любой из меток потока. Ни глоба, ни префикса, ни
регистронезависимости: site-* и SITE-NORTH не совпадут с site-north (проверено, 403).
Поле path можно не указывать вовсе — тогда правило работает исключительно по меткам.
Форма 2 — компактный грант-строка#
Выдаёт биты привилегий (live, dvr, export, preview) и записывается одной строкой
<метка>.<маска-hex>:
{"grants":["PyUE4E-JEdOaDAMF6CwzAQ.03", "*.20"]}
Метка здесь обязана быть корректной 22-символьной формой base64url; * означает «любой поток».
Некорректная строка молча отбрасывается — токен остаётся валидным, но это право в нём просто
не считается.
| Маска | Биты | Что открывает |
|---|---|---|
01 |
live |
живой просмотр любым протоколом |
02 |
dvr |
DVR, таймшифт, VoD |
03 |
live + dvr |
живое и архив |
10 |
export |
скачивание клипа |
20 |
preview |
только preview.mp4 |
33 |
всё перечисленное | максимум, который сервер умеет проверять |
Обе формы можно смешивать в одном массиве прав.
Только тогда, когда права надо разрезать внутри архива: выдать «скачать клип, но не смотреть».
Обычное разрешение с action: playback открывает и просмотр архива, и экспорт одновременно, потому
что путь совпал. Компактный грант *.10 даёт ровно экспорт.
Сопоставление: всюду ИЛИ#
| Ситуация | Правило |
|---|---|
| у потока несколько меток | достаточно совпасть одной |
| в одном разрешении несколько меток | достаточно совпасть одной |
| в токене несколько разрешений | достаточно сработать одному |
| несколько компактных грантов | маски объединяются |
| путь и метки внутри одного разрешения | совпал путь или совпала метка |
Пересечения (И) в системе меток нет нигде. Сужают доступ только action, protocols, привязка к
IP и замок Referer из токена — но не метки.
Отсюда важное: приоритета у путей и меток нет и запретить меткой нельзя. Метка умеет только добавлять доступ.
Поток без меток#
Множество меток пусто, поэтому:
- правило только по меткам такому потоку не подойдёт —
403; - правило по пути (
path) работает как обычно; - компактный грант
*.<маска>сработает: подстановочный знак пересекается с любым набором меток, включая пустой.
Метки и public_paths#
Механизмы независимы и срабатывают на разных этапах. public_paths проверяется первым и
полностью снимает авторизацию для пути; метки участвуют позже, когда бэкенд уже разбирает грант.
Практически это значит: метка не может сделать поток публичным, а public_paths не смотрит на
метки — только на путь. Есть и разница в кэшировании: путь из public_paths сохраняет кэшируемость
сегментов, «открытый меткой» — нет (см. Авторизация просмотра).
Разобранные примеры#
Конфигурация во всех примерах:
auth_backend { type hs256; secret "секрет-подписи-токенов-зрителей"; } stream camA { input rtsp://10.0.0.10:554/live; labels <UUID> site-north; } stream camB { input rtsp://10.0.0.11:554/live; labels site-north; } stream camC { input rtsp://10.0.0.12:554/live; }
где <UUID> = 3f2504e0-4f89-11d3-9a0c-0305e82c3301. После загрузки конфигурации метки camA —
это ["PyUE4E-JEdOaDAMF6CwzAQ", "site-north"]: uuid канонизирован, набор отсортирован и очищен от
повторов.
Пример 1 — доступ к группе по общей метке#
{"exp":1787200000,"grants":[{"action":"read","labels":["site-north"]}]}
| Поток | Вердикт | Почему |
|---|---|---|
| camA | 200 |
site-north есть в метках |
| camB | 200 |
site-north есть в метках |
| camC | 403 |
меток нет, пути в правиле тоже нет |
Пример 2 — метка-uuid записана не в той форме#
{"exp":1787200000,"grants":[{"action":"read","labels":["3f2504e0-4f89-11d3-9a0c-0305e82c3301"]}]}
| Поток | Вердикт | Почему |
|---|---|---|
| camA | 403 |
у потока метка уже PyUE4E-JEdOaDAMF6CwzAQ; строки не равны |
| camB, camC | 403 |
такой метки у них нет |
Тот же uuid в форме ["PyUE4E-JEdOaDAMF6CwzAQ"] → camA 200, camB и camC 403.
Пример 3 — путь и метка в одном разрешении складываются по ИЛИ#
{"exp":1787200000,"grants":[{"action":"read","path":"camB","labels":["PyUE4E-JEdOaDAMF6CwzAQ"]}]}
| Поток | Вердикт | Почему |
|---|---|---|
| camA | 200 |
путь не совпал, но совпала метка |
| camB | 200 |
метка не совпала, но совпал путь |
| camC | 403 |
не совпало ничего |
Пример 4 — компактные гранты разрезают привилегии#
{"exp":1787200000,"grants":["*.02"]} // только dvr {"exp":1787200000,"grants":["*.10"]} // только export {"exp":1787200000,"grants":["*.12"]} // dvr и export
| Токен | timeshift-…m3u8 |
dvr/clip?from&to |
index.m3u8 (живое) |
|---|---|---|---|
*.02 |
200 |
403 |
403 |
*.10 |
403 |
200 |
403 |
*.12 |
200 |
200 |
403 |
Живой просмотр требует бита live (01) — ни dvr, ни export его не дают. Это единственный
способ выдать право «только скачать клип» без права смотреть.
Проверка#
Выпустите токен по метке (см. пресет label в генераторе) и запросите плейлист
потока, у которого эта метка есть, и потока, у которого её нет.
curl -s -o /dev/null -w '%{http_code}\n' \ "http://media.example.com:8080/camA/index.m3u8?token=$TOKEN" curl -s -o /dev/null -w '%{http_code}\n' \ "http://media.example.com:8080/camC/index.m3u8?token=$TOKEN"
200 403
403 на обоих потоках при живом camA — почти наверняка форма метки: сверьте её с выводом
label_from_uuid().