Alright, I now have this running on Google App Engine:
package lubar_webhook_proxy
import (
"bytes"
"errors"
"io/ioutil"
"net/http"
"strings"
"time"
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/delay"
"google.golang.org/appengine/log"
"google.golang.org/appengine/taskqueue"
"google.golang.org/appengine/urlfetch"
)
type GitHubRequest struct {
UserAgent string
Signature string
Event string
Delivery string
Payload []byte
}
var laterGitHubRequest = delay.Func("GitHub-BuildMaster", func(ctx context.Context, req GitHubRequest) error {
r, err := http.NewRequest("POST", "https://buildmaster.local.lubar.me/hooks/github", bytes.NewReader(req.Payload))
if err != nil {
return err
}
r.Header.Set("User-Agent", req.UserAgent)
r.Header.Set("X-Hub-Signature", req.Signature)
r.Header.Set("X-GitHub-Event", req.Event)
r.Header.Set("X-GitHub-Delivery", req.Delivery)
r.Header.Set("Content-Type", "application/json; charset=utf-8")
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
client := urlfetch.Client(ctxTimeout)
start := time.Now()
resp, err := client.Do(r)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
log.Debugf(ctx, "[GitHub webhook] queued build: %v\n\nagent: %q\ndelivery: %q\nevent: %q\nsignature: %q\npayload: %q", time.Since(start), req.UserAgent, req.Delivery, req.Event, req.Signature, req.Payload)
return nil // build was queued
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
log.Errorf(ctx, "[GitHub webhook] %q: %q\n\nagent: %q\ndelivery: %q\nevent: %q\nsignature: %q\npayload: %q", resp.Status, body, req.UserAgent, req.Delivery, req.Event, req.Signature, req.Payload)
if resp.StatusCode == http.StatusBadRequest {
return nil // payload or signature was invalid
}
return errors.New(resp.Status)
})
func GitHubBuildMaster(w http.ResponseWriter, r *http.Request) {
userAgent := r.UserAgent()
signature := r.Header.Get("X-Hub-Signature")
event := r.Header.Get("X-GitHub-Event")
delivery := r.Header.Get("X-GitHub-Delivery")
if r.Method != "POST" || !strings.HasPrefix(userAgent, "GitHub-Hookshot/") || signature == "" || event == "" || delivery == "" {
http.Error(w, "This is the GitHub webhook endpoint.", http.StatusForbidden)
return
}
defer r.Body.Close()
payload, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// GitHub documents payloads as being a maximum of five megabytes.
// Drop payloads that are more than ten.
if len(payload) > 10*1024*1024 {
http.Error(w, "payload too big", http.StatusRequestEntityTooLarge)
return
}
task, err := laterGitHubRequest.Task(GitHubRequest{
UserAgent: userAgent,
Signature: signature,
Event: event,
Delivery: delivery,
Payload: payload,
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
task.RetryOptions = &taskqueue.RetryOptions{
AgeLimit: time.Hour * 24 * 7,
}
taskqueue.Add(appengine.NewContext(r), task, "")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
}
func init() {
http.HandleFunc("/github/buildmaster", GitHubBuildMaster)
}
It should stay well below the free quota limit.