diff --git a/.gitignore b/.gitignore index 25428b6..45f55c2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ *.so *.dylib main -agora-token-server +agora-token-service # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index b799eb4..176ee3c 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,11 @@ The `rtc` token endpoint requires a `tokenType` (uid || userAccount), `channelNa response: ``` json -{"rtcToken":" "} +{"rtcToken":"007rtc-token-djfkaljdla"} ``` -## RTM Token ## +### RTM Token ### + The `rtm` token endpoint requires the user's `rtmuid`. `expiry(optional)` Pass an integer to represent the privelege lifetime in seconds. **endpoint structure** @@ -94,10 +95,10 @@ The `rtm` token endpoint requires the user's `rtmuid`. response: ``` json -{"rtmToken":" "} +{"rtmToken":"007rtm-token-djfkaljdla"} ``` -### Both Tokens ### +### RTM + RTC Tokens ### The `rte` token endpoint generates both the `rtc` and `rtm` tokens with a single request. This endpoint requires a `tokenType` (uid || userAccount), `channelName`, the user's `rtcuid` (type varies `String/Int` based on `tokenType`) and `rtmuid` which is a `String`. Omitting `rtmuid` will assume it's the same as `rtcuid`. `expiry(optional)` Pass an integer to represent the token lifetime in seconds. @@ -109,12 +110,35 @@ The `rte` token endpoint generates both the `rtc` and `rtm` tokens with a single response: ``` json { - "rtcToken":"rtc-token-djfkaljdla", - "rtmToken":"rtm-token-djfkaljdla" + "rtcToken":"007rtc-token-djfkaljdla", + "rtmToken":"007rtm-token-djfkaljdla" +} +``` + +### Chat Tokens ### + +#### endpoint structure #### + +app privileges: +``` +chat/app/?expiry=3600 +``` + +user privileges: +``` +/chat/account/:chatid/?expiry=3600 +``` + +`expiry` is an optional parameter for both. + +response: +``` json +{ + "chatToken":"007chat-token-djfkaljdla" } ``` -### Contributions +## Contributions Contributions are welcome, please test any changes to the Go code with the following command: diff --git a/go.mod b/go.mod index 7f988b8..fcc4574 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AgoraIO-Community/agora-token-service go 1.19 require ( - github.com/AgoraIO-Community/go-tokenbuilder v1.1.0 + github.com/AgoraIO-Community/go-tokenbuilder v1.2.0 github.com/gin-gonic/gin v1.9.1 github.com/joho/godotenv v1.3.0 ) diff --git a/go.sum b/go.sum index 5e002ad..06aaab5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/AgoraIO-Community/go-tokenbuilder v1.1.0 h1:4iuyLV0gPq8D9DVtXa1NsP/8Yvmfy+IVtZsU+LfoTGg= -github.com/AgoraIO-Community/go-tokenbuilder v1.1.0/go.mod h1:xqPdaiFG00M1hNN/CCYh8j+NTmkiJsQtqYdf4YAlncA= +github.com/AgoraIO-Community/go-tokenbuilder v1.2.0 h1:Ktlv7n8PhmSap3tgNxjNHO8hj/qydUIj4UOFF8tRxos= +github.com/AgoraIO-Community/go-tokenbuilder v1.2.0/go.mod h1:xqPdaiFG00M1hNN/CCYh8j+NTmkiJsQtqYdf4YAlncA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= diff --git a/service/endpoints_test.go b/service/endpoints_test.go index 3b581df..44cf5da 100644 --- a/service/endpoints_test.go +++ b/service/endpoints_test.go @@ -10,40 +10,22 @@ import ( func TestRtcValidAndInvalid(t *testing.T) { - // Create a new gin engine for testing - reqValid, err := http.NewRequest(http.MethodGet, "/rtc/fsda/publisher/uid/0/?expiry=600", nil) - if err != nil { - t.Fatal(err) + tests := []UrlCodePair{ + {"/rtc/fsda/publisher/uid/0/?expiry=600", http.StatusOK}, + {"/rtc/fsda/publisher/uid//?expiry=600", http.StatusOK}, + {"/rtc/fsda/publisher/uid/test/?expiry=600", http.StatusBadRequest}, + {"/rtc/fsda/publisher/uid/0/?expiry=failing", http.StatusBadRequest}, } - reqInvalid, err := http.NewRequest(http.MethodGet, "/rtc/fsda/publisher/uid/test/?expiry=600", nil) - if err != nil { - t.Fatal(err) + for _, httpTest := range tests { + testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil) + if err != nil { + t.Fatal(err) + } + resp := httptest.NewRecorder() + // Call the endpoint + testService.Server.Handler.ServeHTTP(resp, testApi) + assert.Equal(t, httpTest.code, resp.Code, resp.Body) } - reqInvalidExp, err := http.NewRequest(http.MethodGet, "/rtc/fsda/publisher/uid/0/?expiry=failing", nil) - if err != nil { - t.Fatal(err) - } - // Create a response recorder to inspect the response - resp := httptest.NewRecorder() - - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqValid) - - assert.Equal(t, http.StatusOK, resp.Code, resp.Body) - - resp = httptest.NewRecorder() - - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqInvalid) - - assert.Equal(t, http.StatusBadRequest, resp.Code) - - resp = httptest.NewRecorder() - - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqInvalidExp) - - assert.Equal(t, http.StatusBadRequest, resp.Code) } func TestRtmValidAndInvalid(t *testing.T) { @@ -73,54 +55,53 @@ func TestRtmValidAndInvalid(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.Code) } -func TestRteValidAndInvalid(t *testing.T) { +type UrlCodePair struct { + url string + code int +} - // Create a new gin engine for testing - reqValid, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/0/rtmid/?expiry=600", nil) - if err != nil { - t.Fatal(err) - } - // Create a new gin engine for testing - reqValid2, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/2345/?expiry=600", nil) - if err != nil { - t.Fatal(err) +func TestChatValidAndInvalid(t *testing.T) { + + tests := []UrlCodePair{ + {"/chat/app/", http.StatusOK}, + {"/chat/account/username/", http.StatusOK}, + {"/chat/account/", http.StatusNotFound}, + {"/chat/invalid/", http.StatusNotFound}, + {"/chat/account/username/?expiry=600", http.StatusOK}, + {"/chat/account/username/?expiry=fail", http.StatusBadRequest}, } - reqInvalid, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/0/?expiry=600", nil) - if err != nil { - t.Fatal(err) + for _, httpTest := range tests { + testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil) + if err != nil { + t.Fatal(err) + } + resp := httptest.NewRecorder() + // Call the endpoint + testService.Server.Handler.ServeHTTP(resp, testApi) + assert.Equal(t, httpTest.code, resp.Code, resp.Body) } - reqInvalidExp, err := http.NewRequest(http.MethodGet, "/rte/channelName/publisher/uid/2345/?expiry=failing", nil) - if err != nil { - t.Fatal(err) - } - // Create a response recorder to inspect the response - resp := httptest.NewRecorder() - - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqValid) - - assert.Equal(t, http.StatusOK, resp.Code) - - resp = httptest.NewRecorder() - - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqValid2) - - assert.Equal(t, http.StatusOK, resp.Code) - - resp = httptest.NewRecorder() - - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqInvalid) - - assert.Equal(t, http.StatusBadRequest, resp.Code) - - resp = httptest.NewRecorder() +} - // Call the endpoint - testService.Server.Handler.ServeHTTP(resp, reqInvalidExp) +func TestRteValidAndInvalid(t *testing.T) { - assert.Equal(t, http.StatusBadRequest, resp.Code) + tests := []UrlCodePair{ + {"/rte/channelName/publisher/uid/0/rtmid/?expiry=600", http.StatusOK}, + {"/rte/channelName/publisher/uid/2345/?expiry=600", http.StatusOK}, + {"/rte/channelName/subscriber/uid/0/rtmid/?expiry=600", http.StatusOK}, + {"/rte/channelName/subscriber/uid/2345/?expiry=600", http.StatusOK}, + {"/rte/channelName/publisher/uid/0/?expiry=600", http.StatusBadRequest}, + {"/rte/channelName/publisher/uid/2345/?expiry=failing", http.StatusBadRequest}, + } + for _, httpTest := range tests { + testApi, err := http.NewRequest(http.MethodGet, httpTest.url, nil) + if err != nil { + t.Fatal(err) + } + resp := httptest.NewRecorder() + // Call the endpoint + testService.Server.Handler.ServeHTTP(resp, testApi) + assert.Equal(t, httpTest.code, resp.Code, resp.Body) + } } func TestTokentypes(t *testing.T) { diff --git a/service/http_handlers.go b/service/http_handlers.go index f2c9c2d..9e9f384 100644 --- a/service/http_handlers.go +++ b/service/http_handlers.go @@ -49,7 +49,7 @@ func (s *Service) getRtmToken(c *gin.Context) { if err != nil { c.Error(err) c.AbortWithStatusJSON(400, gin.H{ - "message": "Error Generating RTC token: " + err.Error(), + "message": "Error Generating RTM token: " + err.Error(), "status": 400, }) return @@ -72,6 +72,36 @@ func (s *Service) getRtmToken(c *gin.Context) { } } +func (s *Service) getChatToken(c *gin.Context) { + log.Println("Generating Chat token") + // get param values + uidStr, tokenType, expireTimestamp, err := s.parseChatParams(c) + + if err != nil { + c.AbortWithStatusJSON(400, gin.H{ + "message": "Error Generating Chat token: " + err.Error(), + "status": 400, + }) + return + } + + chatToken, tokenErr := s.generateChatToken(uidStr, tokenType, expireTimestamp) + + if tokenErr != nil { + c.Error(tokenErr) + errMsg := "Error Generating Chat token: " + tokenErr.Error() + c.AbortWithStatusJSON(400, gin.H{ + "error": errMsg, + "status": 400, + }) + } else { + log.Println("Chat Token generated") + c.JSON(200, gin.H{ + "chatToken": chatToken, + }) + } +} + func (s *Service) getRtcRtmToken(c *gin.Context) { log.Println("Generating RTC and RTM tokens") // get rtc param values diff --git a/service/main_test.go b/service/main_test.go index c527b83..a66f3fb 100644 --- a/service/main_test.go +++ b/service/main_test.go @@ -13,9 +13,10 @@ var testService *Service func TestMain(m *testing.M) { gin.SetMode(gin.TestMode) - // os.Setenv("APP_ID", "example-app-id") - // os.Setenv("APP_CERTIFICATE", "example-app-certificate") - os.Setenv("SERVER_PORT", "8080") + // os.Setenv("APP_ID", "18c2dfa12345678987654321fb84931d") + // os.Setenv("APP_CERTIFICATE", "12345712345678765432117b84ad9ef9") + // os.Setenv("SERVER_PORT", "8080") + // os.Setenv("PORT", "8080") testService = NewService() os.Exit(m.Run()) } diff --git a/service/parsing.go b/service/parsing.go index bba2565..843a4d9 100644 --- a/service/parsing.go +++ b/service/parsing.go @@ -3,6 +3,7 @@ package service import ( "fmt" "strconv" + "strings" "time" rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder" @@ -37,11 +38,7 @@ func (s *Service) parseRtcParams(c *gin.Context) (channelName, tokenType, uidStr expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64) if parseErr != nil { // if string conversion fails return an error - if err != nil { - err = fmt.Errorf("%s. Also failed to parse expireTime: %s, causing error: %s", err, expireTime, parseErr) - } else { - err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) - } + err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) } // set timestamps @@ -75,3 +72,35 @@ func (s *Service) parseRtmParams(c *gin.Context) (uidStr string, expireTimestamp // check if string conversion fails return uidStr, expireTimestamp, err } + +func (s *Service) parseChatParams(c *gin.Context) (uidStr string, tokenType string, expireTimestamp uint32, err error) { + // get param values + uidStr = c.Param("chatid") + urlSplit := strings.Split(c.Request.URL.Path, "/") + for i := range urlSplit { + if urlSplit[i] == "chat" { + tokenType = urlSplit[i+1] + break + } + } + expireTime := c.DefaultQuery("expiry", "3600") + if tokenType == "account" { + tokenType = "userAccount" + } + if uidStr == "" && tokenType != "app" { + err = fmt.Errorf("userAccount type requires chat ID") + return uidStr, tokenType, expireTimestamp, err + } + expireTime64, parseErr := strconv.ParseUint(expireTime, 10, 64) + if parseErr != nil { + // if string conversion fails return an error + err = fmt.Errorf("failed to parse expireTime: %s, causing error: %s", expireTime, parseErr) + } + // set timestamps + expireTimeInSeconds := uint32(expireTime64) + currentTimestamp := uint32(time.Now().UTC().Unix()) + expireTimestamp = currentTimestamp + expireTimeInSeconds + + // check if string conversion fails + return uidStr, tokenType, expireTimestamp, err +} diff --git a/service/service.go b/service/service.go index 685cddb..ec1088d 100644 --- a/service/service.go +++ b/service/service.go @@ -83,6 +83,8 @@ func NewService() *Service { api.GET("rtm/:rtmuid/", s.getRtmToken) api.GET("rte/:channelName/:role/:tokenType/:rtcuid/", s.getRtcRtmToken) api.GET("rte/:channelName/:role/:tokenType/:rtcuid/:rtmuid/", s.getRtcRtmToken) + api.GET("chat/app/", s.getChatToken) // Chat token for API calls + api.GET("chat/account/:chatid/", s.getChatToken) // Chat token for SDK calls s.Server.Handler = api return s diff --git a/service/tokens.go b/service/tokens.go index bfda2b2..d018f01 100644 --- a/service/tokens.go +++ b/service/tokens.go @@ -5,6 +5,7 @@ import ( "log" "strconv" + "github.com/AgoraIO-Community/go-tokenbuilder/chatTokenBuilder" rtctokenbuilder2 "github.com/AgoraIO-Community/go-tokenbuilder/rtctokenbuilder" ) @@ -33,3 +34,20 @@ func (s *Service) generateRtcToken(channelName, uidStr, tokenType string, role r return "", err } } + +func (s *Service) generateChatToken(uidStr string, tokenType string, expireTimestamp uint32) (chatToken string, err error) { + + if tokenType == "userAccount" { + log.Printf("Building Token with userAccount: %s\n", uidStr) + chatToken, err = chatTokenBuilder.BuildChatUserToken(s.appID, s.appCertificate, uidStr, expireTimestamp) + return chatToken, err + + } else if tokenType == "app" { + chatToken, err = chatTokenBuilder.BuildChatAppToken(s.appID, s.appCertificate, expireTimestamp) + return chatToken, err + } else { + err = fmt.Errorf("failed to generate Chat token for Unknown token type: %s", tokenType) + log.Println(err) + return "", err + } +}