Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
// Command webhooks provides example consumer code for Tailscale// webhooks.package main
import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strconv" "strings" "time")
type event struct { Timestamp string `json:"timestamp"` Version int `json:"version"` Type string `json:"type"` Tailnet string `json:"tailnet"` Message string `json:"message"` Data map[string]string `json:"data"`}
const ( currentVersion = "v1" secret = "tskey-webhook-xxxxx" // sensitive, here just as an example)
var ( errNotSigned = errors.New("webhook has no signature") errInvalidHeader = errors.New("webhook has an invalid signature"))
func main() { http.HandleFunc("/webhook", webhooksHandler) if err := http.ListenAndServe(":80", nil); err != nil { log.Fatal(err) }}
func webhooksHandler(w http.ResponseWriter, req *http.Request) { defer req.Body.Close() events, err := verifyWebhookSignature(req, secret) if err != nil { log.Printf("error validating signature: %v\n", err) } else { log.Printf("events received %v\n", events) // Do something with your events. :) }
// The handler should always report 2XX except in the case of // transient failures (e.g. database backend is down). // Otherwise your future events will be blocked by retries.}
// verifyWebhookSignature checks the request's "Tailscale-Webhook-Signature"// header to verify that the events were signed by your webhook secret.// If verification fails, an error is reported.// If verification succeeds, the list of contained events is reported.func verifyWebhookSignature(req *http.Request, secret string) (events []event, err error) { defer req.Body.Close()
// Grab the signature sent on the request header. timestamp, signatures, err := parseSignatureHeader(req.Header.Get("Tailscale-Webhook-Signature")) if err != nil { return nil, err }
// Verify that the timestamp is recent. // Here, we use a threshold of 5 minutes. if timestamp.Before(time.Now().Add(-time.Minute * 5)) { return nil, fmt.Errorf("invalid header: timestamp older than 5 minutes") }
// Form the expected signature. b, err := io.ReadAll(req.Body) if err != nil { return nil, err } mac := hmac.New(sha256.New, []byte(secret)) mac.Write(fmt.Append(nil, timestamp.Unix())) mac.Write([]byte(".")) mac.Write(b) want := hex.EncodeToString(mac.Sum(nil))
// Verify that the signatures match. var match bool for _, signature := range signatures[currentVersion] { if subtle.ConstantTimeCompare([]byte(signature), []byte(want)) == 1 { match = true break } } if !match { return nil, fmt.Errorf("signature does not match: want = %q, got = %q", want, signatures[currentVersion]) }
// If verified, return the events. if err := json.Unmarshal(b, &events); err != nil { return nil, err } return events, nil}
// parseSignatureHeader splits header into its timestamp and included signatures.// The signatures are reported as a map of version (e.g. "v1") to a list of signatures// found with that version.func parseSignatureHeader(header string) (timestamp time.Time, signatures map[string][]string, err error) { if header == "" { return time.Time{}, nil, fmt.Errorf("request has no signature") }
signatures = make(map[string][]string) pairs := strings.SplitSeq(header, ",") for pair := range pairs { parts := strings.Split(pair, "=") if len(parts) != 2 { return time.Time{}, nil, errNotSigned }
switch parts[0] { case "t": tsint, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { return time.Time{}, nil, errInvalidHeader } timestamp = time.Unix(tsint, 0) case currentVersion: signatures[parts[0]] = append(signatures[parts[0]], parts[1]) default: // Ignore unknown parts of the header. continue } }
if len(signatures) == 0 { return time.Time{}, nil, errNotSigned } return}