diff --git a/DynamicKey/AgoraDynamicKey/README.md b/DynamicKey/AgoraDynamicKey/README.md index 639af824..6872f896 100755 --- a/DynamicKey/AgoraDynamicKey/README.md +++ b/DynamicKey/AgoraDynamicKey/README.md @@ -28,6 +28,7 @@ Sample Code for generating AccessToken are available on the following platforms: + C++ + Go + Java + + Kotlin + Node.js + Python + Python3 @@ -65,6 +66,11 @@ Sample Code for generating AccessToken are available on the following platforms: + https://github.com/AgoraIO/Tools/blob/master/DynamicKey/AgoraDynamicKey/java/src/main/java/io/agora/sample/RtcTokenBuilderSample.java + https://github.com/AgoraIO/Tools/blob/master/DynamicKey/AgoraDynamicKey/java/src/main/java/io/agora/sample/RtmTokenBuilderSample.java +### Kotlin + +- Version 007 + + https://github.com/AgoraIO/Tools/blob/master/DynamicKey/AgoraDynamicKey/kotlin/src/sample/kotlin/io/agora/sample/RtcTokenBuilder2Sample.kt + ### Node.js - Version 007 @@ -359,6 +365,71 @@ public class RtcTokenBuilder2Sample { } ``` +### Kotlin +```kotlin +package io.agora.sample + +import io.agora.media.RtcTokenBuilder2 + +object RtcTokenBuilder2Sample { + // Need to set environment variable AGORA_APP_ID + private val appId = System.getenv("AGORA_APP_ID") + + // Need to set environment variable AGORA_APP_CERTIFICATE + private val appCertificate = System.getenv("AGORA_APP_CERTIFICATE") + + private const val channelName = "7d72365eb983485397e3e3f9d460bdda" + private const val account = "2082341273" + private const val uid = 2082341273 + private const val tokenExpirationInSeconds = 3600 + private const val privilegeExpirationInSeconds = 3600 + private const val joinChannelPrivilegeExpireInSeconds = 3600 + private const val pubAudioPrivilegeExpireInSeconds = 3600 + private const val pubVideoPrivilegeExpireInSeconds = 3600 + private const val pubDataStreamPrivilegeExpireInSeconds = 3600 + + @JvmStatic + fun main(args: Array) { + println("App Id: $appId") + println("App Certificate: $appCertificate") + if (appId == null || appId.isEmpty() || appCertificate == null || appCertificate.isEmpty()) { + println("Need to set environment variable AGORA_APP_ID and AGORA_APP_CERTIFICATE") + return + } + + val tokenBuilder = RtcTokenBuilder2() + var result = tokenBuilder.buildTokenWithUid( + appId, appCertificate, channelName, uid, RtcTokenBuilder2.Role.ROLE_PUBLISHER, + tokenExpirationInSeconds, privilegeExpirationInSeconds + ) + println("Token with uid: $result") + + result = tokenBuilder.buildTokenWithUserAccount( + appId, appCertificate, channelName, account, + RtcTokenBuilder2.Role.ROLE_PUBLISHER, + tokenExpirationInSeconds, privilegeExpirationInSeconds + ) + println("Token with account: $result") + + result = tokenBuilder.buildTokenWithUid( + appId, appCertificate, channelName, uid, tokenExpirationInSeconds, + joinChannelPrivilegeExpireInSeconds, pubAudioPrivilegeExpireInSeconds, + pubVideoPrivilegeExpireInSeconds, + pubDataStreamPrivilegeExpireInSeconds + ) + println("Token with uid and privilege: $result") + + result = tokenBuilder.buildTokenWithUserAccount( + appId, appCertificate, channelName, account, + tokenExpirationInSeconds, + joinChannelPrivilegeExpireInSeconds, pubAudioPrivilegeExpireInSeconds, + pubVideoPrivilegeExpireInSeconds, pubDataStreamPrivilegeExpireInSeconds + ) + println("Token with account and privilege: $result") + } +} +``` + ### Node.js ```javascript const RtcTokenBuilder = require("../src/RtcTokenBuilder2").RtcTokenBuilder; diff --git a/DynamicKey/AgoraDynamicKey/kotlin/README.md b/DynamicKey/AgoraDynamicKey/kotlin/README.md new file mode 100644 index 00000000..d889b817 --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/README.md @@ -0,0 +1,60 @@ +# Agora Token Generator Kotlin + +This project provides the implementation of Agora AccessToken2 in Kotlin. + +## Project Structure + +- `src/main/kotlin`: Core logic for generating tokens. +- `src/sample/kotlin`: Sample usage of the token builders. +- `src/test/kotlin`: Unit tests for the implementation. + +## How to use + +### Build + +You can build the project using Gradle: + +```bash +./gradlew build +``` + +### Run Sample + +To run the sample, you need to set the following environment variables: +- `AGORA_APP_ID`: Your Agora App ID. +- `AGORA_APP_CERTIFICATE`: Your Agora App Certificate. + +Then run: + +```bash +./gradlew runSample +``` + +(Note: You might need to add a `runSample` task to your `build.gradle.kts` if you want to run it via Gradle) + +### Run Tests + +```bash +./gradlew test +``` + +## Example Usage + +```kotlin +import io.agora.media.RtcTokenBuilder2 + +val appId = "YOUR_APP_ID" +val appCertificate = "YOUR_APP_CERTIFICATE" +val channelName = "YOUR_CHANNEL_NAME" +val uid = 123 +val role = RtcTokenBuilder2.Role.ROLE_PUBLISHER +val tokenExpire = 3600 +val privilegeExpire = 3600 + +val tokenBuilder = RtcTokenBuilder2() +val token = tokenBuilder.buildTokenWithUid( + appId, appCertificate, channelName, uid, role, + tokenExpire, privilegeExpire +) +println("Token: $token") +``` diff --git a/DynamicKey/AgoraDynamicKey/kotlin/build.gradle.kts b/DynamicKey/AgoraDynamicKey/kotlin/build.gradle.kts new file mode 100644 index 00000000..23cdf9eb --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + kotlin("jvm") version "1.9.0" +} + +group = "io.agora" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(kotlin("test")) + testImplementation("junit:junit:4.13.2") +} + +kotlin { + jvmToolchain(8) +} + +sourceSets { + main { + kotlin.setSrcDirs(listOf("src/main/kotlin")) + } + test { + kotlin.setSrcDirs(listOf("src/test/kotlin")) + } + create("sample") { + kotlin.setSrcDirs(listOf("src/sample/kotlin")) + compileClasspath += sourceSets.main.get().output + sourceSets.test.get().compileClasspath + runtimeClasspath += sourceSets.main.get().output + sourceSets.test.get().runtimeClasspath + } +} + +val sampleJar = task("sampleJar") { + archiveClassifier.set("sample") + from(sourceSets["sample"].allSource) +} + +tasks.register("runSample") { + mainClass.set("io.agora.sample.RtcTokenBuilder2Sample") + classpath = sourceSets["sample"].runtimeClasspath +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/gradle/wrapper/gradle-wrapper.properties b/DynamicKey/AgoraDynamicKey/kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 00000000..a4b44297 --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/DynamicKey/AgoraDynamicKey/kotlin/gradlew b/DynamicKey/AgoraDynamicKey/kotlin/gradlew new file mode 100755 index 00000000..2fe81a7d --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/DynamicKey/AgoraDynamicKey/kotlin/gradlew.bat b/DynamicKey/AgoraDynamicKey/kotlin/gradlew.bat new file mode 100644 index 00000000..9109989e --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/DynamicKey/AgoraDynamicKey/kotlin/settings.gradle.kts b/DynamicKey/AgoraDynamicKey/kotlin/settings.gradle.kts new file mode 100644 index 00000000..923a46fc --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "agora-token-generator-kotlin" diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/AccessToken2.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/AccessToken2.kt new file mode 100644 index 00000000..933a6b32 --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/AccessToken2.kt @@ -0,0 +1,289 @@ +package io.agora.media + +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +class AccessToken2 { + enum class PrivilegeRtc(val intValue: Short) { + PRIVILEGE_JOIN_CHANNEL(1), + PRIVILEGE_PUBLISH_AUDIO_STREAM(2), + PRIVILEGE_PUBLISH_VIDEO_STREAM(3), + PRIVILEGE_PUBLISH_DATA_STREAM(4); + } + + enum class PrivilegeRtm(val intValue: Short) { + PRIVILEGE_LOGIN(1); + } + + enum class PrivilegeFpa(val intValue: Short) { + PRIVILEGE_LOGIN(1); + } + + enum class PrivilegeChat(val intValue: Short) { + PRIVILEGE_CHAT_USER(1), + PRIVILEGE_CHAT_APP(2); + } + + enum class PrivilegeApaas(val intValue: Short) { + PRIVILEGE_ROOM_USER(1), + PRIVILEGE_USER(2), + PRIVILEGE_APP(3); + } + + var appCert: String = "" + var appId: String = "" + var expire: Int = 0 + var issueTs: Int = 0 + var salt: Int = 0 + var services: MutableMap = TreeMap() + + constructor() + + constructor(appId: String, appCert: String, expire: Int) { + this.appCert = appCert + this.appId = appId + this.expire = expire + this.issueTs = Utils.getTimestamp() + this.salt = Utils.randomInt() + } + + fun addService(service: Service) { + services[service.serviceType] = service + } + + @Throws(Exception::class) + fun build(): String { + if (!Utils.isUUID(appId) || !Utils.isUUID(appCert)) { + return "" + } + + val buf = ByteBuf() + .put(appId) + .put(issueTs) + .put(expire) + .put(salt) + .put(services.size.toShort()) + + val signing = getSign() + + services.forEach { (_, v) -> + v.pack(buf) + } + + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(signing, "HmacSHA256")) + val signature = mac.doFinal(buf.asBytes()) + + val bufferContent = ByteBuf() + bufferContent.put(signature) + bufferContent.buffer.put(buf.asBytes()) + + return getVersion() + Utils.base64Encode(Utils.compress(bufferContent.asBytes())) + } + + fun getService(serviceType: Short): Service { + return when (serviceType) { + SERVICE_TYPE_RTC -> ServiceRtc() + SERVICE_TYPE_RTM -> ServiceRtm() + SERVICE_TYPE_FPA -> ServiceFpa() + SERVICE_TYPE_CHAT -> ServiceChat() + SERVICE_TYPE_APAAS -> ServiceApaas() + else -> throw IllegalArgumentException("unknown service type: `$serviceType`") + } + } + + @Throws(Exception::class) + fun getSign(): ByteArray { + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(ByteBuf().put(issueTs).asBytes(), "HmacSHA256")) + val signing = mac.doFinal(appCert.toByteArray()) + mac.init(SecretKeySpec(ByteBuf().put(salt).asBytes(), "HmacSHA256")) + return mac.doFinal(signing) + } + + fun parse(token: String): Boolean { + if (getVersion() != token.substring(0, Utils.VERSION_LENGTH)) { + return false + } + + return try { + val data = Utils.decompress(Utils.base64Decode(token.substring(Utils.VERSION_LENGTH))) + val buff = ByteBuf(data) + val signature = buff.readString() + appId = buff.readString() + issueTs = buff.readInt() + expire = buff.readInt() + salt = buff.readInt() + val servicesNum = buff.readShort() + + for (i in 0 until servicesNum) { + val serviceType = buff.readShort() + val service = getService(serviceType) + service.unpack(buff) + services[serviceType] = service + } + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + open class Service(val type: Short) { + var privileges: TreeMap = TreeMap() + + fun addPrivilegeRtc(privilege: PrivilegeRtc, expire: Int) { + privileges[privilege.intValue] = expire + } + + fun addPrivilegeRtm(privilege: PrivilegeRtm, expire: Int) { + privileges[privilege.intValue] = expire + } + + fun addPrivilegeFpa(privilege: PrivilegeFpa, expire: Int) { + privileges[privilege.intValue] = expire + } + + fun addPrivilegeChat(privilege: PrivilegeChat, expire: Int) { + privileges[privilege.intValue] = expire + } + + fun addPrivilegeApaas(privilege: PrivilegeApaas, expire: Int) { + privileges[privilege.intValue] = expire + } + + val serviceType: Short + get() = type + + open fun pack(buf: ByteBuf): ByteBuf { + return buf.put(type).putIntMap(privileges) + } + + open fun unpack(byteBuf: ByteBuf) { + privileges = byteBuf.readIntMap() + } + } + + class ServiceRtc : Service { + var channelName: String = "" + var uid: String = "" + + constructor() : super(SERVICE_TYPE_RTC) + + constructor(channelName: String, uid: String) : super(SERVICE_TYPE_RTC) { + this.channelName = channelName + this.uid = uid + } + + override fun pack(buf: ByteBuf): ByteBuf { + return super.pack(buf).put(channelName).put(uid) + } + + override fun unpack(byteBuf: ByteBuf) { + super.unpack(byteBuf) + channelName = byteBuf.readString() + uid = byteBuf.readString() + } + } + + class ServiceRtm : Service { + var userId: String = "" + + constructor() : super(SERVICE_TYPE_RTM) + + constructor(userId: String) : super(SERVICE_TYPE_RTM) { + this.userId = userId + } + + override fun pack(buf: ByteBuf): ByteBuf { + return super.pack(buf).put(userId) + } + + override fun unpack(byteBuf: ByteBuf) { + super.unpack(byteBuf) + userId = byteBuf.readString() + } + } + + class ServiceFpa : Service { + constructor() : super(SERVICE_TYPE_FPA) + + override fun pack(buf: ByteBuf): ByteBuf { + return super.pack(buf) + } + + override fun unpack(byteBuf: ByteBuf) { + super.unpack(byteBuf) + } + } + + class ServiceChat : Service { + var userId: String = "" + + constructor() : super(SERVICE_TYPE_CHAT) + + constructor(userId: String) : super(SERVICE_TYPE_CHAT) { + this.userId = userId + } + + override fun pack(buf: ByteBuf): ByteBuf { + return super.pack(buf).put(userId) + } + + override fun unpack(byteBuf: ByteBuf) { + super.unpack(byteBuf) + userId = byteBuf.readString() + } + } + + class ServiceApaas : Service { + var roomUuid: String = "" + var userUuid: String = "" + var role: Short = -1 + + constructor() : super(SERVICE_TYPE_APAAS) + + constructor(roomUuid: String, userUuid: String, role: Short) : super(SERVICE_TYPE_APAAS) { + this.roomUuid = roomUuid + this.userUuid = userUuid + this.role = role + } + + constructor(userUuid: String) : super(SERVICE_TYPE_APAAS) { + this.userUuid = userUuid + } + + override fun pack(buf: ByteBuf): ByteBuf { + return super.pack(buf).put(roomUuid).put(userUuid).put(role) + } + + override fun unpack(byteBuf: ByteBuf) { + super.unpack(byteBuf) + roomUuid = byteBuf.readString() + userUuid = byteBuf.readString() + role = byteBuf.readShort() + } + } + + companion object { + private const val VERSION = "007" + const val SERVICE_TYPE_RTC: Short = 1 + const val SERVICE_TYPE_RTM: Short = 2 + const val SERVICE_TYPE_FPA: Short = 4 + const val SERVICE_TYPE_CHAT: Short = 5 + const val SERVICE_TYPE_APAAS: Short = 7 + + fun getUidStr(uid: Int): String { + return if (uid == 0) { + "" + } else { + (uid.toLong() and 0xFFFFFFFFL).toString() + } + } + + fun getVersion(): String { + return VERSION + } + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/ByteBuf.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/ByteBuf.kt new file mode 100644 index 00000000..108b4f8e --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/ByteBuf.kt @@ -0,0 +1,121 @@ +package io.agora.media + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.charset.StandardCharsets +import java.util.* + +class ByteBuf { + var buffer: ByteBuffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN) + + constructor() + + constructor(bytes: ByteArray) { + buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) + } + + fun asBytes(): ByteArray { + val out = ByteArray(buffer.position()) + buffer.rewind() + buffer.get(out, 0, out.size) + return out + } + + fun put(v: Short): ByteBuf { + ensureCapacity(2) + buffer.putShort(v) + return this + } + + fun put(v: ByteArray): ByteBuf { + ensureCapacity(2 + v.size) + put(v.size.toShort()) + buffer.put(v) + return this + } + + fun put(v: Int): ByteBuf { + ensureCapacity(4) + buffer.putInt(v) + return this + } + + fun put(v: Long): ByteBuf { + ensureCapacity(8) + buffer.putLong(v) + return this + } + + fun put(v: String): ByteBuf { + return put(v.toByteArray(StandardCharsets.UTF_8)) + } + + fun put(extra: TreeMap): ByteBuf { + put(extra.size.toShort()) + for ((key, value) in extra) { + put(key) + put(value) + } + return this + } + + fun putIntMap(extra: TreeMap): ByteBuf { + put(extra.size.toShort()) + for ((key, value) in extra) { + put(key) + put(value) + } + return this + } + + fun readShort(): Short { + return buffer.short + } + + fun readInt(): Int { + return buffer.int + } + + fun readBytes(): ByteArray { + val length = readShort().toInt() + val bytes = ByteArray(length) + buffer.get(bytes) + return bytes + } + + fun readString(): String { + return String(readBytes(), StandardCharsets.UTF_8) + } + + fun readMap(): TreeMap { + val map = TreeMap() + val length = readShort().toInt() + for (i in 0 until length) { + val k = readShort() + val v = readString() + map[k] = v + } + return map + } + + fun readIntMap(): TreeMap { + val map = TreeMap() + val length = readShort().toInt() + for (i in 0 until length) { + val k = readShort() + val v = readInt() + map[k] = v + } + return map + } + + private fun ensureCapacity(capacity: Int) { + if (buffer.remaining() < capacity) { + val newCapacity = buffer.capacity() + Math.max(capacity, buffer.capacity()) + val newBuffer = ByteBuffer.allocate(newCapacity).order(ByteOrder.LITTLE_ENDIAN) + buffer.rewind() + newBuffer.put(buffer) + buffer = newBuffer + } + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/ChatTokenBuilder2.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/ChatTokenBuilder2.kt new file mode 100644 index 00000000..073a4b79 --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/ChatTokenBuilder2.kt @@ -0,0 +1,54 @@ +package io.agora.media + +class ChatTokenBuilder2 { + /** + * Build the CHAT user token. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param userId: The user's id, must be unique. + * @param expire: represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expire as 600(seconds). + * @return The Chat User token. + */ + fun buildUserToken(appId: String, appCertificate: String, userId: String, expire: Int): String { + val accessToken = AccessToken2(appId, appCertificate, expire) + val serviceChat = AccessToken2.ServiceChat(userId) + serviceChat.addPrivilegeChat(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_USER, expire) + accessToken.addService(serviceChat) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + /** + * Build the CHAT app token. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param expire: represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set expire as 600(seconds). + * @return The Chat App token. + */ + fun buildAppToken(appId: String, appCertificate: String, expire: Int): String { + val accessToken = AccessToken2(appId, appCertificate, expire) + val serviceChat = AccessToken2.ServiceChat() + serviceChat.addPrivilegeChat(AccessToken2.PrivilegeChat.PRIVILEGE_CHAT_APP, expire) + accessToken.addService(serviceChat) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/Packable.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/Packable.kt new file mode 100644 index 00000000..87da283a --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/Packable.kt @@ -0,0 +1,9 @@ +package io.agora.media + +interface Packable { + fun marshal(out: ByteBuf): ByteBuf +} + +interface PackableEx : Packable { + fun unmarshal(`in`: ByteBuf) +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/RtcTokenBuilder2.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/RtcTokenBuilder2.kt new file mode 100644 index 00000000..34dd060b --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/RtcTokenBuilder2.kt @@ -0,0 +1,354 @@ +package io.agora.media + +class RtcTokenBuilder2 { + enum class Role(val initValue: Int) { + /** + * RECOMMENDED. Use this role for a voice/video call or a live broadcast, if + * your scenario does not require authentication for + * [Co-host](https://docs.agora.io/en/video-calling/get-started/authentication-workflow?#co-host-token-authentication). + */ + ROLE_PUBLISHER(1), + + /** + * Only use this role if your scenario require authentication for + * [Co-host](https://docs.agora.io/en/video-calling/get-started/authentication-workflow?#co-host-token-authentication). + * + * @note 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. + */ + ROLE_SUBSCRIBER(2); + } + + /** + * Build the RTC token with uid. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param channelName: Unique channel name for the AgoraRTC session in the string format + * @param uid: User ID. A 32-bit unsigned integer with a value ranging from 1 to (2^32-1). + * uid must be unique. + * @param role: ROLE_PUBLISHER: A broadcaster/host in a live-broadcast profile. + * ROLE_SUBSCRIBER: An audience(default) in a live-broadcast profile. + * @param tokenExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set tokenExpire as 600(seconds). + * @param privilegeExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to enable your privilege for 10 minutes, set privilegeExpire as 600(seconds). + * @return The RTC token. + */ + fun buildTokenWithUid( + appId: String, + appCertificate: String, + channelName: String, + uid: Int, + role: Role, + tokenExpire: Int, + privilegeExpire: Int + ): String { + return buildTokenWithUserAccount( + appId, + appCertificate, + channelName, + AccessToken2.getUidStr(uid), + role, + tokenExpire, + privilegeExpire + ) + } + + /** + * Build the RTC token with account. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param channelName: Unique channel name for the AgoraRTC session in the string format + * @param account: The user's account, max length is 255 Bytes. + * @param role: ROLE_PUBLISHER: A broadcaster/host in a live-broadcast profile. + * ROLE_SUBSCRIBER: An audience(default) in a live-broadcast profile. + * @param tokenExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set tokenExpire as 600(seconds). + * @param privilegeExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to enable your privilege for 10 minutes, set privilegeExpire as 600(seconds). + * @return The RTC token. + */ + fun buildTokenWithUserAccount( + appId: String, + appCertificate: String, + channelName: String, + account: String, + role: Role, + tokenExpire: Int, + privilegeExpire: Int + ): String { + val accessToken = AccessToken2(appId, appCertificate, tokenExpire) + val serviceRtc = AccessToken2.ServiceRtc(channelName, account) + + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, privilegeExpire) + if (role == Role.ROLE_PUBLISHER) { + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM, privilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM, privilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM, privilegeExpire) + } + accessToken.addService(serviceRtc) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + /** + * Generates an RTC token with the specified privilege. + * + * This method supports generating a token with the following privileges: + * - Joining an RTC channel. + * - Publishing audio in an RTC channel. + * - Publishing video in an RTC channel. + * - Publishing data streams in an RTC channel. + * + * The privileges for publishing audio, video, and data streams in an RTC channel apply only if you have + * enabled co-host authentication. + * + * A user can have multiple privileges. Each privilege is valid for a maximum of 24 hours. + * The SDK triggers the onTokenPrivilegeWillExpire and onRequestToken callbacks when the token is about to expire + * or has expired. The callbacks do not report the specific privilege affected, and you need to maintain + * the respective timestamp for each privilege in your app logic. After receiving the callback, you need + * to generate a new token, and then call renewToken to pass the new token to the SDK, or call joinChannel to re-join + * the channel. + * + * @note Agora recommends setting a reasonable timestamp for each privilege according to your scenario. + * Suppose the expiration timestamp for joining the channel is set earlier than that for publishing audio. + * When the token for joining the channel expires, the user is immediately kicked off the RTC channel + * and cannot publish any audio stream, even though the timestamp for publishing audio has not expired. + * + * @param appId The App ID of your Agora project. + * @param appCertificate The App Certificate of your Agora project. + * @param channelName The unique channel name for the Agora RTC session in string format. The string length must be less than 64 bytes. + * @param uid The user ID. A 32-bit unsigned integer with a value range from 1 to (2^32 - 1). It must be unique. Set uid as 0, if you do not want to authenticate the user ID, that is, any uid from the app client can join the channel. + * @param tokenExpire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set tokenExpire as 600(seconds). + * @param joinChannelPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to join channel and expect stay in the channel for 10 minutes, set joinChannelPrivilegeExpire as 600(seconds). + * @param pubAudioPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish audio privilege for 10 minutes, set pubAudioPrivilegeExpire as 600(seconds). + * @param pubVideoPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish video privilege for 10 minutes, set pubVideoPrivilegeExpire as 600(seconds). + * @param pubDataStreamPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish data stream privilege for 10 minutes, set pubDataStreamPrivilegeExpire as 600(seconds). + * @return The RTC token. + */ + fun buildTokenWithUid( + appId: String, + appCertificate: String, + channelName: String, + uid: Int, + tokenExpire: Int, + joinChannelPrivilegeExpire: Int, + pubAudioPrivilegeExpire: Int, + pubVideoPrivilegeExpire: Int, + pubDataStreamPrivilegeExpire: Int + ): String { + return buildTokenWithUserAccount( + appId, + appCertificate, + channelName, + AccessToken2.getUidStr(uid), + tokenExpire, + joinChannelPrivilegeExpire, + pubAudioPrivilegeExpire, + pubVideoPrivilegeExpire, + pubDataStreamPrivilegeExpire + ) + } + + /** + * Generates an RTC token with the specified privilege. + * + * This method supports generating a token with the following privileges: + * - Joining an RTC channel. + * - Publishing audio in an RTC channel. + * - Publishing video in an RTC channel. + * - Publishing data streams in an RTC channel. + * + * The privileges for publishing audio, video, and data streams in an RTC channel apply only if you have + * enabled co-host authentication. + * + * A user can have multiple privileges. Each privilege is valid for a maximum of 24 hours. + * The SDK triggers the onTokenPrivilegeWillExpire and onRequestToken callbacks when the token is about to expire + * or has expired. The callbacks do not report the specific privilege affected, and you need to maintain + * the respective timestamp for each privilege in your app logic. After receiving the callback, you need + * to generate a new token, and then call renewToken to pass the new token to the SDK, or call joinChannel to re-join + * the channel. + * + * @note Agora recommends setting a reasonable timestamp for each privilege according to your scenario. + * Suppose the expiration timestamp for joining the channel is set earlier than that for publishing audio. + * When the token for joining the channel expires, the user is immediately kicked off the RTC channel + * and cannot publish any audio stream, even though the timestamp for publishing audio has not expired. + * + * @param appId The App ID of your Agora project. + * @param appCertificate The App Certificate of your Agora project. + * @param channelName The unique channel name for the Agora RTC session in string format. The string length must be less than 64 bytes. + * @param account The user account. + * @param tokenExpire represented by the number of seconds elapsed since now. If, for example, you want to access the + * Agora Service within 10 minutes after the token is generated, set tokenExpire as 600(seconds). + * @param joinChannelPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to join channel and expect stay in the channel for 10 minutes, set joinChannelPrivilegeExpire as 600(seconds). + * @param pubAudioPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish audio privilege for 10 minutes, set pubAudioPrivilegeExpire as 600(seconds). + * @param pubVideoPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish video privilege for 10 minutes, set pubVideoPrivilegeExpire as 600(seconds). + * @param pubDataStreamPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish data stream privilege for 10 minutes, set pubDataStreamPrivilegeExpire as 600(seconds). + * @return The RTC token. + */ + fun buildTokenWithUserAccount( + appId: String, + appCertificate: String, + channelName: String, + account: String, + tokenExpire: Int, + joinChannelPrivilegeExpire: Int, + pubAudioPrivilegeExpire: Int, + pubVideoPrivilegeExpire: Int, + pubDataStreamPrivilegeExpire: Int + ): String { + val accessToken = AccessToken2(appId, appCertificate, tokenExpire) + val serviceRtc = AccessToken2.ServiceRtc(channelName, account) + + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, joinChannelPrivilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM, pubAudioPrivilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM, pubVideoPrivilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM, pubDataStreamPrivilegeExpire) + accessToken.addService(serviceRtc) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + /** + * Build the RTC and RTM token with account. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param channelName: Unique channel name for the AgoraRTC session in the string format + * @param account: The user's account, max length is 255 Bytes. + * @param role: ROLE_PUBLISHER: A broadcaster/host in a live-broadcast profile. + * ROLE_SUBSCRIBER: An audience(default) in a live-broadcast profile. + * @param tokenExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set tokenExpire as 600(seconds). + * @param privilegeExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to enable your privilege for 10 minutes, set privilegeExpire as 600(seconds). + * @return The RTC and RTM token. + */ + fun buildTokenWithRtm( + appId: String, + appCertificate: String, + channelName: String, + account: String, + role: Role, + tokenExpire: Int, + privilegeExpire: Int + ): String { + val accessToken = AccessToken2(appId, appCertificate, tokenExpire) + val serviceRtc = AccessToken2.ServiceRtc(channelName, account) + + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, privilegeExpire) + if (role == Role.ROLE_PUBLISHER) { + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM, privilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM, privilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM, privilegeExpire) + } + accessToken.addService(serviceRtc) + + val serviceRtm = AccessToken2.ServiceRtm(account) + serviceRtm.addPrivilegeRtm(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN, tokenExpire) + accessToken.addService(serviceRtm) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } + + /** + * Build the RTC and RTM token with account. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param channelName: Unique channel name for the AgoraRTC session in the string format + * @param rtcAccount: The RTC user's account, max length is 255 Bytes. + * @param rtcRole: ROLE_PUBLISHER: A broadcaster/host in a live-broadcast profile. + * ROLE_SUBSCRIBER: An audience(default) in a live-broadcast profile. + * @param rtcTokenExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set rtcTokenExpire as 600(seconds). + * @param joinChannelPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to join channel and expect stay in the channel for 10 minutes, set joinChannelPrivilegeExpire as 600(seconds). + * @param pubAudioPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish audio privilege for 10 minutes, set pubAudioPrivilegeExpire as 600(seconds). + * @param pubVideoPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish video privilege for 10 minutes, set pubVideoPrivilegeExpire as 600(seconds). + * @param pubDataStreamPrivilegeExpire represented by the number of seconds elapsed since now. + * If, for example, you want to enable publish data stream privilege for 10 minutes, set pubDataStreamPrivilegeExpire as 600(seconds). + * @param rtmUserId: The RTM user's account, max length is 64 Bytes. + * @param rtmTokenExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set rtmTokenExpire as 600(seconds). + * @return The RTC and RTM token. + */ + fun buildTokenWithRtm2( + appId: String, + appCertificate: String, + channelName: String, + rtcAccount: String, + rtcRole: Role, + rtcTokenExpire: Int, + joinChannelPrivilegeExpire: Int, + pubAudioPrivilegeExpire: Int, + pubVideoPrivilegeExpire: Int, + pubDataStreamPrivilegeExpire: Int, + rtmUserId: String, + rtmTokenExpire: Int + ): String { + val accessToken = AccessToken2(appId, appCertificate, rtcTokenExpire) + val serviceRtc = AccessToken2.ServiceRtc(channelName, rtcAccount) + + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, joinChannelPrivilegeExpire) + if (rtcRole == Role.ROLE_PUBLISHER) { + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_AUDIO_STREAM, pubAudioPrivilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_VIDEO_STREAM, pubVideoPrivilegeExpire) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_PUBLISH_DATA_STREAM, pubDataStreamPrivilegeExpire) + } + accessToken.addService(serviceRtc) + + val serviceRtm = AccessToken2.ServiceRtm(rtmUserId) + serviceRtm.addPrivilegeRtm(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN, rtmTokenExpire) + accessToken.addService(serviceRtm) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/RtmTokenBuilder2.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/RtmTokenBuilder2.kt new file mode 100644 index 00000000..1fc267fe --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/RtmTokenBuilder2.kt @@ -0,0 +1,30 @@ +package io.agora.media + +class RtmTokenBuilder2 { + /** + * Build the RTM token with userId. + * + * @param appId: The App ID issued to you by Agora. Apply for a new App ID from + * Agora Dashboard if it is missing from your kit. See Get an App ID. + * @param appCertificate: Certificate of the application that you registered in + * the Agora Dashboard. See Get an App Certificate. + * @param userId: The user's account, max length is 64 Bytes. + * @param tokenExpire: represented by the number of seconds elapsed since now. If, for example, + * you want to access the Agora Service within 10 minutes after the token is generated, + * set tokenExpire as 600(seconds). + * @return The RTM token. + */ + fun buildToken(appId: String, appCertificate: String, userId: String, tokenExpire: Int): String { + val accessToken = AccessToken2(appId, appCertificate, tokenExpire) + val serviceRtm = AccessToken2.ServiceRtm(userId) + serviceRtm.addPrivilegeRtm(AccessToken2.PrivilegeRtm.PRIVILEGE_LOGIN, tokenExpire) + accessToken.addService(serviceRtm) + + return try { + accessToken.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/Utils.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/Utils.kt new file mode 100644 index 00000000..ef008e4f --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/main/kotlin/io/agora/media/Utils.kt @@ -0,0 +1,100 @@ +package io.agora.media + +import java.io.ByteArrayOutputStream +import java.nio.charset.StandardCharsets +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.util.* +import java.util.zip.CRC32 +import java.util.zip.Deflater +import java.util.zip.Inflater +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +object Utils { + const val HMAC_SHA256_LENGTH = 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(StandardCharsets.UTF_8), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(keySpec) + return mac.doFinal(msg) + } + + fun base64Encode(data: ByteArray): String { + return Base64.getEncoder().encodeToString(data) + } + + fun base64Decode(data: String): ByteArray { + return Base64.getDecoder().decode(data) + } + + fun crc32(data: String): Int { + return crc32(data.toByteArray(StandardCharsets.UTF_8)) + } + + fun crc32(bytes: ByteArray): Int { + val checksum = CRC32() + checksum.update(bytes) + return checksum.value.toInt() + } + + fun getTimestamp(): Int { + return (System.currentTimeMillis() / 1000).toInt() + } + + fun randomInt(): Int { + return SecureRandom().nextInt() + } + + fun isUUID(uuid: String?): Boolean { + if (uuid == null || uuid.length != 32) { + return false + } + return uuid.matches(Regex("\\p{XDigit}+")) + } + + fun compress(data: ByteArray): ByteArray { + val deflater = Deflater() + val bos = ByteArrayOutputStream(data.size) + return try { + deflater.reset() + deflater.setInput(data) + deflater.finish() + val buf = ByteArray(data.size) + while (!deflater.finished()) { + val i = deflater.deflate(buf) + bos.write(buf, 0, i) + } + bos.toByteArray() + } catch (e: Exception) { + e.printStackTrace() + data + } finally { + deflater.end() + } + } + + fun decompress(data: ByteArray): ByteArray { + val inflater = Inflater() + val bos = ByteArrayOutputStream(data.size) + return try { + inflater.setInput(data) + val buf = ByteArray(8192) + var len: Int + while (inflater.inflate(buf).also { len = it } > 0) { + bos.write(buf, 0, len) + } + bos.toByteArray() + } catch (e: Exception) { + e.printStackTrace() + ByteArray(0) + } finally { + inflater.end() + } + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/sample/kotlin/io/agora/sample/RtcTokenBuilder2Sample.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/sample/kotlin/io/agora/sample/RtcTokenBuilder2Sample.kt new file mode 100644 index 00000000..837cc462 --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/sample/kotlin/io/agora/sample/RtcTokenBuilder2Sample.kt @@ -0,0 +1,61 @@ +package io.agora.sample + +import io.agora.media.RtcTokenBuilder2 + +object RtcTokenBuilder2Sample { + // Need to set environment variable AGORA_APP_ID + private val appId = System.getenv("AGORA_APP_ID") + + // Need to set environment variable AGORA_APP_CERTIFICATE + private val appCertificate = System.getenv("AGORA_APP_CERTIFICATE") + + private const val channelName = "7d72365eb983485397e3e3f9d460bdda" + private const val account = "2082341273" + private const val uid = 2082341273 + private const val tokenExpirationInSeconds = 3600 + private const val privilegeExpirationInSeconds = 3600 + private const val joinChannelPrivilegeExpireInSeconds = 3600 + private const val pubAudioPrivilegeExpireInSeconds = 3600 + private const val pubVideoPrivilegeExpireInSeconds = 3600 + private const val pubDataStreamPrivilegeExpireInSeconds = 3600 + + @JvmStatic + fun main(args: Array) { + println("App Id: $appId") + println("App Certificate: $appCertificate") + if (appId == null || appId.isEmpty() || appCertificate == null || appCertificate.isEmpty()) { + println("Need to set environment variable AGORA_APP_ID and AGORA_APP_CERTIFICATE") + return + } + + val tokenBuilder = RtcTokenBuilder2() + var result = tokenBuilder.buildTokenWithUid( + appId, appCertificate, channelName, uid, RtcTokenBuilder2.Role.ROLE_PUBLISHER, + tokenExpirationInSeconds, privilegeExpirationInSeconds + ) + println("Token with uid: $result") + + result = tokenBuilder.buildTokenWithUserAccount( + appId, appCertificate, channelName, account, + RtcTokenBuilder2.Role.ROLE_PUBLISHER, + tokenExpirationInSeconds, privilegeExpirationInSeconds + ) + println("Token with account: $result") + + result = tokenBuilder.buildTokenWithUid( + appId, appCertificate, channelName, uid, tokenExpirationInSeconds, + joinChannelPrivilegeExpireInSeconds, pubAudioPrivilegeExpireInSeconds, + pubVideoPrivilegeExpireInSeconds, + pubDataStreamPrivilegeExpireInSeconds + ) + println("Token with uid and privilege: $result") + + result = tokenBuilder.buildTokenWithUserAccount( + appId, appCertificate, channelName, account, + tokenExpirationInSeconds, + joinChannelPrivilegeExpireInSeconds, pubAudioPrivilegeExpireInSeconds, + pubVideoPrivilegeExpireInSeconds, pubDataStreamPrivilegeExpireInSeconds + ) + println("Token with account and privilege: $result") + } +} diff --git a/DynamicKey/AgoraDynamicKey/kotlin/src/test/kotlin/io/agora/media/AccessToken2Test.kt b/DynamicKey/AgoraDynamicKey/kotlin/src/test/kotlin/io/agora/media/AccessToken2Test.kt new file mode 100644 index 00000000..7d7134fc --- /dev/null +++ b/DynamicKey/AgoraDynamicKey/kotlin/src/test/kotlin/io/agora/media/AccessToken2Test.kt @@ -0,0 +1,82 @@ +package io.agora.media + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class AccessToken2Test { + private val appId = "970CA35de60c44645bbae8a215061b33" + private val appCertificate = "5CFd2fd1755d40ecb72977518be15d3b" + private val channelName = "7d72365eb983485397e3e3f9d460bdda" + private val expire = 600 + private val issueTs = 1111111 + private val salt = 1 + private val uid = "2882341273" + private val userId = "test_user" + + @Test + @Throws(Exception::class) + fun build() { + val accessToken = AccessToken2(appId, appCertificate, expire) + accessToken.issueTs = issueTs + accessToken.salt = salt + + assertEquals(appCertificate, accessToken.appCert) + assertEquals(appId, accessToken.appId) + assertEquals(expire, accessToken.expire) + assertEquals(issueTs, accessToken.issueTs) + assertEquals(salt, accessToken.salt) + + val token = accessToken.build() + assertEquals( + "007eJxTYEiJ9+zw7Gb1viNuGtMfy3JriuZNp+1h1iLu/rOePHlS91WBwdLcwNnR2DQl1cwg2cTEzMQ0KSkx1SLRyNDUwMwwydjY/YsAQwQTAwMjAwgAAKtnGK8=", + token + ) + } + + @Test + @Throws(Exception::class) + fun build_ServiceRtc() { + val accessToken = AccessToken2(appId, appCertificate, expire) + accessToken.issueTs = issueTs + accessToken.salt = salt + + val serviceRtc = AccessToken2.ServiceRtc(channelName, uid) + serviceRtc.addPrivilegeRtc(AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL, expire) + accessToken.addService(serviceRtc) + + assertEquals(channelName, serviceRtc.channelName) + assertEquals(uid, serviceRtc.uid) + + val token = accessToken.build() + assertEquals( + "007eJxTYBBbsMMnKq7p9Hf/HcIX5kce9b518kCiQgSr5Zrp4X1Tu6UUGCzNDZwdjU1TUs0Mkk1MzExMk5ISUy0SjQxNDcwMk4yN3b8IMEQwMTAwMoAwBIL4CgzmKeZGxmamqUmWFsYmFqbGluapxqnGaZYpJmYGSSkpiVwMRhYWRsYmhkbmxgDCaiTj", + token + ) + } + + @Test + fun parse_TokenRtc() { + val accessToken = AccessToken2() + val res = accessToken.parse( + "007eJxTYBBbsMMnKq7p9Hf/HcIX5kce9b518kCiQgSr5Zrp4X1Tu6UUGCzNDZwdjU1TUs0Mkk1MzExMk5ISUy0SjQxNDcwMk4yN3b8IMEQwMTAwMoAwBIL4CgzmKeZGxmamqUmWFsYmFqbGluapxqnGaZYpJmYGSSkpiVwMRhYWRsYmhkbmxgDCaiTj" + ) + assertTrue(res) + assertEquals(appId, accessToken.appId) + assertEquals(expire, accessToken.expire) + assertEquals(issueTs, accessToken.issueTs) + assertEquals(salt, accessToken.salt) + assertEquals(1, accessToken.services.size) + val serviceRtc = accessToken.services[AccessToken2.SERVICE_TYPE_RTC] as AccessToken2.ServiceRtc + assertEquals(channelName, serviceRtc.channelName) + assertEquals(uid, serviceRtc.uid) + assertEquals(expire.toLong(), serviceRtc.privileges[AccessToken2.PrivilegeRtc.PRIVILEGE_JOIN_CHANNEL.intValue]?.toLong()) + } + + @Test + fun getUidStr() { + assertEquals("", AccessToken2.getUidStr(0)) + assertEquals("123", AccessToken2.getUidStr(123)) + } +}