2024-03-20 15:44:01 +01:00
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package webhook
import (
"context"
"fmt"
2024-03-22 16:02:48 +01:00
"html/template"
2024-03-20 15:44:01 +01:00
"net/http"
"net/url"
"strings"
webhook_model "code.gitea.io/gitea/models/webhook"
2024-04-30 10:18:02 +02:00
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
2024-03-20 15:44:01 +01:00
"code.gitea.io/gitea/modules/log"
2024-03-22 16:02:48 +01:00
"code.gitea.io/gitea/modules/svg"
2024-03-20 15:44:01 +01:00
webhook_module "code.gitea.io/gitea/modules/webhook"
2024-03-21 13:42:40 +01:00
"code.gitea.io/gitea/services/forms"
2024-04-03 14:22:36 +02:00
"code.gitea.io/gitea/services/webhook/shared"
2024-03-20 15:44:01 +01:00
)
var _ Handler = defaultHandler { }
type defaultHandler struct {
forgejo bool
}
func ( dh defaultHandler ) Type ( ) webhook_module . HookType {
if dh . forgejo {
return webhook_module . FORGEJO
}
return webhook_module . GITEA
}
2024-03-22 16:02:48 +01:00
func ( dh defaultHandler ) Icon ( size int ) template . HTML {
if dh . forgejo {
// forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work
2024-04-03 14:22:36 +02:00
return shared . ImgIcon ( "forgejo.svg" , size )
2024-03-22 16:02:48 +01:00
}
return svg . RenderHTML ( "gitea-gitea" , size , "img" )
}
2024-03-20 15:44:01 +01:00
func ( defaultHandler ) Metadata ( * webhook_model . Webhook ) any { return nil }
2024-04-03 14:22:36 +02:00
func ( defaultHandler ) UnmarshalForm ( bind func ( any ) ) forms . WebhookForm {
2024-03-21 13:42:40 +01:00
var form struct {
2024-04-03 14:22:36 +02:00
forms . WebhookCoreForm
2024-03-21 13:42:40 +01:00
PayloadURL string ` binding:"Required;ValidUrl" `
HTTPMethod string ` binding:"Required;In(POST,GET)" `
ContentType int ` binding:"Required" `
Secret string
}
bind ( & form )
contentType := webhook_model . ContentTypeJSON
if webhook_model . HookContentType ( form . ContentType ) == webhook_model . ContentTypeForm {
contentType = webhook_model . ContentTypeForm
}
2024-04-03 14:22:36 +02:00
return forms . WebhookForm {
WebhookCoreForm : form . WebhookCoreForm ,
URL : form . PayloadURL ,
ContentType : contentType ,
Secret : form . Secret ,
HTTPMethod : form . HTTPMethod ,
Metadata : nil ,
2024-03-21 13:42:40 +01:00
}
2024-03-21 13:23:27 +01:00
}
2024-03-20 15:44:01 +01:00
func ( defaultHandler ) NewRequest ( ctx context . Context , w * webhook_model . Webhook , t * webhook_model . HookTask ) ( req * http . Request , body [ ] byte , err error ) {
2024-04-30 10:18:02 +02:00
payloadContent := t . PayloadContent
if w . Type == webhook_module . GITEA &&
( t . EventType == webhook_module . HookEventCreate || t . EventType == webhook_module . HookEventDelete ) {
// Woodpecker expects the ref to be short on tag creation only
// https://github.com/woodpecker-ci/woodpecker/blob/00ccec078cdced80cf309cd4da460a5041d7991a/server/forge/gitea/helper.go#L134
// see https://codeberg.org/codeberg/community/issues/1556
payloadContent , err = substituteRefShortName ( payloadContent )
if err != nil {
2024-05-25 09:43:50 +02:00
return nil , nil , fmt . Errorf ( "could not substitute ref: %w" , err )
2024-04-30 10:18:02 +02:00
}
}
2024-03-20 15:44:01 +01:00
switch w . HTTPMethod {
case "" :
log . Info ( "HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST" , w . Type , w . URL , w . ID )
fallthrough
case http . MethodPost :
switch w . ContentType {
case webhook_model . ContentTypeJSON :
2024-04-30 10:18:02 +02:00
req , err = http . NewRequest ( "POST" , w . URL , strings . NewReader ( payloadContent ) )
2024-03-20 15:44:01 +01:00
if err != nil {
return nil , nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
case webhook_model . ContentTypeForm :
forms := url . Values {
2024-04-30 10:18:02 +02:00
"payload" : [ ] string { payloadContent } ,
2024-03-20 15:44:01 +01:00
}
req , err = http . NewRequest ( "POST" , w . URL , strings . NewReader ( forms . Encode ( ) ) )
if err != nil {
return nil , nil , err
}
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
default :
return nil , nil , fmt . Errorf ( "invalid content type: %v" , w . ContentType )
}
case http . MethodGet :
u , err := url . Parse ( w . URL )
if err != nil {
return nil , nil , fmt . Errorf ( "invalid URL: %w" , err )
}
vals := u . Query ( )
2024-04-30 10:18:02 +02:00
vals [ "payload" ] = [ ] string { payloadContent }
2024-03-20 15:44:01 +01:00
u . RawQuery = vals . Encode ( )
req , err = http . NewRequest ( "GET" , u . String ( ) , nil )
if err != nil {
return nil , nil , err
}
case http . MethodPut :
switch w . Type {
case webhook_module . MATRIX : // used when t.Version == 1
2024-04-30 10:18:02 +02:00
txnID , err := getMatrixTxnID ( [ ] byte ( payloadContent ) )
2024-03-20 15:44:01 +01:00
if err != nil {
return nil , nil , err
}
url := fmt . Sprintf ( "%s/%s" , w . URL , url . PathEscape ( txnID ) )
2024-04-30 10:18:02 +02:00
req , err = http . NewRequest ( "PUT" , url , strings . NewReader ( payloadContent ) )
2024-03-20 15:44:01 +01:00
if err != nil {
return nil , nil , err
}
default :
return nil , nil , fmt . Errorf ( "invalid http method: %v" , w . HTTPMethod )
}
default :
return nil , nil , fmt . Errorf ( "invalid http method: %v" , w . HTTPMethod )
}
2024-04-30 10:18:02 +02:00
body = [ ] byte ( payloadContent )
2024-04-03 14:22:36 +02:00
return req , body , shared . AddDefaultHeaders ( req , [ ] byte ( w . Secret ) , t , body )
2024-03-20 15:44:01 +01:00
}
2024-04-30 10:18:02 +02:00
func substituteRefShortName ( body string ) ( string , error ) {
var m map [ string ] any
if err := json . Unmarshal ( [ ] byte ( body ) , & m ) ; err != nil {
return body , err
}
ref , ok := m [ "ref" ] . ( string )
if ! ok {
return body , fmt . Errorf ( "expected string 'ref', got %T" , m [ "ref" ] )
}
m [ "ref" ] = git . RefName ( ref ) . ShortName ( )
buf , err := json . Marshal ( m )
return string ( buf ) , err
}