Created
November 15, 2024 22:52
-
-
Save juicemia/e28c382bf367f3d9445d07afcf94aaaa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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