Skip to content

Commit 957c565

Browse files
committed
Add webhook to rebuild website
1 parent 3ae1f70 commit 957c565

File tree

3 files changed

+183
-0
lines changed

3 files changed

+183
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ www/_site
22
termine/termine
33
c14h/c14h
44
yarpnarp/yarpnarp
5+
hook/hook

hook/hook

7.9 MB
Binary file not shown.

hook/main.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package main
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha1"
6+
"encoding/hex"
7+
"encoding/json"
8+
"errors"
9+
"flag"
10+
"fmt"
11+
"hash"
12+
"io"
13+
"log"
14+
"net/http"
15+
"os"
16+
"os/exec"
17+
"strings"
18+
"sync"
19+
"sync/atomic"
20+
)
21+
22+
var (
23+
// mtx protects new builds, so that concurrent runs of the webhook don't
24+
// interfere with each other.
25+
mtx = new(sync.Mutex)
26+
27+
// reqId contains a unique id of a request, used for logging of concurrent
28+
// requests.
29+
reqId id
30+
31+
hook = flag.String("hook", "/srv/git/website.git/post-update", "The hook to run when the website repo is pushed")
32+
listen = flag.String("listen", "localhost:12345", "The interface/port to listen on")
33+
repo = flag.String("repo", "nnev/website", "Only rebuild on push to this repository")
34+
ref = flag.String("ref", "refs/heads/master", "Only rebuild on push to this ref")
35+
36+
secret []byte
37+
)
38+
39+
type id uint64
40+
41+
func (i *id) String() string {
42+
return fmt.Sprintf("%x", uint64(*i))
43+
}
44+
45+
func (i *id) Next() id {
46+
return id(atomic.AddUint64((*uint64)(i), 1))
47+
}
48+
49+
// verifier implements the verification of the signature by github. This is
50+
// factored into it's own type, to ease security-review.
51+
type verifier struct {
52+
io.Reader
53+
io.Closer
54+
sig []byte
55+
h hash.Hash
56+
}
57+
58+
func newVerifier(req *http.Request, secret []byte) (v *verifier, err error) {
59+
v = new(verifier)
60+
if s := req.Header.Get("X-Hub-Signature"); s == "" {
61+
return nil, errors.New("no signature provided")
62+
} else {
63+
if !strings.HasPrefix(s, "sha1=") {
64+
// According to https://developer.github.com/webhooks/securing/ the
65+
// signature *always* uses sha1.
66+
return nil, errors.New("malformed signature: must start with sha1=")
67+
}
68+
if v.sig, err = hex.DecodeString(s[5:]); err != nil {
69+
return nil, fmt.Errorf("malformed signature: %v", err)
70+
}
71+
}
72+
73+
v.h = hmac.New(sha1.New, secret)
74+
v.Reader = io.TeeReader(req.Body, v.h)
75+
v.Closer = req.Body
76+
77+
return v, nil
78+
}
79+
80+
func (v *verifier) Verify() bool {
81+
return hmac.Equal(v.h.Sum(nil), v.sig)
82+
}
83+
84+
func HandleHook(r http.ResponseWriter, req *http.Request) {
85+
rid := reqId.Next()
86+
l := log.New(os.Stderr, rid.String()+": ", log.LstdFlags)
87+
88+
l.Printf("Event guid is %q", req.Header.Get("X-GitHub-Delivery"))
89+
90+
if req.Method != http.MethodPost {
91+
l.Printf("Ignoring method %q", req.Method)
92+
http.Error(r, "", http.StatusMethodNotAllowed)
93+
return
94+
}
95+
96+
if ct := req.Header.Get("Content-Type"); ct != "application/json" {
97+
l.Printf("Ignoring invalid content type %q", ct)
98+
http.Error(r, "", http.StatusUnsupportedMediaType)
99+
return
100+
}
101+
102+
l.Println("Signature is %q", req.Header.Get("X-Hub-Signature"))
103+
104+
// github signs their events, we need to verify them.
105+
v, err := newVerifier(req, secret)
106+
if err != nil {
107+
l.Print(err)
108+
http.Error(r, "", http.StatusForbidden)
109+
return
110+
}
111+
112+
// payload size is limited to 5 MB by github. We also enforce this limit,
113+
// to prevent DoS.
114+
body := http.MaxBytesReader(r, v, 5*(1<<20))
115+
116+
var ev struct {
117+
Ref string
118+
Head string
119+
Before string
120+
Size int
121+
Repository struct {
122+
FullName string `json:"full_name"`
123+
}
124+
}
125+
126+
dec := json.NewDecoder(body)
127+
if err := dec.Decode(&ev); err != nil {
128+
l.Printf("Could not unmarshal json: %v", err)
129+
http.Error(r, "invalid json: "+err.Error(), http.StatusBadRequest)
130+
return
131+
}
132+
133+
if !v.Verify() {
134+
l.Printf("Invalid signature")
135+
http.Error(r, "", http.StatusForbidden)
136+
return
137+
}
138+
139+
l.Printf("Got push event: %+v", ev)
140+
141+
if ev.Ref != *ref {
142+
l.Printf("Ignoring ref %q", ev.Ref)
143+
return
144+
}
145+
146+
if ev.Repository.FullName != *repo {
147+
l.Printf("Ignoring ref %q", ev.Repository.FullName)
148+
return
149+
}
150+
151+
if err := RunHook(); err != nil {
152+
l.Printf("Could not run hook: %v", err)
153+
http.Error(r, "internal server error", http.StatusInternalServerError)
154+
return
155+
}
156+
l.Printf("Done")
157+
}
158+
159+
func RunHook() error {
160+
mtx.Lock()
161+
defer mtx.Unlock()
162+
163+
cmd := exec.Command(*hook)
164+
cmd.Stdout = os.Stdout
165+
cmd.Stderr = os.Stderr
166+
return cmd.Run()
167+
}
168+
169+
func main() {
170+
flag.Parse()
171+
172+
if s := os.Getenv("WEBHOOK_SECRET"); s == "" {
173+
log.Fatal("WEBHOOK_SECRET is a required environment variable")
174+
} else {
175+
secret = []byte(s)
176+
}
177+
178+
http.HandleFunc("/", HandleHook)
179+
if err := http.ListenAndServe(*listen, nil); err != nil {
180+
log.Fatal(err)
181+
}
182+
}

0 commit comments

Comments
 (0)