라이브 방 - 아고라 설정 및 라이브 방 관련 API

This commit is contained in:
2023-08-01 04:56:47 +09:00
parent f393c7630e
commit 58a7f87ffd
37 changed files with 1823 additions and 6 deletions

View File

@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.agora
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.TreeMap
class AccessToken(
var appId: String,
private val appCertificate: String,
val channelName: String,
private val uid: String,
var crcChannelName: Int = 0,
private var crcUid: Int = 0,
val message: PrivilegeMessage = PrivilegeMessage()
) {
private lateinit var signature: ByteArray
private lateinit var messageRawContent: ByteArray
enum class Privileges(value: Int) {
JoinChannel(1),
PublishAudioStream(2),
PublishVideoStream(3),
PublishDataStream(4), // For RTM only
RtmLogin(1000);
var intValue: Short
init {
intValue = value.toShort()
}
}
@Throws(Exception::class)
fun build(): String {
if (!AgoraUtils.isUUID(appId)) {
return ""
}
if (!AgoraUtils.isUUID(appCertificate)) {
return ""
}
messageRawContent = AgoraUtils.pack(message)
signature = generateSignature(
appCertificate,
appId,
channelName,
uid,
messageRawContent
)
crcChannelName = AgoraUtils.crc32(channelName)
crcUid = AgoraUtils.crc32(uid)
val packContent = PackContent(signature, crcChannelName, crcUid, messageRawContent)
val content: ByteArray = AgoraUtils.pack(packContent)
return getVersion() + appId + AgoraUtils.base64Encode(content)
}
fun addPrivilege(privilege: Privileges, expireTimestamp: Int) {
message.messages[privilege.intValue] = expireTimestamp
}
private fun getVersion(): String {
return VER
}
@Throws(java.lang.Exception::class)
fun generateSignature(
appCertificate: String,
appID: String,
channelName: String,
uid: String,
message: ByteArray
): ByteArray {
val baos = ByteArrayOutputStream()
try {
baos.write(appID.toByteArray())
baos.write(channelName.toByteArray())
baos.write(uid.toByteArray())
baos.write(message)
} catch (e: IOException) {
e.printStackTrace()
}
return AgoraUtils.hmacSign(appCertificate, baos.toByteArray())
}
fun fromString(token: String): Boolean {
if (getVersion() != token.substring(0, AgoraUtils.VERSION_LENGTH)) {
return false
}
try {
appId = token.substring(AgoraUtils.VERSION_LENGTH, AgoraUtils.VERSION_LENGTH + AgoraUtils.APP_ID_LENGTH)
val packContent = PackContent()
AgoraUtils.unpack(
AgoraUtils.base64Decode(
token.substring(
AgoraUtils.VERSION_LENGTH + AgoraUtils.APP_ID_LENGTH,
token.length
)
),
packContent
)
signature = packContent.signature
crcChannelName = packContent.crcChannelName
crcUid = packContent.crcUid
messageRawContent = packContent.rawMessage
AgoraUtils.unpack(messageRawContent, message)
} catch (e: java.lang.Exception) {
e.printStackTrace()
return false
}
return true
}
class PrivilegeMessage : PackableEx {
var salt: Int
var ts: Int
var messages: TreeMap<Short, Int>
override fun marshal(out: ByteBuf): ByteBuf {
return out.put(salt).put(ts).putIntMap(messages)
}
override fun unmarshal(input: ByteBuf) {
salt = input.readInt()
ts = input.readInt()
messages = input.readIntMap()
}
init {
salt = AgoraUtils.randomInt()
ts = AgoraUtils.getTimestamp() + 24 * 3600
messages = TreeMap()
}
}
class PackContent() : PackableEx {
var signature: ByteArray = byteArrayOf()
var crcChannelName = 0
var crcUid = 0
var rawMessage: ByteArray = byteArrayOf()
constructor(signature: ByteArray, crcChannelName: Int, crcUid: Int, rawMessage: ByteArray) : this() {
this.signature = signature
this.crcChannelName = crcChannelName
this.crcUid = crcUid
this.rawMessage = rawMessage
}
override fun marshal(out: ByteBuf): ByteBuf {
return out
.put(signature)
.put(crcChannelName)
.put(crcUid).put(rawMessage)
}
override fun unmarshal(input: ByteBuf) {
signature = input.readBytes()
crcChannelName = input.readInt()
crcUid = input.readInt()
rawMessage = input.readBytes()
}
}
companion object {
const val VER = "006"
}
}

View File

