@live-change/peer-connection-service 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/definition.js ADDED
@@ -0,0 +1,11 @@
1
+ const App = require("@live-change/framework")
2
+ const app = App.app()
3
+
4
+ const relationsPlugin = require('@live-change/relations-plugin')
5
+
6
+ const definition = app.createServiceDefinition({
7
+ name: "peerConnection",
8
+ use: [ relationsPlugin ]
9
+ })
10
+
11
+ module.exports = definition
@@ -0,0 +1,35 @@
1
+ const crypto = require("crypto")
2
+
3
+ const config = {
4
+ TURN_URLS: 'turn:turn1.xaos.ninja:4433',//;turn:turn2.xaos.ninja:4433',
5
+ TURN_SECRET: 'c1e3705c2'
6
+ }
7
+
8
+ const urls = config.TURN_URLS
9
+ const secret = config.TURN_SECRET
10
+
11
+ function randomHexString(size) {
12
+ return new Promise((resolve, reject) => {
13
+ crypto.randomBytes(size / 2, function(err, res) {
14
+ if (err) return reject(err)
15
+ resolve(res.toString('hex'))
16
+ })
17
+ })
18
+ }
19
+ async function genTurnAuth() {
20
+ const expire = (Date.now() / 1000 + 1 * 60 * 60) | 0 // 1 hour
21
+ const username = await randomHexString(10)
22
+ const rusername = expire + ':' + username
23
+ const password = crypto
24
+ .createHmac('sha1', secret)
25
+ .update(rusername)
26
+ .digest('base64')
27
+ /// TODO: select nearest servers by geoip
28
+ console.dir({
29
+ urls,
30
+ username: rusername,
31
+ credential: password
32
+ })
33
+ }
34
+
35
+ genTurnAuth()
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ const App = require("@live-change/framework")
2
+ const app = App.app()
3
+
4
+ const definition = require('./definition.js')
5
+
6
+ require('./turn.js')
7
+ require('./peer.js')
8
+
9
+ module.exports = definition
package/message.js ADDED
@@ -0,0 +1,158 @@
1
+ const Peer = require('./peer.js')
2
+
3
+ const messageFields = {
4
+ to: {
5
+ type: Peer
6
+ },
7
+ from: {
8
+ type: Peer
9
+ },
10
+ type: {
11
+ type: String
12
+ },
13
+ data: {
14
+ type: Object
15
+ }
16
+ }
17
+
18
+ const Message = definition.model({
19
+ name: "Message",
20
+ properties: {
21
+ timestamp: {
22
+ type: Date,
23
+ validation: ['nonEmpty']
24
+ },
25
+ ...messageFields
26
+ },
27
+ indexes: {
28
+ /*byToTimestamp: {
29
+ property: ['to', 'timestamp']
30
+ },*/
31
+ },
32
+ crud: {
33
+ deleteTrigger: true,
34
+ writeOptions: {
35
+ access: (params, {client, service}) => {
36
+ return client.roles.includes('admin')
37
+ }
38
+ }
39
+ }
40
+ })
41
+
42
+ definition.view({
43
+ name: "messages",
44
+ properties: {
45
+ peer: {
46
+ type: Peer
47
+ },
48
+ gt: {
49
+ type: String,
50
+ },
51
+ lt: {
52
+ type: String,
53
+ },
54
+ gte: {
55
+ type: String,
56
+ },
57
+ lte: {
58
+ type: String,
59
+ },
60
+ limit: {
61
+ type: Number
62
+ },
63
+ reverse: {
64
+ type: Boolean
65
+ }
66
+ },
67
+ returns: {
68
+ type: Array,
69
+ of: {
70
+ type: Message
71
+ }
72
+ },
73
+ access: async({ peer }, { client, service, visibilityTest }) => {
74
+ if(visibilityTest) return true
75
+ if(!peer) throw new Error("peer parameter is required")
76
+ const publicSessionInfo = await getPublicInfo(client.sessionId)
77
+ //console.log('MESSAGES ACCESS', peer.split('_'), "[2] == ", publicSessionInfo.id)
78
+ return peer.split('_')[2] == publicSessionInfo.id
79
+ },
80
+ async daoPath({ peer, gt, lt, gte, lte, limit, reverse }, { client, service }, method) {
81
+ const channelId = peer
82
+ if(!Number.isSafeInteger(limit)) limit = 100
83
+ const range = {
84
+ gt: gt ? `${channelId}_${gt.split('_').pop()}` : (gte ? undefined : `${channelId}_`),
85
+ lt: lt ? `${channelId}_${lt.split('_').pop()}` : undefined,
86
+ gte: gte ? `${channelId}_${gte.split('_').pop()}` : undefined,
87
+ lte: lte ? `${channelId}_${lte.split('_').pop()}` : ( lt ? undefined : `${channelId}_\xFF\xFF\xFF\xFF`),
88
+ limit,
89
+ reverse
90
+ }
91
+ const messages = await Message.rangeGet(range)
92
+ console.log("MESSAGES RANGE", JSON.stringify({ peer, gt, lt, gte, lte, limit, reverse }) ,
93
+ "\n TO", JSON.stringify(range),
94
+ "\n RESULTS", messages.length, messages.map(m => m.id))
95
+
96
+ /* console.log("MESSAGES RANGE", range, "RESULTS", messages.length)*/
97
+ return Message.rangePath(range)
98
+ }
99
+ })
100
+
101
+ let lastMessageTime = new Map()
102
+
103
+ async function postMessage(props, { client, service }, emit, conversation) {
104
+ console.log("POST MESSAGE", props)
105
+ const channelId = props.to
106
+ let lastTime = lastMessageTime.get(channelId)
107
+ const now = new Date()
108
+ if(lastTime && now.toISOString() <= lastTime.toISOString()) {
109
+ lastTime.setTime(lastTime.getTime() + 1)
110
+ } else {
111
+ lastTime = now
112
+ }
113
+ if(lastTime.getTime() > now.getTime() + 100) { /// Too many messages per second, drop message
114
+ return;
115
+ }
116
+ lastMessageTime.set(channelId, lastTime)
117
+ const message = `${channelId}_${lastTime.toISOString()}`
118
+ let data = {}
119
+ for(const key in messageFields) {
120
+ data[key] = props[key]
121
+ }
122
+ data.timestamp = now
123
+ if(!data.user) {
124
+ const publicInfo = await getPublicInfo(client.sessionId)
125
+ data.session = publicInfo.id
126
+ }
127
+ emit({
128
+ type: "MessageCreated",
129
+ message,
130
+ data
131
+ })
132
+ }
133
+
134
+ definition.action({
135
+ name: "postMessage",
136
+ properties: {
137
+ ...messageFields
138
+ },
139
+ //queuedBy: (command) => `${command.toType}_${command.toId})`,
140
+ access: async ({ from, to }, context) => {
141
+ const { client, service, visibilityTest } = context
142
+ if(visibilityTest) return true
143
+ const [fromType, fromId, fromSession] = from.split('_')
144
+ const [toType, toId, toSession] = to.split('_')
145
+ if(toType != fromType) return false
146
+ if(toId != fromId) return false
147
+ const publicSessionInfo = await getPublicInfo(client.sessionId)
148
+ if(publicSessionInfo.id != fromSession) return false
149
+ return toType.split('.')[0] == 'priv'
150
+ ? checkPrivAccess(toId, context)
151
+ : checkIfRole(toType.split('.')[0], toId, ['speaker', 'vip', 'moderator', 'owner'], context)
152
+ },
153
+ async execute(props, { client, service }, emit) {
154
+ const result = await postMessage(props, { client, service }, emit)
155
+ console.log("MESSAGE POSTED!")
156
+ return result
157
+ }
158
+ })
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@live-change/peer-connection-service",
3
+ "version": "0.2.28",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node index.js",
8
+ "setup": "INSTALL=true node index.js",
9
+ "test": "NODE_ENV=test blue-tape tests/*"
10
+ },
11
+ "author": "Michał Łaszczewski <michal@emikse.com>",
12
+ "license": "BSD-3-Clause",
13
+ "gitHead": "34309fef58572e1a8794b52438430dd421c57d65"
14
+ }
package/peer.js ADDED
@@ -0,0 +1,115 @@
1
+ const definition = require('./definition.js')
2
+
3
+ const { clientHasAccessRole } = require("../access-control-service/access.js")(definition)
4
+
5
+ const Peer = definition.model({
6
+ name: "Peer",
7
+ itemOfAny: {
8
+ to: ['channel', 'session']
9
+ },
10
+ properties: {
11
+ }
12
+ })
13
+
14
+ definition.view({
15
+ name: "peers",
16
+ properties: {
17
+ channelType: {
18
+ type: String,
19
+ validation: ['nonEmpty']
20
+ },
21
+ channel: {
22
+ type: String,
23
+ validation: ['nonEmpty']
24
+ }
25
+ },
26
+ returns: {
27
+ type: Array,
28
+ of: {
29
+ type: Peer
30
+ }
31
+ },
32
+ access: (params, { client, visibilityTest }) => {
33
+ if(visibilityTest) return true
34
+ const { channelType, channel } = params
35
+ //console.log("CHECK PEERS ACCESS", params, client, visibilityTest)
36
+ return clientHasAccessRole(client, { objectType: channelType, object: channel },
37
+ ['reader', 'speaker', 'vip', 'moderator', 'owner'])
38
+ },
39
+ async daoPath({ channelType, channel }, { client, service }, method) {
40
+ return Peer.indexRangePath('byChannel', [ channelType, channel.split('.')[0] ])
41
+ }
42
+ })
43
+
44
+ definition.event({
45
+ name: "peerOnline",
46
+ async execute({ channelType, channel, sessionType, session, instance }) {
47
+ const peer = channelType + ':' + channel + ':' + sessionType + ':' + session + ':' + instance
48
+ await Peer.create({ id: peer, channelType, channel, instance, sessionType, session })
49
+ }
50
+ })
51
+
52
+ definition.event({
53
+ name: "peerOffline",
54
+ async execute({ channelType, channel, sessionType, session, instance }) {
55
+ const peer = channelType + ':' + channel + ':' + sessionType + ':' + session + ':' + instance
56
+ Peer.delete(peer)
57
+ }
58
+ })
59
+
60
+ definition.event({
61
+ name: "allOffline",
62
+ async execute() {
63
+ await app.dao.request(['database', 'query', app.databaseName, `(${
64
+ async (input, output, { table }) => {
65
+ await input.table(table).range({}).onChange(async obj => {
66
+ output.table(table).delete(obj.id)
67
+ })
68
+ }
69
+ })`, { table: Peer.tableName }])
70
+ }
71
+ })
72
+
73
+ definition.trigger({
74
+ name: "sessionPeerOnline",
75
+ properties: {
76
+ },
77
+ async execute({ session, peer }, context, emit) {
78
+ console.log("PEER ONLINE PARAMS", { session, peer })
79
+ const [ channelType, channel, sessionType, peerSession, instance ] = peer.split(':')
80
+ if(sessionType != 'session_Session') throw new Error('wrongSessionType')
81
+ if(peerSession != session) throw new Error('wrongSession')
82
+ /// TODO: check channel access
83
+ emit({
84
+ type: 'peerOnline',
85
+ channelType, channel, sessionType, session, instance
86
+ })
87
+ }
88
+ })
89
+
90
+ definition.trigger({
91
+ name: "sessionPeerOffline",
92
+ properties: {
93
+ },
94
+ async execute({ session, peer }, context, emit) {
95
+ console.log("PEER OFFLINE PARAMS", { session, peer })
96
+ const [ channelType, channel, sessionType, peerSession, instance ] = peer.split(':')
97
+ if(sessionType != 'session_Session') throw new Error('wrongSessionType')
98
+ if(peerSession != session) throw new Error('wrongSession')
99
+ emit({
100
+ type: 'peerOffline',
101
+ channelType, channel, sessionType, session, instance
102
+ })
103
+ }
104
+ })
105
+
106
+ definition.trigger({
107
+ name: "allOffline",
108
+ properties: {
109
+ },
110
+ async execute({ }, context, emit) {
111
+ emit({
112
+ type: "allOffline"
113
+ })
114
+ }
115
+ })
package/peerState.js ADDED
@@ -0,0 +1,85 @@
1
+ const definition = require('./definition.js')
2
+
3
+ const { Peer } = require('./peer.js')
4
+
5
+ const peerStateFields = {
6
+ audioState: {
7
+ type: String
8
+ },
9
+ videoState: {
10
+ type: String
11
+ }
12
+ }
13
+
14
+ const PeerState = definition.model({
15
+ name: "PeerState",
16
+ propertyOf: {
17
+ what: Peer
18
+ },
19
+ properties: {
20
+ ...peerStateFields
21
+ }
22
+ })
23
+
24
+ definition.event({
25
+ name: "peerStateSet",
26
+ async execute({ peer, data }) {
27
+ await PeerState.create({ ...data, id: peer })
28
+ }
29
+ })
30
+
31
+ definition.view({
32
+ name: "peerState",
33
+ properties: {
34
+ peer: {
35
+ type: Peer
36
+ }
37
+ },
38
+ returns: {
39
+ type: PeerState
40
+ },
41
+ access: async ({ peer }, context) => {
42
+ const { client, service, visibilityTest } = context
43
+ if(visibilityTest) return true
44
+ const [toType, toId, toSession] = peer.split('_')
45
+ return toType.split('.')[0] == 'priv'
46
+ ? checkPrivAccess(toId, context)
47
+ : checkIfRole(toType.split('.')[0], toId, ['speaker', 'vip', 'moderator', 'owner'], context)
48
+ },
49
+ async daoPath({ peer }, { client, service }, method) {
50
+ return PeerState.path(peer)
51
+ }
52
+ })
53
+
54
+ definition.action({
55
+ name: "setPeerState",
56
+ properties: {
57
+ peer: {
58
+ type: Peer
59
+ },
60
+ ...peerStateFields
61
+ },
62
+ //queuedBy: (command) => `${command.toType}_${command.toId})`,
63
+ access: async ({ peer }, context) => {
64
+ const { client, service, visibilityTest } = context
65
+ if(visibilityTest) return true
66
+ const [toType, toId, toSession] = peer.split('_')
67
+ const publicSessionInfo = await getPublicInfo(client.sessionId)
68
+ if(publicSessionInfo.id != toSession) return false
69
+ return toType.split('.')[0] == 'priv'
70
+ ? checkPrivAccess(toId, context)
71
+ : checkIfRole(toType.split('.')[0], toId, ['speaker', 'vip', 'moderator', 'owner'], context)
72
+ },
73
+ async execute(props, { client, service }, emit) {
74
+ let data = { }
75
+ for(const key in peerStateFields) {
76
+ data[key] = props[key]
77
+ }
78
+ emit({
79
+ type: 'peerStateSet',
80
+ peer: props.peer,
81
+ data
82
+ })
83
+ return 'ok'
84
+ }
85
+ })
package/turn.js ADDED
@@ -0,0 +1,89 @@
1
+ const crypto = require('crypto')
2
+ const ReactiveDao = require('@live-change/dao')
3
+ const definition = require('./definition.js')
4
+ const config = definition.config
5
+
6
+ const urls = config?.turn?.urls || process.env.TURN_URLS?.split(';')
7
+ const secret = config?.turn?.secret || process.env.TURN_SECRET
8
+ const turnExpireTime = config?.turn?.expire || (+process.env.TURN_EXPIRE) || (60 * 60) // 1 hour for default
9
+
10
+ const { clientHasAccessRole } = require("../access-control-service/access.js")(definition)
11
+
12
+ const { Peer } = require('./peer.js')
13
+
14
+ function randomHexString(size) {
15
+ return new Promise((resolve, reject) => {
16
+ crypto.randomBytes(size / 2, function(err, res) {
17
+ if (err) return reject(err)
18
+ resolve(res.toString('hex'))
19
+ })
20
+ })
21
+ }
22
+
23
+ async function createTurnConfiguration({ client }) {
24
+ const expire = Date.now() / 1000 + turnExpireTime | 0
25
+ const username = await randomHexString(10)
26
+ const rusername = expire + ':' + username
27
+ const password = crypto
28
+ .createHmac('sha1', secret)
29
+ .update(rusername)
30
+ .digest('base64')
31
+ /// TODO: select nearest servers by geoip
32
+ return {
33
+ urls,
34
+ credentialType: 'password',
35
+ username: rusername,
36
+ credential: password,
37
+ clientIp: client.ip
38
+ }
39
+ }
40
+
41
+ async function releaseTurnConfiguration() {
42
+ /// not used in static shared secret configuration
43
+ }
44
+
45
+ definition.view({
46
+ name: "turnConfiguration",
47
+ properties: {
48
+ peer: {
49
+ type: Peer
50
+ }
51
+ },
52
+ access: async ({ peer }, { client, service, visibilityTest }) => {
53
+ if(visibilityTest) return true
54
+ const [ channelType, channel, sessionType, session, instance ] = peer.split(':')
55
+ if(sessionType != 'session_Session') throw new Error('wrongSessionType')
56
+ if(session != client.session) throw new Error('wrongSession')
57
+ return clientHasAccessRole(client, { objectType: channelType.split('.')[0], object: channel },
58
+ ['speaker', 'vip', 'moderator', 'owner'])
59
+ },
60
+ observable({ peer }, context) {
61
+ const observable = new ReactiveDao.ObservableValue()
62
+ let turnWorking = true
63
+ const refreshTurn = async () => {
64
+ if(observable.isDisposed()) {
65
+ turnWorking = false
66
+ return
67
+ }
68
+ try {
69
+ observable.set(await createTurnConfiguration(context))
70
+ } catch(error) {
71
+ observable.error(error)
72
+ }
73
+ const refreshDelay = turnExpireTime * 1000 / 2
74
+ setTimeout(refreshTurn, refreshDelay)
75
+ }
76
+ refreshTurn() // must be async!
77
+ const oldRespawn = observable.respawn
78
+ observable.respawn = () => {
79
+ oldRespawn.call(observable)
80
+ if(!turnWorking) refreshTurn() // must be async!
81
+ }
82
+ return observable
83
+ },
84
+ async get({ peer }, context) {
85
+ return await createTurnConfiguration(context)
86
+ }
87
+ })
88
+
89
+ module.exports = { createTurnConfiguration, releaseTurnConfiguration, turnExpireTime }