웹훅 개요

애플리케이션 예시

Frame.io 웹훅을 위한 자체 컨슈머를 직접 구축하고 싶다면, Github에 있는 예제 앱을 다운로드하여 자유롭게 확장해 보세요.

소개

웹훅은 Frame.io 내부에서 발생하는 이벤트를 외부 시스템으로 전송하여 처리, API 콜백, 및 궁극적으로는 워크플로 자동화를 위해 알림으로 활용할 수 있는 방법을 제공합니다.

설정

웹훅은 저희 개발자 사이트의 Webhooks 영역에서 구성할 수 있습니다. 웹훅에는 다음 항목이 필요합니다.

  • Name — 개발자 사이트에서만 표시됩니다.
  • URL — 이벤트를 전달할 위치.
  • Team — 이 웹훅이 추가될 팀.
  • Events — 웹훅을 트리거할 하나 이상의 이벤트.

지원되는 이벤트

단일 웹훅은 다음 이벤트 중 원하는 개수만큼 구독할 수 있습니다.

프로젝트

이벤트트리거
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 이벤트가 발생하면 버전 스택 자체가 아니라 버전이 지정된 에셋의 id가 포함된 페이로드를 받게 됩니다. 따라서 해당 id를 버전 스택의 id라고 생각하고 다른 함수에 전달하려고 한다면, 먼저 해당 특정 ‘상위’ 리소스를 찾아 추적해야 합니다.

에셋 레이블 업데이트

공개 API를 사용하여 /v2/assets/:id 엔드포인트에 대한 PUT 호출을 통해 상태 레이블이 변경된 경우 asset.label.updated 이벤트가 발생하지 않습니다(BES-408). 하지만, 기본 Frame.io 앱 및 연동 프로그램(Web, 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}

모든 페이로드에는 발생하는 이벤트의 유형을 나타내는 type 필드와 resource 오브젝트가 포함되어 있습니다. resource 오브젝트는 이 이벤트와 관련된 리소스의 typeid를 지정합니다. 위의 asset.created 이벤트 예시에서 이는 새로 생성된 에셋의 id가 됩니다. 또한 userteam 오브젝트도 포함됩니다. 여기에는 이벤트를 트리거한 사용자와 리소스의 팀 컨텍스트가 참조됩니다. 직접적인 User 및 Team 컨텍스트 외에, 구독된 리소스에 대한 추가 정보는 포함하지 않습니다. 애플리케이션에 추가 정보나 컨텍스트가 필요한 경우, HTTP API를 사용하여 후속 요청을 수행하는 것을 권장합니다.

재시도

웹훅을 귀하의 서비스로 전달하는 동안 오류(200이 아닌 상태 코드 응답) 또는 시간 초과가 발생할 경우 페이로드 전송은 세 번 재시도되며, 총 4번의 전송 시도가 이루어집니다.

보안

기본적으로 모든 웹훅에는 서명 키가 제공됩니다. 이는 구성할 수 없습니다. 이 키를 사용하여 요청이 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}