package main import ( "crypto/ed25519" "crypto/x509" _ "embed" "encoding/json" "encoding/pem" "io/ioutil" "log" "net/http" "os" "strings" "github.com/go-ap/httpsig" "github.com/woodpecker-ci/woodpecker/server/model" "gopkg.in/yaml.v3" ) 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"` } type pipeline struct { Extend string `yaml:"extend"` } func main() { log.Println("Woodpecker central config server") 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 if pubKeyPath == "" { log.Fatal("Please make sure CONFIG_SERVICE_PUBLIC_KEY_FILE is set properly") } pubKeyRaw, err := ioutil.ReadFile(pubKeyPath) if err != nil { log.Fatal("Failed to read public key file") } pemblock, _ := pem.Decode(pubKeyRaw) b, err := x509.ParsePKIXPublicKey(pemblock.Bytes) if err != nil { log.Fatal("Failed to parse public key file ", err) } pubKey, ok := b.(ed25519.PublicKey) if !ok { log.Fatal("Failed to parse public key file") } http.HandleFunc("/ciconfig", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } // 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) if err != nil { log.Printf("config: invalid or missing signature in http.Request") http.Error(w, "Invalid or Missing Signature", http.StatusBadRequest) return } if keyID != pubKeyID { 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 } 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 { w.WriteHeader(http.StatusOK) err = json.NewEncoder(w).Encode(map[string]interface{}{"configs": configOverride}) 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 } }) err = http.ListenAndServe(":8000", nil) if err != nil { log.Fatalf("Error on listen: %v", err) } }