Обзор веб-перехватчиков

Пример приложения

Чтобы создать собственный потребитель для веб-перехватчиков Frame.io, можете использовать и расширять наш образец приложения на Github.

Введение

Веб-перехватчики предоставляют способ использования событий, происходящих внутри Frame.io, в качестве уведомлений, которые можно отправить во внешние системы для обработки, обратные вызовы API-интерфейса и автоматизации рабочих процессов.

Настройка

Веб-перехватчики можно настроить в области веб-перехватчиков на сайте для разработчиков. Для веб-перехватчика требуется:

  • Название — будет отображаться только на сайте для разработчиков.
  • URL-адрес — место доставки событий.
  • Команда — к какой команде будет добавлен этот веб-перехватчик.
  • События — какое событие или какие события должны запускать веб-перехватчик.

Поддерживаемые события

Один веб-перехватчик может подписаться на любое количество следующих событий:

Проекты

СобытиеТриггер
project.createdСоздан новый проект
project.updatedОбновлены настройки проекта
project.deletedПроект удален

Ресурсы

СобытиеТриггер
asset.createdРесурс впервые добавляется/создается в Frame.io, но, вероятно, до того, как ресурс будет полностью добавлен
asset.copiedРесурс скопирован
asset.updatedИзменяется описание и название ресурса или другая информация о файле
asset.deletedРесурс удален (вручную или иным способом)
asset.readyВсе транскодирования завершены после добавления и обработки ресурса
asset.label.updatedМетка статуса ресурса устанавливается, изменяется или удаляется
asset.versionedРесурс версионируется
Версионирование ресурса

Когда сработает событие asset.versioned, вы получите полезную нагрузку с идентификатором ресурса, который был версионирован, а не сам стек версий. Поэтому, чтобы передать этот идентификатор в другую функцию, полагая, что это идентификатор стека версий, вам сначала потребуется найти и отследить этот конкретный «родительский» ресурс.

Обновления меток ресурса

Событие asset.label.updated не сработает, когда метка статуса изменится через вызов PUT к конечной точке /v2/assets/:id через общедоступный API-интерфейс (BES-408). Однако оно сработает, когда метка статуса обновится с помощью любых встроенных приложений и интеграций Frame.io (веб-версия, iOS, Premiere, After Effects, FCPX и т. д.).

Комментарии

СобытиеТриггер
comment.createdСоздан новый комментарий или ответ
comment.updatedКомментарий отредактирован
comment.deletedКомментарий удален
comment.completedКомментарий завершен
comment.uncompletedКомментарий не завершен

Ссылки для проверки

СобытиеТриггер
reviewlink.createdСоздана новая ссылка для рецензирования

Соавторы

СобытиеТриггер
collaborator.createdСоавтор добавлен в учетную запись
collaborator.deletedСоавтор удален из учетной записи

Участники команды

СобытиеТриггер
teammember.createdУчастник команды добавлен в учетную запись
teammember.deletedУчастник команды удален из учетной записи

Полезная нагрузка

Frame.io доставляет полезную нагрузку JSON на указанную конечную точку веб-перехватчика. Вот пример полезной нагрузки для события asset.created:

1{
2 "type": "asset.created",
3 "resource": {
4 "type": "asset",
5 "id": "<asset-id>"
6 },
7 "user": {
8 "id": "<user-id>"
9 },
10 "team": {
11 "id": "<team-id>"
12 }
13}

Все полезные нагрузки содержат поле тип, указывающее тип происходящего события, а также объект resource. Объект resource определяет тип и ИД ресурса, связанного с этим событием. В приведенном выше примере события asset.created это будет ИД для только что созданного ресурса. Дополнительно включены объекты пользователь и команда. Они ссылаются на пользователя, который инициировал событие, и контекст команды для ресурса. Помимо непосредственного контекста пользователя и команды, мы не включаем никакой дополнительной информации о подписанном ресурсе. Если вашему приложению требуется дополнительная информация или контекст, рекомендуется использовать наш API-интерфейс HTTP для выполнения последующих запросов.

Повторные попытки

В случае ошибки (ответ с кодом состояния, отличным от 200) или таймаута при доставке веб-перехватчика вашему сервису отправка полезной нагрузки будет повторяться три раза, всего четыре попытки.

Безопасность

По умолчанию все веб-перехватчики предоставляются с ключом подписи. Это не настраивается. Данный ключ можно использовать для проверки того, исходит ли запрос от Frame.io.

Проверка подписей веб-перехватчика

Чтобы защитить интеграцию от атак типа «человек посередине» и атак повторного воспроизведения, важно проверять подписи веб-перехватчика. Проверка гарантирует, что полезная нагрузка веб-перехватчика действительно была отправлена платформой Frame.io, а ее содержимое не было изменено при передаче.

В запрос POST включается следующее:

ИмяОписание
X-Frameio-Request-TimestampВремя доставки веб-перехватчика
X-Frameio-SignatureВычисленная подпись
Метка времени — это время доставки из систем Frame.io. Можно использовать для предотвращения атак повторного воспроизведения. Мы рекомендуем проверять, чтобы это время отличалось от локального времени не более чем на 5 минут. Подпись — это хеш HMAC SHA256, использующий ключ подписи, предоставленный при первоначальном создании веб-перехватчика.

