Skip to content

Instantly share code, notes, and snippets.

@juicemia
Created November 15, 2024 22:52
Show Gist options
  • Save juicemia/e28c382bf367f3d9445d07afcf94aaaa to your computer and use it in GitHub Desktop.
Save juicemia/e28c382bf367f3d9445d07afcf94aaaa to your computer and use it in GitHub Desktop.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
)
type webhookPayload struct {
DeploymentCallbackURL string `json:"deployment_callback_url"`
Deployment struct {
Environment string `json:"environment"`
} `json:"deployment"`
Installation struct {
ID int `json:"id"`
} `json:"installation"`
Repository struct {
ID int `json:"id"`
} `json:"repository"`
}
type installTokenPayload struct {
RepositoryIDs []int `json:"repository_ids"`
Permissions map[string]string `json:"permissions"`
}
type installTokenResponsePayload struct {
Token string `json:"token"`
}
type decisionRequestPayload struct {
State string `json:"state"`
EnvironmentName string `json:"environment_name"`
Comment string `json:"comment"`
}
func main() {
fmt.Println("booting server...")
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("%v - %v\n", r.Method, r.URL.Path)
signatureHeader := r.Header.Get("X-Hub-Signature-256")
if signatureHeader == "" {
fmt.Println("no signature header")
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("signature header: %v\n", signatureHeader)
buf, err := io.ReadAll(r.Body)
if err != nil {
fmt.Printf("err: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// TODO: verify header with HMAC(buf, secretToken)
// https://pkg.go.dev/crypto/subtle recommended by github for comparing the hashes
var payload webhookPayload
if err := json.Unmarshal(buf, &payload); err != nil {
fmt.Printf("err: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
token, err := getJWT()
if err != nil {
fmt.Printf("error getting jwt: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
u := fmt.Sprintf("https://api.github.com/app/installations/%v/access_tokens", payload.Installation.ID)
installTokenPayload := installTokenPayload{
RepositoryIDs: []int{payload.Repository.ID},
Permissions: map[string]string{
"deployments": "write",
},
}
installTokenPayloadReader := bytes.Buffer{}
if err := json.NewEncoder(&installTokenPayloadReader).Encode(installTokenPayload); err != nil {
fmt.Printf("error encoding install token payload: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
installTokenRequest, err := http.NewRequest(http.MethodPost, u, &installTokenPayloadReader)
if err != nil {
fmt.Printf("error creating install token request: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
installTokenRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token))
installTokenRequest.Header.Add("Accept", "application/vnd.github+json")
installTokenRequest.Header.Add("Content-Type", "application/json")
installTokenResponse, err := http.DefaultClient.Do(installTokenRequest)
if err != nil {
fmt.Printf("error making install token http request: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer installTokenResponse.Body.Close()
var installTokenResponsePayload installTokenResponsePayload
{
buf, err := io.ReadAll(installTokenResponse.Body)
if err != nil {
fmt.Printf("err: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
if err := json.Unmarshal(buf, &installTokenResponsePayload); err != nil {
fmt.Printf("error unmarshaling install token response payload: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
decisionRequestPayload := decisionRequestPayload{
State: "approved",
EnvironmentName: payload.Deployment.Environment,
Comment: "have fun",
}
{
payloadReader := bytes.Buffer{}
if err := json.NewEncoder(&payloadReader).Encode(decisionRequestPayload); err != nil {
fmt.Printf("error encoding decision request payload: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
req, err := http.NewRequest(http.MethodPost, payload.DeploymentCallbackURL, &payloadReader)
if err != nil {
fmt.Printf("error creating decision http request: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", installTokenResponsePayload.Token))
req.Header.Add("Accept", "application/vnd.github+json")
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("error making decision http request: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
fmt.Printf("got status code %v instead of %v\n", resp.StatusCode, http.StatusNoContent)
buf, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("unable to read response body: %v\n", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
fmt.Printf("non-%v response body: %s\n", http.StatusNoContent, buf)
}
}
})
if err := http.ListenAndServe(":8888", nil); err != nil {
panic(err)
}
}
var privateKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
stuff
-----END RSA PRIVATE KEY-----
`
func getJWT() (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iat": time.Now().Add(-1 * time.Minute).Unix(),
"exp": time.Now().Add(5 * time.Minute).Unix(),
"iss": "client_id", // ClientID of app found in app settings like private key
})
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKeyPEM))
if err != nil {
return "", err
}
return token.SignedString(privateKey)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment