2022-02-27 20:18:27 +01:00
package main
import (
2022-06-01 20:38:04 +02:00
"crypto/ed25519"
2022-12-27 10:45:16 +01:00
"crypto/x509"
2022-02-27 20:18:27 +01:00
_ "embed"
"encoding/json"
2022-12-27 10:45:16 +01:00
"encoding/pem"
2022-02-27 20:18:27 +01:00
"io/ioutil"
"log"
"net/http"
"os"
2023-07-06 16:46:53 +02:00
"strings"
2022-02-27 20:18:27 +01:00
2022-06-01 20:38:04 +02:00
"github.com/go-ap/httpsig"
2022-02-27 20:18:27 +01:00
"github.com/woodpecker-ci/woodpecker/server/model"
2023-07-06 16:46:53 +02:00
"gopkg.in/yaml.v3"
2022-02-27 20:18:27 +01:00
)
type config struct {
Name string ` json:"name" `
Data string ` json:"data" `
}
type incoming struct {
Repo * model . Repo ` json:"repo" `
Build * model . Build ` json:"build" `
Configuration [ ] * config ` json:"configs" `
}
2023-07-06 16:46:53 +02:00
type pipeline struct {
Extend string ` yaml:"extend" `
}
2022-02-27 20:18:27 +01:00
func main ( ) {
log . Println ( "Woodpecker central config server" )
2022-12-27 10:45:16 +01:00
pubKeyPath := os . Getenv ( "CONFIG_SERVICE_PUBLIC_KEY_FILE" ) // Key in format of the one fetched from http(s)://your-woodpecker-server/api/signature/public-key
2022-02-27 20:18:27 +01:00
2023-07-06 16:46:53 +02:00
if pubKeyPath == "" {
log . Fatal ( "Please make sure CONFIG_SERVICE_PUBLIC_KEY_FILE is set properly" )
2022-02-27 20:18:27 +01:00
}
2022-06-01 20:38:04 +02:00
pubKeyRaw , err := ioutil . ReadFile ( pubKeyPath )
if err != nil {
log . Fatal ( "Failed to read public key file" )
}
2022-12-27 10:45:16 +01:00
pemblock , _ := pem . Decode ( pubKeyRaw )
b , err := x509 . ParsePKIXPublicKey ( pemblock . Bytes )
2022-06-01 20:38:04 +02:00
if err != nil {
2022-12-27 10:45:16 +01:00
log . Fatal ( "Failed to parse public key file " , err )
}
pubKey , ok := b . ( ed25519 . PublicKey )
if ! ok {
log . Fatal ( "Failed to parse public key file" )
2022-06-01 20:38:04 +02:00
}
2022-02-27 20:18:27 +01:00
http . HandleFunc ( "/ciconfig" , func ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodPost {
w . WriteHeader ( http . StatusMethodNotAllowed )
return
}
2022-06-01 20:38:04 +02:00
// check signature
pubKeyID := "woodpecker-ci-plugins"
keystore := httpsig . NewMemoryKeyStore ( )
keystore . SetKey ( pubKeyID , pubKey )
verifier := httpsig . NewVerifier ( keystore )
verifier . SetRequiredHeaders ( [ ] string { "(request-target)" , "date" } )
keyID , err := verifier . Verify ( r )
2022-02-27 20:18:27 +01:00
if err != nil {
log . Printf ( "config: invalid or missing signature in http.Request" )
http . Error ( w , "Invalid or Missing Signature" , http . StatusBadRequest )
return
}
2022-06-01 20:38:04 +02:00
if keyID != pubKeyID {
2022-02-27 20:18:27 +01:00
log . Printf ( "config: invalid signature in http.Request" )
http . Error ( w , "Invalid Signature" , http . StatusBadRequest )
return
}
var req incoming
body , err := ioutil . ReadAll ( r . Body )
if err != nil {
log . Printf ( "Error reading body: %v" , err )
http . Error ( w , "can't read body" , http . StatusBadRequest )
return
}
err = json . Unmarshal ( body , & req )
if err != nil {
http . Error ( w , "Failed to parse JSON" + err . Error ( ) , http . StatusBadRequest )
return
}
2023-07-06 16:46:53 +02:00
change := false
configOverride := [ ] config { }
for _ , conf := range req . Configuration {
originalPipeline := pipeline { }
err := yaml . Unmarshal ( [ ] byte ( conf . Data ) , & originalPipeline )
if err != nil {
http . Error ( w , "Failed to parse yaml" + err . Error ( ) , http . StatusBadRequest )
}
if originalPipeline . Extend != "" {
resp , err := http . Get ( originalPipeline . Extend )
if err != nil {
http . Error ( w , "Failed to download extend pipeline: " + err . Error ( ) , http . StatusBadRequest )
return
}
if resp . StatusCode != 200 {
http . Error ( w , "Failed to download extend pipeline: " + resp . Status , http . StatusBadRequest )
return
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
http . Error ( w , "Failed to download extend pipeline: " + err . Error ( ) , http . StatusBadRequest )
return
}
splited := strings . Split ( originalPipeline . Extend , "/" )
confName := splited [ len ( splited ) - 1 ]
configOverride = append ( configOverride , config { Name : confName , Data : string ( body ) } )
change = true
}
}
if change {
2022-02-27 20:18:27 +01:00
w . WriteHeader ( http . StatusOK )
2023-07-06 16:46:53 +02:00
err = json . NewEncoder ( w ) . Encode ( map [ string ] interface { } { "configs" : configOverride } )
2022-02-27 20:18:27 +01:00
if err != nil {
log . Printf ( "Error on encoding json %v\n" , err )
}
} else {
w . WriteHeader ( http . StatusNoContent ) // use default config
// No need to write a response body
}
} )
2023-07-06 16:46:53 +02:00
err = http . ListenAndServe ( ":8000" , nil )
2022-02-27 20:18:27 +01:00
if err != nil {
2022-02-27 22:31:40 +01:00
log . Fatalf ( "Error on listen: %v" , err )
2022-02-27 20:18:27 +01:00
}
}