diff --git a/app/http/endpoints/api/integrations/activateintegration.go b/app/http/endpoints/api/integrations/activateintegration.go index ea85f59..f49eff9 100644 --- a/app/http/endpoints/api/integrations/activateintegration.go +++ b/app/http/endpoints/api/integrations/activateintegration.go @@ -1,9 +1,11 @@ package api import ( + "encoding/json" dbclient "github.com/TicketsBot/GoPanel/database" "github.com/TicketsBot/GoPanel/utils" "github.com/gin-gonic/gin" + "net/http" "strconv" ) @@ -38,6 +40,17 @@ func ActivateIntegrationHandler(ctx *gin.Context) { return } + integration, ok, err := dbclient.Client.CustomIntegrations.Get(integrationId) + if err != nil { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + + if !ok { + ctx.JSON(404, utils.ErrorStr("Integration not found")) + return + } + // Check the integration is public or the user created it canActivate, err := dbclient.Client.CustomIntegrationGuilds.CanActivate(integrationId, userId) if err != nil { @@ -87,6 +100,48 @@ func ActivateIntegrationHandler(ctx *gin.Context) { } } + // Validate secrets + if integration.Public && integration.Approved && integration.ValidationUrl != nil { + res, statusCode, err := utils.SecureProxyClient.DoRequest(http.MethodPost, *integration.ValidationUrl, nil, data.Secrets) + if err != nil { + if statusCode == http.StatusRequestTimeout { + ctx.JSON(400, utils.ErrorStr("Secret validation server did not respond in time (contact the integration author)")) + return + } else { + ctx.JSON(500, utils.ErrorJson(err)) + return + } + } + + type validationResponse struct { + Error string `json:"error"` + } + + var useClientError bool + var parsed validationResponse + if err := json.Unmarshal(res, &parsed); err == nil { + useClientError = len(parsed.Error) > 0 + + if len(parsed.Error) > 255 { + parsed.Error = parsed.Error[:255] + } + } + + if statusCode > 299 { + if useClientError { + ctx.JSON(400, gin.H{ + "success": false, + "error": "Integration rejected the secret values (contact the integration author for help)", + "client_error": parsed.Error, + }) + } else { + ctx.JSON(400, utils.ErrorStr("Integration rejected the secret values (contact the integration author for help)")) + } + + return + } + } + if err := dbclient.Client.CustomIntegrationGuilds.AddToGuildWithSecrets(integrationId, guildId, secretMap); err != nil { ctx.JSON(500, utils.ErrorJson(err)) return diff --git a/app/http/endpoints/api/integrations/createintegration.go b/app/http/endpoints/api/integrations/createintegration.go index 48807cf..4bfab02 100644 --- a/app/http/endpoints/api/integrations/createintegration.go +++ b/app/http/endpoints/api/integrations/createintegration.go @@ -14,8 +14,9 @@ type integrationCreateBody struct { ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"` PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"` - Method string `json:"http_method" validate:"required,oneof=GET POST"` - WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255"` + Method string `json:"http_method" validate:"required,oneof=GET POST"` + WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"` + ValidationUrl *string `json:"validation_url" validate:"omitempty,url,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"` Secrets []struct { Name string `json:"name" validate:"required,min=1,max=32,excludesall=% "` @@ -67,7 +68,7 @@ func CreateIntegrationHandler(ctx *gin.Context) { return } - integration, err := dbclient.Client.CustomIntegrations.Create(userId, data.WebhookUrl, data.Method, data.Name, data.Description, data.ImageUrl, data.PrivacyPolicyUrl) + integration, err := dbclient.Client.CustomIntegrations.Create(userId, data.WebhookUrl, data.ValidationUrl, data.Method, data.Name, data.Description, data.ImageUrl, data.PrivacyPolicyUrl) if err != nil { ctx.JSON(500, utils.ErrorJson(err)) return diff --git a/app/http/endpoints/api/integrations/updateintegration.go b/app/http/endpoints/api/integrations/updateintegration.go index 3a40963..002855c 100644 --- a/app/http/endpoints/api/integrations/updateintegration.go +++ b/app/http/endpoints/api/integrations/updateintegration.go @@ -17,8 +17,9 @@ type integrationUpdateBody struct { ImageUrl *string `json:"image_url" validate:"omitempty,url,max=255,startswith=https://"` PrivacyPolicyUrl *string `json:"privacy_policy_url" validate:"omitempty,url,max=255,startswith=https://"` - Method string `json:"http_method" validate:"required,oneof=GET POST"` - WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255"` + Method string `json:"http_method" validate:"required,oneof=GET POST"` + WebhookUrl string `json:"webhook_url" validate:"required,webhook,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"` + ValidationUrl *string `json:"validation_url" validate:"omitempty,url,max=255,startsnotwith=https://discord.com,startsnotwith=https://discord.gg"` Secrets []struct { Id int `json:"id" validate:"omitempty,min=1"` @@ -93,6 +94,7 @@ func UpdateIntegrationHandler(ctx *gin.Context) { OwnerId: integration.OwnerId, HttpMethod: data.Method, WebhookUrl: data.WebhookUrl, + ValidationUrl: data.ValidationUrl, Name: data.Name, Description: data.Description, ImageUrl: data.ImageUrl, diff --git a/app/http/server.go b/app/http/server.go index 1ec3254..9f1a00f 100644 --- a/app/http/server.go +++ b/app/http/server.go @@ -160,7 +160,13 @@ func StartServer() { guildAuthApiAdmin.GET("/integrations/available", api_integrations.ListIntegrationsHandler) guildAuthApiAdmin.GET("/integrations/:integrationid", api_integrations.IsIntegrationActiveHandler) - guildAuthApiAdmin.POST("/integrations/:integrationid", api_integrations.ActivateIntegrationHandler) + guildAuthApiAdmin.POST("/integrations/:integrationid", + rl(middleware.RateLimitTypeUser, 10, time.Minute), + rl(middleware.RateLimitTypeGuild, 10, time.Minute), + rl(middleware.RateLimitTypeUser, 30, time.Minute*30), + rl(middleware.RateLimitTypeGuild, 30, time.Minute*30), + api_integrations.ActivateIntegrationHandler, + ) guildAuthApiAdmin.PATCH("/integrations/:integrationid", api_integrations.UpdateIntegrationSecretsHandler) guildAuthApiAdmin.DELETE("/integrations/:integrationid", api_integrations.RemoveIntegrationHandler) } diff --git a/cmd/panel/main.go b/cmd/panel/main.go index 2ef0b38..93f8a29 100644 --- a/cmd/panel/main.go +++ b/cmd/panel/main.go @@ -15,6 +15,7 @@ import ( "github.com/TicketsBot/archiverclient" "github.com/TicketsBot/common/chatrelay" "github.com/TicketsBot/common/premium" + "github.com/TicketsBot/common/secureproxy" "github.com/TicketsBot/worker/i18n" "github.com/apex/log" "github.com/getsentry/sentry-go" @@ -54,6 +55,7 @@ func main() { cache.Instance = cache.NewCache() utils.ArchiverClient = archiverclient.NewArchiverClientWithTimeout(config.Conf.Bot.ObjectStore, time.Second*15, []byte(config.Conf.Bot.AesKey)) + utils.SecureProxyClient = secureproxy.NewSecureProxy(config.Conf.SecureProxyUrl) utils.LoadEmoji() diff --git a/config/config.go b/config/config.go index e0705fd..eb92f93 100644 --- a/config/config.go +++ b/config/config.go @@ -20,6 +20,7 @@ type ( Bot Bot Redis Redis Cache Cache + SecureProxyUrl string } Server struct { @@ -177,5 +178,6 @@ func fromEnvvar() { Cache: Cache{ Uri: os.Getenv("CACHE_URI"), }, + SecureProxyUrl: os.Getenv("SECURE_PROXY_URL"), } } diff --git a/frontend/src/components/IntegrationEditor.svelte b/frontend/src/components/IntegrationEditor.svelte index 80738ce..e1c28e3 100644 --- a/frontend/src/components/IntegrationEditor.svelte +++ b/frontend/src/components/IntegrationEditor.svelte @@ -136,6 +136,17 @@ +
You can specify a URL to send a POST request to when a user adds your integration to their server / + updates the secrets. Respond with 2XX if the secrets are valid, or any other status code to reject + them. You can read more about secret validation in our documentation.
+ +