mirror of
https://github.com/therootcompany/golib.git
synced 2026-04-24 12:48:00 +00:00
Verify X-Hub-Signature-256 (and SHA-1) webhook signatures. Middleware buffers and re-exposes the body for downstream handlers. Errors honor Accept header: TSV default (text/plain for browsers), JSON, CSV, or Markdown — three fields (error, description, hint) with pseudocode hints.
81 lines
2.1 KiB
Go
81 lines
2.1 KiB
Go
package xhubsig
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type httpError struct {
|
|
Error string `json:"error"`
|
|
Description string `json:"description,omitempty"`
|
|
Hint string `json:"hint,omitempty"`
|
|
}
|
|
|
|
func (e httpError) rows() [][2]string {
|
|
rows := [][2]string{{"error", e.Error}}
|
|
if e.Description != "" {
|
|
rows = append(rows, [2]string{"description", e.Description})
|
|
}
|
|
if e.Hint != "" {
|
|
rows = append(rows, [2]string{"hint", e.Hint})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func acceptedFormat(accept string) string {
|
|
for part := range strings.SplitSeq(accept, ",") {
|
|
mt := strings.TrimSpace(strings.SplitN(part, ";", 2)[0])
|
|
switch mt {
|
|
case "application/json":
|
|
return "json"
|
|
case "text/csv":
|
|
return "csv"
|
|
case "text/markdown":
|
|
return "markdown"
|
|
case "text/html":
|
|
return "text/plain"
|
|
}
|
|
}
|
|
return "tsv"
|
|
}
|
|
|
|
// writeDelimited writes vertical key-value TSV or CSV. Newlines within
|
|
// values are collapsed to a space so agents can split on newlines reliably.
|
|
func writeDelimited(w http.ResponseWriter, httpCode int, ct string, sep rune, e httpError) {
|
|
w.Header().Set("Content-Type", ct)
|
|
w.WriteHeader(httpCode)
|
|
cw := csv.NewWriter(w)
|
|
cw.Comma = sep
|
|
cw.Write([]string{"field", "value"})
|
|
for _, row := range e.rows() {
|
|
cw.Write([]string{row[0], strings.ReplaceAll(row[1], "\n", " ")})
|
|
}
|
|
cw.Flush()
|
|
}
|
|
|
|
func serializeError(w http.ResponseWriter, r *http.Request, httpCode int, e httpError) {
|
|
switch acceptedFormat(r.Header.Get("Accept")) {
|
|
case "tsv":
|
|
writeDelimited(w, httpCode, "text/tab-separated-values", '\t', e)
|
|
case "text/plain":
|
|
writeDelimited(w, httpCode, "text/plain", '\t', e)
|
|
case "csv":
|
|
writeDelimited(w, httpCode, "text/csv", ',', e)
|
|
case "markdown":
|
|
w.Header().Set("Content-Type", "text/markdown")
|
|
w.WriteHeader(httpCode)
|
|
fmt.Fprintln(w, "| field | value |")
|
|
fmt.Fprintln(w, "| --- | --- |")
|
|
for _, row := range e.rows() {
|
|
fmt.Fprintf(w, "| %s | %s |\n", row[0], strings.ReplaceAll(row[1], "\n", " "))
|
|
}
|
|
default:
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(httpCode)
|
|
json.NewEncoder(w).Encode(e)
|
|
}
|
|
}
|