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  }