From 48cb88c0ec496d446faddd4cf3a36c01e0022763 Mon Sep 17 00:00:00 2001 From: rxdn <29165304+rxdn@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:16:58 +0100 Subject: [PATCH] Secret validation --- .../api/integrations/activateintegration.go | 55 +++++++++++++++++++ .../api/integrations/createintegration.go | 7 ++- .../api/integrations/updateintegration.go | 6 +- app/http/server.go | 8 ++- cmd/panel/main.go | 2 + config/config.go | 2 + .../src/components/IntegrationEditor.svelte | 15 +++++ go.mod | 4 +- go.sum | 9 ++- utils/secureproxy.go | 7 +++ 10 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 utils/secureproxy.go 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 @@ +
+

Secret Validation (Optional)

+
+

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.

+ +
+
+

Request Headers

@@ -366,6 +377,10 @@ if (data.privacy_policy_url !== undefined && data.privacy_policy_url !== null && data.privacy_policy_url.length === 0) { data.privacy_policy_url = null; } + + if (data.validation_url !== undefined && data.validation_url !== null && data.validation_url.length === 0) { + data.validation_url = null; + } } function updateExampleJson() { diff --git a/go.mod b/go.mod index 0fb5484..b7b3665 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.18 require ( github.com/BurntSushi/toml v0.3.1 github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc - github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 - github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b + github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26 + github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408 github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c github.com/TicketsBot/worker v0.0.0-20220830131837-12d85aca5c71 github.com/apex/log v1.1.2 diff --git a/go.sum b/go.sum index 9b5323c..aee0cde 100644 --- a/go.sum +++ b/go.sum @@ -37,10 +37,10 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/ReneKroon/ttlcache v1.6.0/go.mod h1:DG6nbhXKUQhrExfwwLuZUdH7UnRDDRA1IW+nBuCssvs= github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc h1:n15W8Eg+ik3/0yqPzZVRP2oZJcIZCIgQ071cZleedKo= github.com/TicketsBot/archiverclient v0.0.0-20220326163414-558fd52746dc/go.mod h1:2KcfHS0JnSsgcxZBs3NyWMXNQzEo67mBSGOyzHPWOCc= -github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42 h1:3/qnbrEfL8gqSbjJ4o7WKkdoPngmhjAGEXFwteEjpqs= -github.com/TicketsBot/common v0.0.0-20220703211704-f792aa9f0c42/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM= -github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b h1:ZlPTCuJVEjvt6Rdz1mUgkJUVMj8XrLOwrbh05vcl1KI= -github.com/TicketsBot/database v0.0.0-20221223231047-b0d3d36c563b/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI= +github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26 h1:jFQj7JTrDULhSpVqn1L3RUs5YOghOvsGMCjndTrv6HM= +github.com/TicketsBot/common v0.0.0-20230608150251-8d29dcf6ae26/go.mod h1:WxHh6bY7KhIqdayeOp5f0Zj2NNi/7QqCQfMEqHnpdAM= +github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408 h1:MkcYud/oOmmplBIK7zWhgnnE5vU9XSnS4e30vH2+Rv0= +github.com/TicketsBot/database v0.0.0-20230608141414-836e32290408/go.mod h1:gAtOoQKZfCkQ4AoNWQUSl51Fnlqk+odzD/hZ1e1sXyI= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c h1:OqGjFH6mbE6gd+NqI2ARJdtH3UUvhiAkD0r0fhGJK2s= github.com/TicketsBot/logarchiver v0.0.0-20220326162808-cdf0310f5e1c/go.mod h1:jgi2OXQKsd5nUnTIRkwvPmeuD/i7OhN68LKMssuQY1c= github.com/TicketsBot/ttlcache v1.6.1-0.20200405150101-acc18e37b261 h1:NHD5GB6cjlkpZFjC76Yli2S63/J2nhr8MuE6KlYJpQM= @@ -481,7 +481,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= diff --git a/utils/secureproxy.go b/utils/secureproxy.go new file mode 100644 index 0000000..a0f75ac --- /dev/null +++ b/utils/secureproxy.go @@ -0,0 +1,7 @@ +package utils + +import ( + "github.com/TicketsBot/common/secureproxy" +) + +var SecureProxyClient *secureproxy.Client