golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cloudfns/wikiwebhook/wikiwebhook.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package wikiwebhook implements an Google Cloud Function HTTP handler that 6 // expects GitHub webhook change events. Specifically, it reacts to wiki change 7 // events and posts the payload to a pubsub topic. 8 package wikiwebhook 9 10 import ( 11 "context" 12 "crypto/hmac" 13 "crypto/sha1" 14 "encoding/hex" 15 "fmt" 16 "io" 17 "net/http" 18 "os" 19 20 "cloud.google.com/go/pubsub" 21 ) 22 23 var ( 24 githubSecret = os.Getenv("GITHUB_WEBHOOK_SECRET") 25 projectID = os.Getenv("GCP_PROJECT") 26 pubsubTopic = os.Getenv("PUBSUB_TOPIC") 27 ) 28 29 func GitHubWikiChangeWebHook(w http.ResponseWriter, r *http.Request) { 30 body, err := io.ReadAll(r.Body) 31 if err != nil { 32 fmt.Fprintf(os.Stderr, "Could not read request body: %v", err) 33 http.Error(w, err.Error(), http.StatusInternalServerError) 34 return 35 } 36 37 if !validSignature(body, []byte(githubSecret), r.Header.Get("X-Hub-Signature")) { 38 http.Error(w, "signature mismatch", http.StatusUnauthorized) 39 return 40 } 41 42 evt := r.Header.Get("X-GitHub-Event") 43 // Ping event is sent upon initial setup of the webhook. 44 if evt == "ping" { 45 fmt.Fprintf(w, "pong") 46 return 47 } 48 // See https://developer.github.com/v3/activity/events/types/#gollumevent. 49 if evt != "gollum" { 50 http.Error(w, fmt.Sprintf("incorrect event type %q", evt), http.StatusBadRequest) 51 return 52 } 53 54 id, err := publishToTopic(pubsubTopic, body) 55 if err != nil { 56 fmt.Fprintf(os.Stderr, "Unable to publish to topic: %v", err) 57 http.Error(w, err.Error(), http.StatusInternalServerError) 58 return 59 } 60 fmt.Fprintf(w, "Message ID: %s\n", id) 61 } 62 63 // publishToTopic publishes body to the given topic. 64 // It returns the ID of the message published. 65 var publishToTopic = func(topic string, body []byte) (string, error) { 66 // TODO(dmitshur): Can factor out pubsub.NewClient to run once at init time as suggested at 67 // https://cloud.google.com/functions/docs/concepts/go-runtime#one-time_initialization, and 68 // determine projectID via metadata.ProjectID instead of needing the GCP_PROJECT env var. 69 ctx := context.Background() 70 if projectID == "" { 71 return "", fmt.Errorf("projectID is an empty string") 72 } 73 client, err := pubsub.NewClient(ctx, projectID) 74 if err != nil { 75 return "", fmt.Errorf("pubsub.NewClient: %v", err) 76 } 77 78 t := client.Topic(topic) 79 resp := t.Publish(ctx, &pubsub.Message{Data: body}) 80 id, err := resp.Get(ctx) 81 if err != nil { 82 return "", fmt.Errorf("topic.Publish: %v", err) 83 } 84 return id, nil 85 } 86 87 // validSignature reports whether the HMAC-SHA1 of body with key matches sig, 88 // which is in the form "sha1=<HMAC-SHA1 in hex>". 89 func validSignature(body, key []byte, sig string) bool { 90 const prefix = "sha1=" 91 if len(sig) < len(prefix) { 92 return false 93 } 94 sig = sig[len(prefix):] 95 mac := hmac.New(sha1.New, key) 96 mac.Write(body) 97 b, err := hex.DecodeString(sig) 98 if err != nil { 99 return false 100 } 101 102 // Use hmac.Equal to avoid timing attacks. 103 return hmac.Equal(mac.Sum(nil), b) 104 }