Выполните следующие шаги для проверки подписи.

  1. Извлечение подписи из заголовков HTTP
  2. Создание сообщения для подписи, объединив версию, время доставки и тело запроса в формате: v0:timestamp:body
  3. Вычислите подпись HMAC SHA256 с помощью вашего секретного ключа подписи. Примечание. К предоставленной подписи добавлен префикс v0=. В настоящее время в Frame.io существует только одна версия для подписи запросов. Убедитесь, что этот префикс добавлен к вычисленной вами подписи.
  4. Сравните!
Python
1import hmac
2import hashlib
3
4def verify_signature(curr_time, req_time, signature, body, secret):
5 """
6 Verify Webhook signature
7 :Args:
8 curr_time (float): Current epoch time
9 req_time (float): Request epoch time
10 signature (str): Signature provided by the Frame.io API for the given request
11 body (str): Webhook body from the received POST
12 secret (str): The secret for this Webhook that you saved when you first created it
13 """
14 if int(curr_time) - int(req_time) < 500:
15 message = 'v0:{}:{}'.format(req_time, body)
16 calculated_signature = 'v0={}'.format(hmac.new(
17 bytes(secret, 'latin-1'),
18 msg=bytes(message, 'latin-1'),
19 digestmod=hashlib.sha256).hexdigest())
20 if calculated_signature == signature:
21 return True
22 return False
1const crypto = require('crypto');
2
3// Capture the signature, secret, timestamp and payload from a new webhook event:
4const
5signature = 'v0=a77ce6856e609c884575c2fd211d07a9ad1c3f72e19c06ff710e8f086ffca883',
6secret = 'yxSE59T0gtZOFZxw6UhLwTkhd2m8ntNSdSWnApQ0xOnMEzSoXbD8sGFP4bzb7MbS',
7timestamp = 1604004499, // UNIX timestamp in seconds
8payload = {
9 "project": {
10 "id": "f348e9f4-f142-42f9-b3bf-478d93f0feb4"
11 },
12 "resource": {
13 "id": "6aad9151-c216-4d6f-b5e9-530df551a426",
14 "type": "asset"
15 },
16 "team": {
17 "id": "aa891687-4b1e-4150-9b6d-9e4911c5b436"
18 },
19 "type": "asset.label.updated",
20 "user": {
21 "id": "59c9ade1-311b-4c3b-8231-b9d88e9a1a85"
22 }
23},
24body = JSON.stringify(payload),
25
26// Validate that caught payload is not older than 5 minutes
27currentTimeUTC = (new Date()).getTime(),
28currentTimestamp = currentTimeUTC / 1000, // JavaScript uses milliseconds whereas Unix Time is in seconds.
29minutes = 5,
30expired = (currentTimestamp - timestamp) > minutes*60
31hmac1 = crypto.createHmac('sha256', secret),
32generateSignature = hmac1.update(`v0:${timestamp}:${body}`).digest('hex')
33
34// Evaluates to true if the webhook is verified
35console.log(!expired && signature === `v0=${generateSignature}`)
1// Full Go sample code: https://github.com/Frameio/webhooks-example-app/blob/master/main.go
2
3func handler(w http.ResponseWriter, r *http.Request) {
4 out, err := httputil.DumpRequest(r, true)
5 if err != nil {
6 w.WriteHeader(http.StatusInternalServerError)
7 return
8 }
9
10 log.Println(string(out))
11
12 // Verify the message has been delivered in the last 5 minutes.
13 timestampStr := r.Header.Get("X-Frameio-Request-Timestamp")
14 timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
15 if err != nil {
16 w.WriteHeader(http.StatusBadRequest)
17 return
18 }
19
20 if time.Since(time.Unix(timestamp, 0)) > 5*time.Minute {
21 w.WriteHeader(http.StatusBadRequest)
22 return
23 }
24
25 // Verify request signature.
26 expected := r.Header.Get("X-Frameio-Signature")
27 signature, _ := computeSignature(r, timestamp, secretKey)
28 if expected != signature {
29 w.WriteHeader(http.StatusUnauthorized)
30 return
31 }
32
33 var event *Event
34 decoder := json.NewDecoder(r.Body)
35 err = decoder.Decode(&event)
36 if err != nil {
37 w.WriteHeader(http.StatusInternalServerError)
38 return
39 }
40
41 // Handle webhook here.
42 log.Println(event.ID)
43
44 w.WriteHeader(http.StatusOK)
45}
46
47// The request includes headers to enable the recipient to validate
48// that the request is from Frame.io and that it's been delivered within
49// the expected time range. To learn more about how this works, take a
50// look at our docs https://docs.frame.io/docs/webhooks#section-security.
51func computeSignature(r *http.Request, timestamp int64, secret string) (string, error) {
52 body, err := ioutil.ReadAll(r.Body)
53 if err != nil {
54 return "", err
55 }
56 copy := body[:]
57 r.Body = ioutil.NopCloser(bytes.NewReader(copy))
58
59 msg := fmt.Sprintf("%s:%d:%s", version, timestamp, string(body))
60
61 key := []byte(secret)
62 h := hmac.New(sha256.New, key)
63 h.Write([]byte(msg))
64
65 result := fmt.Sprintf("%s=%s", version, hex.EncodeToString(h.Sum(nil)))
66
67 return result, nil
68}