@@ -0,0 +1,72 @@
package kr.co.vividnext.sodalive.agora
import org.apache.commons.codec.binary.Base64
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.Date
import java.util.zip.CRC32
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object AgoraUtils {
const val HMAC_SHA256_LENGTH: Long = 32
const val VERSION_LENGTH = 3
const val APP_ID_LENGTH = 32
@Throws(InvalidKeyException::class, NoSuchAlgorithmException::class)
fun hmacSign(keyString: String, msg: ByteArray?): ByteArray {
val keySpec = SecretKeySpec(keyString.toByteArray(), "HmacSHA256")
val mac = Mac.getInstance("HmacSHA256")
mac.init(keySpec)
return mac.doFinal(msg)
}
fun pack(packableEx: PackableEx): ByteArray {
val buffer = ByteBuf()
packableEx.marshal(buffer)
return buffer.asBytes()
}
fun unpack(data: ByteArray?, packableEx: PackableEx) {
val buffer = ByteBuf(data!!)
packableEx.unmarshal(buffer)
}
fun base64Encode(data: ByteArray?): String {
val encodedBytes: ByteArray = Base64.encodeBase64(data)
return String(encodedBytes)
}
fun base64Decode(data: String): ByteArray {
return Base64.decodeBase64(data.toByteArray())
}
fun crc32(data: String): Int {
// get bytes from string
val bytes = data.toByteArray()
return crc32(bytes)
}
fun crc32(bytes: ByteArray?): Int {
val checksum = CRC32()
checksum.update(bytes)
return checksum.value.toInt()
}
fun getTimestamp(): Int {
return (Date().time / 1000).toInt()
}
fun randomInt(): Int {
return SecureRandom().nextInt()
}
fun isUUID(uuid: String): Boolean {
return if (uuid.length != 32) {
false
} else {
uuid.matches("\\p{XDigit}+".toRegex())
}
}
}

View File

@@ -0,0 +1,111 @@
package kr.co.vividnext.sodalive.agora
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.TreeMap
class ByteBuf() {
private var buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN)
constructor(bytes: ByteArray) : this() {
this.buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
}
fun asBytes(): ByteArray {
val out = ByteArray(buffer.position())
buffer.rewind()
buffer[out, 0, out.size]
return out
}
// packUint16
fun put(v: Short): ByteBuf {
buffer.putShort(v)
return this
}
fun put(v: ByteArray): ByteBuf {
put(v.size.toShort())
buffer.put(v)
return this
}
// packUint32
fun put(v: Int): ByteBuf {
buffer.putInt(v)
return this
}
fun put(v: Long): ByteBuf {
buffer.putLong(v)
return this
}
fun put(v: String): ByteBuf {
return put(v.toByteArray())
}
fun put(extra: TreeMap<Short, String>): ByteBuf {
put(extra.size.toShort())
for ((key, value) in extra.entries) {
put(key)
put(value)
}
return this
}
fun putIntMap(extra: TreeMap<Short, Int>): ByteBuf {
put(extra.size.toShort())
for ((key, value) in extra.entries) {
put(key)
put(value)
}
return this
}
fun readShort(): Short {
return buffer.short
}
fun readInt(): Int {
return buffer.int
}
fun readBytes(): ByteArray {
val length = readShort()
val bytes = ByteArray(length.toInt())
buffer[bytes]
return bytes
}
fun readString(): String {
val bytes = readBytes()
return String(bytes)
}
fun readMap(): TreeMap<Short, String> {
val map = TreeMap<Short, String>()
val length = readShort()
for (i in 0 until length) {
val k = readShort()
val v = readString()
map[k] = v
}
return map
}
fun readIntMap(): TreeMap<Short, Int> {
val map = TreeMap<Short, Int>()
val length = readShort()
for (i in 0 until length) {
val k = readShort()
val v = readInt()
map[k] = v
}
return map
}
}

View File

