-
Notifications
You must be signed in to change notification settings - Fork 0
/
opentok.go
456 lines (380 loc) · 11.1 KB
/
opentok.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
package opentok
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"time"
)
// New Creates a new OpenTok object. This is the factory
// function that should normally be used.
func New(apiKey int, apiSecret string) *OpenTok {
return &OpenTok{
APIKey: apiKey,
APISecret: apiSecret,
apiURL: "https://api.opentok.com",
partnerAuth: fmt.Sprintf("%d:%s", apiKey, apiSecret),
client: &http.Client{},
}
}
// NewWithAppEngine creates a new OpenTok object.
// This is the factory function that should normally be used
// when deploying the service on AppEngine
func NewWithAppEngine(apiKey int, apiSecret string) *OpenTok {
c := &http.Client{
Transport: &http.Transport{},
}
return newOpenTokWithClient(apiKey, apiSecret, c)
}
func newOpenTokWithClient(apiKey int, apiSecret string, c httpClient) *OpenTok {
ot := New(apiKey, apiSecret)
ot.client = c
return ot
}
func newOpenTokWithURL(apiKey int, apiSecret string, apiURL string) *OpenTok {
ot := New(apiKey, apiSecret)
ot.apiURL = apiURL
return ot
}
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// OpenTok contains all the necessary information to
// create sessions and interact with the OpenTok platform
type OpenTok struct {
// ApiKey that you get after creating
// a project at the OpenTok Dashboard
APIKey int
// ApiSecret that you get after
// creating a project with the OpenTok Dashboard
APISecret string
apiURL string
partnerAuth string
client httpClient
}
// Session generates a new OpenTok Session. The Session.ID is
// necessary for the clients to be able to connect to an
// OpenTok Session
func (ot *OpenTok) Session(props *SessionProps) (s *Session, err error) {
var (
req *http.Request
res *http.Response
sessions xmlSessions
)
if props == nil {
props = &SessionProps{}
}
// Sets default values to the properties if they haven't
// been set or they are incorrect
defaultsSessionProps(props)
// prepare payload to be sent
propsMap := map[string]string{
"location": props.Location,
"p2p.preference": string(props.MediaMode),
"archiveMode": string(props.ArchiveMode),
}
payload := formURLEncode(propsMap)
// create request
if req, err = http.NewRequest("POST", ot.apiURL+"/session/create", payload); err != nil {
return nil, err
}
req.Header.Add("Content-type", "application/x-www-form-urlencoded")
ot.commonHeaders(&req.Header)
// perform request
if res, err = ot.client.Do(req); err != nil {
return nil, err
}
// check if request is correct
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, errFromStatusCode(res)
}
// read body response
if err = xml.NewDecoder(res.Body).Decode(&sessions); err != nil {
return nil, err
}
// get result
return &Session{
ID: sessions.Sessions[0].SessionID,
}, nil
}
// Token generates a token that each client needs to use
// to be able to connect to an OpenTok Session
func (ot *OpenTok) Token(sessionID string, props *TokenProps) (*Token, error) {
if len(sessionID) == 0 {
return nil, fmt.Errorf("Session has not been created. Please use OpenTok.Session")
}
if props == nil {
props = &TokenProps{}
}
key := calcKey(sessionID, props)
signature := ot.signKey(key)
buffer := bytes.NewBufferString("")
buffer.WriteString(fmt.Sprintf("partner_id=%d", ot.APIKey))
buffer.WriteString(fmt.Sprintf("&sig=%s:%s", signature, key))
encoded := base64.StdEncoding.EncodeToString(buffer.Bytes())
token := Token("T1==" + encoded)
return &token, nil
}
// ArchiveStart starts a new archive for the session. The archive id
// is generated by the OpenTok platform and the archive status becomes
// started
func (ot *OpenTok) ArchiveStart(sessionID string, props *ArchiveProps) (*Archive, error) {
if len(sessionID) == 0 && props != nil && len(props.SessionID) == 0 {
return nil, fmt.Errorf("Session has empty id")
}
if props == nil {
props = &ArchiveProps{
HasAudio: true,
HasVideo: true,
}
}
if len(props.SessionID) == 0 {
props.SessionID = sessionID
}
var (
req *http.Request
res *http.Response
payload io.Reader
err error
)
url := fmt.Sprintf("%s/v2/partner/%d/archive", ot.apiURL, ot.APIKey)
defaultArchiveProps(props)
if payload, err = jsonEncode(props); err != nil {
return nil, err
}
if req, err = http.NewRequest("POST", url, payload); err != nil {
return nil, err
}
req.Header.Add("Content-type", "application/json")
ot.commonHeaders(&req.Header)
if res, err = ot.client.Do(req); err != nil {
return nil, err
}
// check that request status code is not an error
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, errFromStatusCode(res)
}
var archive Archive
// read body response
if err = json.NewDecoder(res.Body).Decode(&archive); err != nil {
return nil, err
}
return &archive, nil
}
// ArchiveStop stops an archive that is being recorded. If the
// archive is not in status started an error will be returned.
// The status of the archive becomes stopped
func (ot *OpenTok) ArchiveStop(archiveID string) error {
if len(archiveID) == 0 {
return fmt.Errorf("archiveID should not be empty")
}
var (
req *http.Request
res *http.Response
err error
)
url := fmt.Sprintf("%s/v2/partner/%d/archive/%s/stop",
ot.apiURL, ot.APIKey, archiveID)
if req, err = http.NewRequest("POST", url, nil); err != nil {
return err
}
ot.commonHeaders(&req.Header)
if res, err = ot.client.Do(req); err != nil {
return err
}
// check that request status code is not an error
if res.StatusCode < 200 || res.StatusCode > 299 {
return errFromStatusCode(res)
}
return nil
}
// ArchiveGet retrieves an archive from the server. If the
// archive does not exist an error will be raised
func (ot *OpenTok) ArchiveGet(archiveID string) (*Archive, error) {
if len(archiveID) == 0 {
return nil, fmt.Errorf("ArchiveId is empty")
}
var (
req *http.Request
res *http.Response
payload io.Reader
err error
)
url := fmt.Sprintf("%s/v2/partner/%d/archive/%s",
ot.apiURL, ot.APIKey, archiveID)
if req, err = http.NewRequest("GET", url, payload); err != nil {
return nil, err
}
ot.commonHeaders(&req.Header)
if res, err = ot.client.Do(req); err != nil {
return nil, err
}
// check that request status code is not an error
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, errFromStatusCode(res)
}
// read body response
var archive Archive
if err = json.NewDecoder(res.Body).Decode(&archive); err != nil {
return nil, err
}
return &archive, nil
}
// ArchiveDelete deletes an existing archive with status available. If
// the archive is in any other state the operation will
// fail and return an error
func (ot *OpenTok) ArchiveDelete(archiveID string) error {
if len(archiveID) == 0 {
return fmt.Errorf("ArchiveId is empty")
}
var (
req *http.Request
res *http.Response
payload io.Reader
err error
)
url := fmt.Sprintf("%s/v2/partner/%d/archive/%s",
ot.apiURL, ot.APIKey, archiveID)
if req, err = http.NewRequest("DELETE", url, payload); err != nil {
return err
}
ot.commonHeaders(&req.Header)
if res, err = ot.client.Do(req); err != nil {
return err
}
// check that request status code is not an error
if res.StatusCode < 200 || res.StatusCode > 299 {
return errFromStatusCode(res)
}
return nil
}
// ArchiveList returns a list of archives. If Count == 0, the limit of
// the number of archives returned by the server is limited
// by the server. Otherwise it will be count. Offset is
// useful for pagination
func (ot *OpenTok) ArchiveList(count, offset int) (*ArchiveList, error) {
if count < 0 {
return nil, fmt.Errorf("count must be bigger than 0: %d", count)
}
if offset < 0 {
return nil, fmt.Errorf("offset must be bigger than or equal to0: %d",
offset)
}
var (
req *http.Request
res *http.Response
payload io.Reader
err error
)
url := fmt.Sprintf("%s/v2/partner/%d/archive?offset=%d",
ot.apiURL, ot.APIKey, offset)
if count > 0 {
url = fmt.Sprintf("%s&count=%d", url, count)
}
if req, err = http.NewRequest("GET", url, payload); err != nil {
return nil, err
}
ot.commonHeaders(&req.Header)
if res, err = ot.client.Do(req); err != nil {
return nil, err
}
// check that request status code is not an error
if res.StatusCode < 200 || res.StatusCode > 299 {
return nil, errFromStatusCode(res)
}
// read body response
var archiveList ArchiveList
if err = json.NewDecoder(res.Body).Decode(&archiveList); err != nil {
return nil, err
}
return &archiveList, nil
}
func (ot *OpenTok) signKey(key []byte) string {
hash := hmac.New(sha1.New, []byte(ot.APISecret))
hash.Write(key)
return hex.EncodeToString(hash.Sum(nil))
}
func calcKey(sessionID string, props *TokenProps) []byte {
var (
createTime = time.Now().Unix()
nonce = rand.Int31() % 1000000
role = props.Role
expires = props.ExpireTime
)
// Set role to Publisher if it hasn't been set by the client
// or if it has been set to an invalid value
if len(props.Role) == 0 ||
(props.Role != Moderator && props.Role != Publisher && props.Role != Subscriber) {
role = Publisher
}
// If it hasn't been set or it has been set incorrectly, it
// defaults to a day
if props.ExpireTime < createTime {
expires = createTime + 60*60*24
}
key := bytes.NewBuffer([]byte(""))
key.WriteString(fmt.Sprintf("session_id=%s", sessionID))
key.WriteString(fmt.Sprintf("&create_time=%d", createTime))
key.WriteString(fmt.Sprintf("&nonce=%d", nonce))
key.WriteString(fmt.Sprintf("&role=%s", string(role)))
key.WriteString(fmt.Sprintf("&expire_time=%d", expires))
if len(props.Data) > 0 && len(props.Data) < 1000 {
key.WriteString(fmt.Sprintf("&connection_data=%s", props.Data))
}
return key.Bytes()
}
func jsonEncode(data interface{}) (io.Reader, error) {
buf := bytes.NewBufferString("")
if err := json.NewEncoder(buf).Encode(data); err != nil {
return nil, err
}
return buf, nil
}
func formURLEncode(data map[string]string) io.Reader {
var params = url.Values{}
for key, value := range data {
params.Add(key, value)
}
encoded := params.Encode()
bufferString := bytes.NewBufferString(encoded)
return bytes.NewReader(bufferString.Bytes())
}
func defaultsSessionProps(props *SessionProps) {
if len(props.MediaMode) == 0 ||
(props.MediaMode != Routed && props.MediaMode != Relayed) {
props.MediaMode = Routed
}
if len(props.ArchiveMode) == 0 ||
(props.ArchiveMode != Always && props.ArchiveMode != Manual) {
props.ArchiveMode = Manual
}
}
func defaultArchiveProps(props *ArchiveProps) {
if len(props.OutputMode) == 0 ||
(props.OutputMode != Individual && props.OutputMode != Composed) {
props.OutputMode = Composed
}
}
func errFromStatusCode(res *http.Response) error {
if res.ContentLength == 0 {
return fmt.Errorf("Error: statusCode: %d", res.StatusCode)
}
var message string
body := make([]byte, res.ContentLength)
res.Body.Read(body)
message = string(body)
return fmt.Errorf("Error: statusCode: %d, message: %s",
res.StatusCode, message)
}
func (ot *OpenTok) commonHeaders(h *http.Header) {
h.Add("X-TB-PARTNER-AUTH", ot.partnerAuth)
h.Add("X-TB-VERSION", "1")
}