feat: add secret key configuration for webhook authentication

Signed-off-by: zhenyus <zhenyus@mathmast.com>
This commit is contained in:
zhenyus 2025-03-31 00:53:33 +08:00
parent 60817c1be4
commit 32ba41f1f4
6 changed files with 47 additions and 59 deletions

View File

@ -2,6 +2,7 @@ server:
port: 8080 port: 8080
webhookPath: "/webhook" webhookPath: "/webhook"
secretHeader: "X-Gitea-Signature" secretHeader: "X-Gitea-Signature"
secretKey: "custom-secret-key"
jenkins: jenkins:
url: "http://jenkins.example.com" url: "http://jenkins.example.com"

View File

@ -1,10 +1,6 @@
package main package main
import ( import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@ -29,7 +25,8 @@ type Configuration struct {
Server struct { Server struct {
Port int `yaml:"port" validate:"required,gt=0"` Port int `yaml:"port" validate:"required,gt=0"`
WebhookPath string `yaml:"webhookPath" validate:"required"` WebhookPath string `yaml:"webhookPath" validate:"required"`
SecretHeader string `yaml:"secretHeader" default:"X-Gitea-Signature"` SecretHeader string `yaml:"secretHeader" default:"Authorization"`
SecretKey string `yaml:"secretKey"`
} `yaml:"server"` } `yaml:"server"`
Jenkins struct { Jenkins struct {
@ -486,18 +483,20 @@ func handleWebhook(w http.ResponseWriter, r *http.Request) {
// Verify signature if secret token is set // Verify signature if secret token is set
configMutex.RLock() configMutex.RLock()
secretToken := config.Gitea.SecretToken
secretHeader := config.Server.SecretHeader secretHeader := config.Server.SecretHeader
serverSecretKey := config.Server.SecretKey
configMutex.RUnlock() configMutex.RUnlock()
if secretToken != "" { // If server secret key is set, use it as the secret token
signature := r.Header.Get(secretHeader) receivedSecretKey := r.Header.Get(secretHeader)
if !verifySignature(r, signature, secretToken) { if receivedSecretKey == "" {
http.Error(w, "Invalid signature", http.StatusUnauthorized) http.Error(w, "Invalid server secret key", http.StatusUnauthorized)
logWarn("Invalid webhook signature received") logWarn("No secret key provided in header")
} else if receivedSecretKey != serverSecretKey {
http.Error(w, "Invalid server secret key", http.StatusUnauthorized)
logWarn("Invalid server secret key provided")
return return
} }
}
// Read and parse the webhook payload // Read and parse the webhook payload
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
@ -619,47 +618,34 @@ func determineJobName(config ProjectConfig, branchName string) string {
return "" return ""
} }
func verifySignature(r *http.Request, signature string, secret string) bool {
if signature == "" {
return false
}
body, err := io.ReadAll(r.Body)
if err != nil {
return false
}
// Reset the body for subsequent reads
r.Body = io.NopCloser(bytes.NewBuffer(body))
// The signature from Gitea is in format "sha256=HASH"
parts := strings.SplitN(signature, "=", 2)
if len(parts) != 2 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expectedMAC := mac.Sum(nil)
receivedMAC, err := hex.DecodeString(parts[1])
if err != nil {
return false
}
return hmac.Equal(expectedMAC, receivedMAC)
}
func triggerJenkinsJob(job jobRequest) bool { func triggerJenkinsJob(job jobRequest) bool {
configMutex.RLock() configMutex.RLock()
jenkinsURL := fmt.Sprintf("%s/job/%s/buildWithParameters", jenkinsBaseURL := strings.TrimSuffix(config.Jenkins.URL, "/")
strings.TrimSuffix(config.Jenkins.URL, "/"),
job.jobName)
jenkinsUser := config.Jenkins.Username jenkinsUser := config.Jenkins.Username
jenkinsToken := config.Jenkins.Token jenkinsToken := config.Jenkins.Token
configMutex.RUnlock() configMutex.RUnlock()
// Handle Jenkins job paths correctly
// Jenkins jobs can be organized in folders, with proper URL format:
// /job/folder1/job/folder2/job/jobname
jobPath := job.jobName
// If job name contains slashes, format it properly for Jenkins URL
if strings.Contains(jobPath, "/") {
// Replace regular slashes with "/job/" for Jenkins URL format
parts := strings.Split(jobPath, "/")
jobPath = "job/" + strings.Join(parts, "/job/")
} else {
jobPath = "job/" + jobPath
}
jenkinsURL := fmt.Sprintf("%s/%s/buildWithParameters", jenkinsBaseURL, jobPath)
logDebug("Triggering Jenkins job URL: %s", jenkinsURL)
req, err := http.NewRequest("POST", jenkinsURL, nil) req, err := http.NewRequest("POST", jenkinsURL, nil)
if err != nil { if err != nil {
logger.Printf("Error creating Jenkins request for job %s: %v", job.jobName, err) logError("Error creating Jenkins request for job %s: %v", job.jobName, err)
return false return false
} }
@ -678,19 +664,19 @@ func triggerJenkinsJob(job jobRequest) bool {
// Execute request // Execute request
resp, err := httpClient.Do(req) resp, err := httpClient.Do(req)
if err != nil { if err != nil {
logger.Printf("Error triggering Jenkins job %s: %v", job.jobName, err) logError("Error triggering Jenkins job %s: %v", job.jobName, err)
return false return false
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(resp.Body) bodyBytes, _ := io.ReadAll(resp.Body)
logger.Printf("Jenkins returned error for job %s: status=%d, body=%s", logError("Jenkins returned error for job %s: status=%d, URL=%s, body=%s",
job.jobName, resp.StatusCode, string(bodyBytes)) job.jobName, resp.StatusCode, jenkinsURL, string(bodyBytes))
return false return false
} }
logger.Printf("Successfully triggered Jenkins job %s for event %s", logInfo("Successfully triggered Jenkins job %s for event %s",
job.jobName, job.eventID) job.jobName, job.eventID)
return true return true
} }

View File

@ -10,10 +10,11 @@ data:
server: server:
port: 8080 port: 8080
webhookPath: "/webhook" webhookPath: "/webhook"
secretHeader: "X-Gitea-Signature" secretHeader: "Authorization"
secretKey: "r6Y@QTb*7BQN@hDGsN"
jenkins: jenkins:
url: "http://jenkins.freeleaps-devops-system.svc.cluster.local:8080" url: "http://jenkins.freeleaps-devops-system.svc.freeleaps.cluster:8080"
username: "admin" username: "admin"
token: "115127e693f1bc6b7194f58ff6d6283bd0" token: "115127e693f1bc6b7194f58ff6d6283bd0"
timeout: 30 timeout: 30

View File

@ -3,10 +3,10 @@ library 'first-class-pipeline'
executeFreeleapsPipeline { executeFreeleapsPipeline {
serviceName = 'freeleaps' serviceName = 'freeleaps'
environmentSlug = 'alpha' environmentSlug = 'alpha'
serviceGitBranch = 'develop' serviceGitBranch = 'dev'
serviceGitRepo = "https://freeleaps@dev.azure.com/freeleaps/freeleaps-service-hub/_git/freeleaps-service-hub" serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub.git"
serviceGitRepoType = 'monorepo' serviceGitRepoType = 'monorepo'
serviceGitCredentialsId = 'freeleaps-azure-devops-credentials' serviceGitCredentialsId = 'freeleaps-repos-gitea-credentails'
executeMode = 'fully' executeMode = 'fully'
commitMessageLintEnabled = false commitMessageLintEnabled = false
components = [ components = [

View File

@ -3,8 +3,8 @@ library 'first-class-pipeline'
executeFreeleapsPipeline { executeFreeleapsPipeline {
serviceName = 'freeleaps' serviceName = 'freeleaps'
environmentSlug = 'alpha' environmentSlug = 'alpha'
serviceGitBranch = 'develop' serviceGitBranch = 'dev'
serviceGitRepo = "https://freeleaps@dev.azure.com/freeleaps/freeleaps2-devsvc/_git/freeleaps2-devsvc" serviceGitRepo = "https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps2-devsvc.git"
serviceGitRepoType = 'monorepo' serviceGitRepoType = 'monorepo'
serviceGitCredentialsId = 'freeleaps-azure-devops-credentials' serviceGitCredentialsId = 'freeleaps-azure-devops-credentials'
executeMode = 'fully' executeMode = 'fully'

View File

@ -3,8 +3,8 @@ library 'first-class-pipeline'
executeFreeleapsPipeline { executeFreeleapsPipeline {
serviceName = 'freeleaps' serviceName = 'freeleaps'
environmentSlug = 'alpha' environmentSlug = 'alpha'
serviceGitBranch = 'develop' serviceGitBranch = 'dev'
serviceGitRepo = "https://freeleaps@dev.azure.com/freeleaps/freeleaps2-frontend/_git/freeleaps2-frontend" serviceGitRepo = "https://gitea.freeleaps.mathmast.com/products/freeleaps.git"
serviceGitRepoType = 'monorepo' serviceGitRepoType = 'monorepo'
serviceGitCredentialsId = 'freeleaps-azure-devops-credentials' serviceGitCredentialsId = 'freeleaps-azure-devops-credentials'
executeMode = 'fully' executeMode = 'fully'