@@ -0,0 +1,256 @@
package kr.co.vividnext.sodalive.agora
import org.apache.commons.codec.binary.Base64
import org.apache.commons.codec.binary.Hex
import java.util.TreeMap
class DynamicKey5 {
lateinit var content: DynamicKey5Content
fun fromString(key: String): Boolean {
if (key.substring(0, 3) != version) {
return false
}
val rawContent: ByteArray = Base64().decode(key.substring(3))
if (rawContent.isEmpty()) {
return false
}
content = DynamicKey5Content()
val buffer = ByteBuf(rawContent)
content.unmarshall(buffer)
return true
}
companion object {
const val version = "005"
const val noUpload = "0"
const val audioVideoUpload = "3"
// ServiceType
const val MEDIA_CHANNEL_SERVICE: Short = 1
const val RECORDING_SERVICE: Short = 2
const val PUBLIC_SHARING_SERVICE: Short = 3
const val IN_CHANNEL_PERMISSION: Short = 4
// InChannelPermissionKey
const val ALLOW_UPLOAD_IN_CHANNEL: Short = 1
@Throws(Exception::class)
fun generateSignature(
appCertificate: String,
service: Short,
appID: String,
unixTs: Int,
salt: Int,
channelName: String,
uid: Long,
expiredTs: Int,
extra: TreeMap<Short, String>
): String {
// decode hex to avoid case problem
val hex = Hex()
val rawAppID: ByteArray = hex.decode(appID.toByteArray())
val rawAppCertificate: ByteArray = hex.decode(appCertificate.toByteArray())
val m = Message(
service,
rawAppID,
unixTs,
salt,
channelName,
(uid and 0xFFFFFFFFL).toInt(),
expiredTs,
extra
)
val toSign: ByteArray = pack(m)
return String(Hex.encodeHex(DynamicKeyUtil.encodeHMAC(rawAppCertificate, toSign), false))
}
@Throws(java.lang.Exception::class)
fun generateDynamicKey(
appID: String,
appCertificate: String,
channel: String,
ts: Int,
salt: Int,
uid: Long,
expiredTs: Int,
extra: TreeMap<Short, String>,
service: Short
): String {
val signature = generateSignature(appCertificate, service, appID, ts, salt, channel, uid, expiredTs, extra)
val content =
DynamicKey5Content(service, signature, Hex().decode(appID.toByteArray()), ts, salt, expiredTs, extra)
val bytes: ByteArray = pack(content)
val encoded = Base64().encode(bytes)
val base64 = String(encoded)
return version + base64
}
private fun pack(content: Packable): ByteArray {
val buffer = ByteBuf()
content.marshal(buffer)
return buffer.asBytes()
}
@Throws(Exception::class)
fun generatePublicSharingKey(
appID: String,
appCertificate: String,
channel: String,
ts: Int,
salt: Int,
uid: Long,
expiredTs: Int
) = generateDynamicKey(
appID,
appCertificate,
channel,
ts,
salt,
uid,
expiredTs,
TreeMap(),
PUBLIC_SHARING_SERVICE
)
@Throws(Exception::class)
fun generateRecordingKey(
appID: String,
appCertificate: String,
channel: String,
ts: Int,
salt: Int,
uid: Long,
expiredTs: Int
) = generateDynamicKey(
appID,
appCertificate,
channel,
ts,
salt,
uid,
expiredTs,
TreeMap(),
RECORDING_SERVICE
)
@Throws(Exception::class)
fun generateMediaChannelKey(
appID: String,
appCertificate: String,
channel: String,
ts: Int,
salt: Int,
uid: Long,
expiredTs: Int
) = generateDynamicKey(
appID,
appCertificate,
channel,
ts,
salt,
uid,
expiredTs,
TreeMap(),
MEDIA_CHANNEL_SERVICE
)
@Throws(Exception::class)
fun generateInChannelPermissionKey(
appID: String,
appCertificate: String,
channel: String,
ts: Int,
salt: Int,
uid: Long,
expiredTs: Int,
permission: String
): String {
val extra = TreeMap<Short, String>()
extra[ALLOW_UPLOAD_IN_CHANNEL] = permission
return generateDynamicKey(
appID,
appCertificate,
channel,
ts,
salt,
uid,
expiredTs,
extra,
IN_CHANNEL_PERMISSION
)
}
internal class Message(
var serviceType: Short,
var appID: ByteArray,
var unixTs: Int,
var salt: Int,
var channelName: String,
var uid: Int,
var expiredTs: Int,
var extra: TreeMap<Short, String>
) : Packable {
override fun marshal(out: ByteBuf): ByteBuf {
return out
.put(serviceType)
.put(appID)
.put(unixTs)
.put(salt)
.put(channelName)
.put(uid)
.put(expiredTs)
.put(extra)
}
}
class DynamicKey5Content() : Packable {
var serviceType: Short = 0
var signature: String? = null
var appID: ByteArray = byteArrayOf()
var unixTs = 0
var salt = 0
var expiredTs = 0
var extra: TreeMap<Short, String>? = null
constructor(
serviceType: Short,
signature: String?,
appID: ByteArray,
unixTs: Int,
salt: Int,
expiredTs: Int,
extra: TreeMap<Short, String>
) : this() {
this.serviceType = serviceType
this.signature = signature
this.appID = appID
this.unixTs = unixTs
this.salt = salt
this.expiredTs = expiredTs
this.extra = extra
}
override fun marshal(out: ByteBuf): ByteBuf {
return out
.put(serviceType)
.put(signature!!)
.put(appID)
.put(unixTs)
.put(salt)
.put(expiredTs)
.put(extra!!)
}
fun unmarshall(input: ByteBuf) {
serviceType = input.readShort()
signature = input.readString()
appID = input.readBytes()
unixTs = input.readInt()
salt = input.readInt()
expiredTs = input.readInt()
extra = input.readMap()
}
}
}
}

View File

@@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.agora
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/**
* Created by hefeng on 15/8/10.
* Util to generate Agora media dynamic key.
*/
object DynamicKeyUtil {
@Throws(NoSuchAlgorithmException::class, InvalidKeyException::class)
fun encodeHMAC(key: String, message: ByteArray?): ByteArray? {
return encodeHMAC(key.toByteArray(), message)
}
@Throws(NoSuchAlgorithmException::class, InvalidKeyException::class)
fun encodeHMAC(key: ByteArray?, message: ByteArray?): ByteArray? {
val keySpec = SecretKeySpec(key, "HmacSHA1")
val mac = Mac.getInstance("HmacSHA1")
mac.init(keySpec)
return mac.doFinal(message)
}
fun bytesToHex(`in`: ByteArray): String {
val builder = StringBuilder()
for (b in `in`) {
builder.append(String.format("%02x", b))
}
return builder.toString()
}
}

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.agora
interface Packable {
fun marshal(out: ByteBuf): ByteBuf
}

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.agora
interface PackableEx : Packable {
fun unmarshal(input: ByteBuf)
}

View File

@@ -0,0 +1,96 @@
package kr.co.vividnext.sodalive.agora
import org.springframework.stereotype.Component
@Component
class RtcTokenBuilder {
/**
* Builds an RTC token using an int uid.
*
* @param appId The App ID issued to you by Agora.
* @param appCertificate Certificate of the application that you registered in
* the Agora Dashboard.
* @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are:
*
* * The 26 lowercase English letters: a to z.
* * The 26 uppercase English letters: A to Z.
* * The 10 digits: 0 to 9.
* * The space.
* * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",".
*
* @param uid User ID. A 32-bit unsigned integer with a value ranging from
* 1 to (2^32-1).
* @param role The user role.
*
* * Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast.
* * Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher.
*
* @param privilegeTs Represented by the number of seconds elapsed since 1/1/1970.
* If, for example, you want to access the Agora Service within 10 minutes
* after the token is generated, set expireTimestamp as the current time stamp
* + 600 (seconds).
*/
fun buildTokenWithUid(
appId: String,
appCertificate: String,
channelName: String,
uid: Int,
privilegeTs: Int
): String {
val account = if (uid == 0) "" else uid.toString()
return buildTokenWithUserAccount(
appId,
appCertificate,
channelName,
account,
privilegeTs
)
}
/**
* Builds an RTC token using a string userAccount.
*
* @param appId The App ID issued to you by Agora.
* @param appCertificate Certificate of the application that you registered in
* the Agora Dashboard.
* @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are:
*
* * The 26 lowercase English letters: a to z.
* * The 26 uppercase English letters: A to Z.
* * The 10 digits: 0 to 9.
* * The space.
* * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",".
*
* @param account The user account.
* @param role The user role.
*
* * Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast.
* * Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher.
*
* @param privilegeTs represented by the number of seconds elapsed since 1/1/1970.
* If, for example, you want to access the Agora Service within 10 minutes
* after the token is generated, set expireTimestamp as the current time stamp
* + 600 (seconds).
*/
fun buildTokenWithUserAccount(
appId: String,
appCertificate: String,
channelName: String,
account: String,
privilegeTs: Int
): String {
// Assign appropriate access privileges to each role.
val builder = AccessToken(appId, appCertificate, channelName, account)
builder.addPrivilege(AccessToken.Privileges.JoinChannel, privilegeTs)
builder.addPrivilege(AccessToken.Privileges.PublishAudioStream, privilegeTs)
builder.addPrivilege(AccessToken.Privileges.PublishVideoStream, privilegeTs)
builder.addPrivilege(AccessToken.Privileges.PublishDataStream, privilegeTs)
return try {
builder.build()
} catch (e: Exception) {
e.printStackTrace()
""
}
}
}

View File

@@ -0,0 +1,31 @@
package kr.co.vividnext.sodalive.agora
import kr.co.vividnext.sodalive.agora.AccessToken.Privileges
import org.springframework.stereotype.Component
@Component
class RtmTokenBuilder {
lateinit var mTokenCreator: AccessToken
@Throws(Exception::class)
fun buildToken(
appId: String,
appCertificate: String,
uid: String,
privilegeTs: Int
): String {
mTokenCreator = AccessToken(appId, appCertificate, uid, "")
mTokenCreator.addPrivilege(Privileges.RtmLogin, privilegeTs)
return mTokenCreator.build()
}
fun setPrivilege(privilege: Privileges?, expireTs: Int) {
mTokenCreator.addPrivilege(privilege!!, expireTs)
}
fun initTokenBuilder(originToken: String?): Boolean {
mTokenCreator.fromString(originToken!!)
return true
}
}