From dadf26ff90320eab8e77e67c85b7a5ae9e134956 Mon Sep 17 00:00:00 2001 From: ByteDream Date: Thu, 6 May 2021 06:05:47 +0200 Subject: [PATCH] Added github webhook --- github-webhook/README.md | 19 ++ github-webhook/main.go | 460 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 479 insertions(+) create mode 100644 github-webhook/README.md create mode 100644 github-webhook/main.go diff --git a/github-webhook/README.md b/github-webhook/README.md new file mode 100644 index 0000000..d5f1472 --- /dev/null +++ b/github-webhook/README.md @@ -0,0 +1,19 @@ +## Github Webhook + +A simple webhook server, which saves a github repository whenever changes were made. + +Inspired from [this webhook api](https://github.com/go-playground/webhooks). +The library is pretty useful, but too overloaded for my purposes. + +#### Requires + + - [gorilla/mux](https://github.com/gorilla/mux), but if you want to, you can easily cut out the gurilla/mux api and replace it with the api you prefer + +### Usage + +1. Create a repository [webhook](https://docs.github.com/en/developers/webhooks-and-events/creating-webhooks) on github. +2. Not necessary, but highly recommended: Choose a [secret](https://docs.github.com/en/developers/webhooks-and-events/securing-your-webhooks) for the webhook. +3. Set the value of `gitTree` in [main.go](main.go#L19) to the directory you want to clone the repositories in. +4. Set the value of `secret` in [main.go](main.go#L20) to the secret, you have chosen in step 2. +5. Set the callback path in the `main` method in [main.go](main.go#L406) to the path you have chosen when creating the webhook. +6. Set the port in the `main` method in [main.go](main.go#L408) to the port github is sending the webhook callback. diff --git a/github-webhook/main.go b/github-webhook/main.go new file mode 100644 index 0000000..90b900e --- /dev/null +++ b/github-webhook/main.go @@ -0,0 +1,460 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "github.com/gorilla/mux" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "time" +) + +const ( + // the directory to clone the repos in + gitTree = "" + secret = "" // please do not hardcode this, use .env files or something similar +) + +type payload interface{} + +type PushPayload struct { + payload + + Ref string `json:"ref"` + Before string `json:"before"` + After string `json:"after"` + Created bool `json:"created"` + Deleted bool `json:"deleted"` + Forced bool `json:"forced"` + BaseRef *string `json:"base_ref"` + Compare string `json:"compare"` + Commits []struct { + Sha string `json:"sha"` + ID string `json:"id"` + NodeID string `json:"node_id"` + TreeID string `json:"tree_id"` + Distinct bool `json:"distinct"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` + } `json:"author"` + Committer struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` + } `json:"committer"` + Added []string `json:"added"` + Removed []string `json:"removed"` + Modified []string `json:"modified"` + } `json:"commits"` + HeadCommit struct { + ID string `json:"id"` + NodeID string `json:"node_id"` + TreeID string `json:"tree_id"` + Distinct bool `json:"distinct"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + URL string `json:"url"` + Author struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` + } `json:"author"` + Committer struct { + Name string `json:"name"` + Email string `json:"email"` + Username string `json:"username"` + } `json:"committer"` + Added []string `json:"added"` + Removed []string `json:"removed"` + Modified []string `json:"modified"` + } `json:"head_commit"` + Repository struct { + ID int64 `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Owner struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"owner"` + Private bool `json:"private"` + HTMLURL string `json:"html_url"` + Description string `json:"description"` + Fork bool `json:"fork"` + URL string `json:"url"` + ForksURL string `json:"forks_url"` + KeysURL string `json:"keys_url"` + CollaboratorsURL string `json:"collaborators_url"` + TeamsURL string `json:"teams_url"` + HooksURL string `json:"hooks_url"` + IssueEventsURL string `json:"issue_events_url"` + EventsURL string `json:"events_url"` + AssigneesURL string `json:"assignees_url"` + BranchesURL string `json:"branches_url"` + TagsURL string `json:"tags_url"` + BlobsURL string `json:"blobs_url"` + GitTagsURL string `json:"git_tags_url"` + GitRefsURL string `json:"git_refs_url"` + TreesURL string `json:"trees_url"` + StatusesURL string `json:"statuses_url"` + LanguagesURL string `json:"languages_url"` + StargazersURL string `json:"stargazers_url"` + ContributorsURL string `json:"contributors_url"` + SubscribersURL string `json:"subscribers_url"` + SubscriptionURL string `json:"subscription_url"` + CommitsURL string `json:"commits_url"` + GitCommitsURL string `json:"git_commits_url"` + CommentsURL string `json:"comments_url"` + IssueCommentURL string `json:"issue_comment_url"` + ContentsURL string `json:"contents_url"` + CompareURL string `json:"compare_url"` + MergesURL string `json:"merges_url"` + ArchiveURL string `json:"archive_url"` + DownloadsURL string `json:"downloads_url"` + IssuesURL string `json:"issues_url"` + PullsURL string `json:"pulls_url"` + MilestonesURL string `json:"milestones_url"` + NotificationsURL string `json:"notifications_url"` + LabelsURL string `json:"labels_url"` + ReleasesURL string `json:"releases_url"` + CreatedAt int64 `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt int64 `json:"pushed_at"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SvnURL string `json:"svn_url"` + Homepage *string `json:"homepage"` + Size int64 `json:"size"` + StargazersCount int64 `json:"stargazers_count"` + WatchersCount int64 `json:"watchers_count"` + Language *string `json:"language"` + HasIssues bool `json:"has_issues"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + ForksCount int64 `json:"forks_count"` + MirrorURL *string `json:"mirror_url"` + OpenIssuesCount int64 `json:"open_issues_count"` + Forks int64 `json:"forks"` + OpenIssues int64 `json:"open_issues"` + Watchers int64 `json:"watchers"` + DefaultBranch string `json:"default_branch"` + Stargazers int64 `json:"stargazers"` + MasterBranch string `json:"master_branch"` + } `json:"repository"` + Pusher struct { + Name string `json:"name"` + Email string `json:"email"` + } `json:"pusher"` + Sender struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"sender"` + Installation struct { + ID int `json:"id"` + } `json:"installation"` +} + +type RepositoryPayload struct { + Action string `json:"action"` + Repository struct { + ID int64 `json:"id"` + NodeID string `json:"node_id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Owner struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"owner"` + Private bool `json:"private"` + HTMLURL string `json:"html_url"` + Description string `json:"description"` + Fork bool `json:"fork"` + URL string `json:"url"` + ForksURL string `json:"forks_url"` + KeysURL string `json:"keys_url"` + CollaboratorsURL string `json:"collaborators_url"` + TeamsURL string `json:"teams_url"` + HooksURL string `json:"hooks_url"` + IssueEventsURL string `json:"issue_events_url"` + EventsURL string `json:"events_url"` + AssigneesURL string `json:"assignees_url"` + BranchesURL string `json:"branches_url"` + TagsURL string `json:"tags_url"` + BlobsURL string `json:"blobs_url"` + GitTagsURL string `json:"git_tags_url"` + GitRefsURL string `json:"git_refs_url"` + TreesURL string `json:"trees_url"` + StatusesURL string `json:"statuses_url"` + LanguagesURL string `json:"languages_url"` + StargazersURL string `json:"stargazers_url"` + ContributorsURL string `json:"contributors_url"` + SubscribersURL string `json:"subscribers_url"` + SubscriptionURL string `json:"subscription_url"` + CommitsURL string `json:"commits_url"` + GitCommitsURL string `json:"git_commits_url"` + CommentsURL string `json:"comments_url"` + IssueCommentURL string `json:"issue_comment_url"` + ContentsURL string `json:"contents_url"` + CompareURL string `json:"compare_url"` + MergesURL string `json:"merges_url"` + ArchiveURL string `json:"archive_url"` + DownloadsURL string `json:"downloads_url"` + IssuesURL string `json:"issues_url"` + PullsURL string `json:"pulls_url"` + MilestonesURL string `json:"milestones_url"` + NotificationsURL string `json:"notifications_url"` + LabelsURL string `json:"labels_url"` + ReleasesURL string `json:"releases_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PushedAt time.Time `json:"pushed_at"` + GitURL string `json:"git_url"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + SvnURL string `json:"svn_url"` + Homepage *string `json:"homepage"` + Size int64 `json:"size"` + StargazersCount int64 `json:"stargazers_count"` + WatchersCount int64 `json:"watchers_count"` + Language *string `json:"language"` + HasIssues bool `json:"has_issues"` + HasDownloads bool `json:"has_downloads"` + HasWiki bool `json:"has_wiki"` + HasPages bool `json:"has_pages"` + ForksCount int64 `json:"forks_count"` + MirrorURL *string `json:"mirror_url"` + OpenIssuesCount int64 `json:"open_issues_count"` + Forks int64 `json:"forks"` + OpenIssues int64 `json:"open_issues"` + Watchers int64 `json:"watchers"` + DefaultBranch string `json:"default_branch"` + } `json:"repository"` + Organization struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + URL string `json:"url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + MembersURL string `json:"members_url"` + PublicMembersURL string `json:"public_members_url"` + AvatarURL string `json:"avatar_url"` + } `json:"organization"` + Sender struct { + Login string `json:"login"` + ID int64 `json:"id"` + NodeID string `json:"node_id"` + AvatarURL string `json:"avatar_url"` + GravatarID string `json:"gravatar_id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + FollowersURL string `json:"followers_url"` + FollowingURL string `json:"following_url"` + GistsURL string `json:"gists_url"` + StarredURL string `json:"starred_url"` + SubscriptionsURL string `json:"subscriptions_url"` + OrganizationsURL string `json:"organizations_url"` + ReposURL string `json:"repos_url"` + EventsURL string `json:"events_url"` + ReceivedEventsURL string `json:"received_events_url"` + Type string `json:"type"` + SiteAdmin bool `json:"site_admin"` + } `json:"sender"` +} + +type OnPayload func(payload payload, gw *GithubWebhook) error + +type GithubWebhook struct { + pushEvents map[int64][]OnPayload + repositoryEvents map[int64][]OnPayload + + repoNames map[int64]string +} + +func (gw *GithubWebhook) AddPushEvent(repoID int64, function OnPayload) { + gw.pushEvents[repoID] = append(gw.pushEvents[repoID], function) +} + +func (gw *GithubWebhook) AddRepositoryEvent(repoID int64, function OnPayload) { + gw.repositoryEvents[repoID] = append(gw.repositoryEvents[repoID], function) +} + +func (gw *GithubWebhook) OnWebhook(w http.ResponseWriter, r *http.Request) { + event := r.Header.Get("X-GitHub-Event") + if event == "" { + return + } + + payload, err := ioutil.ReadAll(r.Body) + if err != nil || len(payload) == 0 { + return + } + + // when a secret is given, this checks if the webhook and the local secret are even + if secret != "" { + signature := r.Header.Get("X-Hub-Signature") + if len(signature) == 0 { + return + } + mac := hmac.New(sha1.New, []byte(secret)) + _, _ = mac.Write(payload) + expectedMAC := hex.EncodeToString(mac.Sum(nil)) + + if !hmac.Equal([]byte(signature[5:]), []byte(expectedMAC)) { + return + } + } + + switch event { + case "push": + var pushPayload PushPayload + if err = json.Unmarshal(payload, &pushPayload); err != nil { + return + } + if events, ok := gw.pushEvents[pushPayload.Repository.ID]; ok { + for _, event := range events { + if err := event(pushPayload, gw); err != nil { + panic(err) + } + } + } + case "repository": + var repositoryPayload RepositoryPayload + if err = json.Unmarshal(payload, &repositoryPayload); err != nil { + return + } + if events, ok := gw.repositoryEvents[repositoryPayload.Repository.ID]; ok { + for _, event := range events { + if err := event(repositoryPayload, gw); err != nil { + panic(err) + } + } + } + default: + return + } +} + +func main() { + r := mux.NewRouter() + gw := &GithubWebhook{pushEvents: make(map[int64][]OnPayload), repositoryEvents: make(map[int64][]OnPayload), repoNames: make(map[int64]string)} + gw.AddPushEvent(364735060, updateLocal) + gw.AddRepositoryEvent(364735060, renameLocal) + + // NOTE: To find out the id of a repository, go to the repo page, open the browser / developer console and execute `document.head.querySelector("meta[name=octolytics-dimension-repository_id]").content` + + r.HandleFunc("/github_webhook", gw.OnWebhook).Methods("POST") + + http.ListenAndServe(":8080", r) +} + +func checkIfLocalPresent(gw *GithubWebhook, id int64, name string, cloneUrl string) error { + if _, ok := gw.repoNames[id]; !ok { + files, err := ioutil.ReadDir(gitTree) + if err != nil { + return err + } + + for _, file := range files { + if file.IsDir() && file.Name() == name { + gw.repoNames[id] = name + return nil + } + } + + cmd := exec.Command("git", + "clone", cloneUrl, + filepath.Join(gitTree, name)) + if err = cmd.Run(); err != nil { + return err + } + gw.repoNames[id] = name + } + return nil +} + +func renameLocal(payload2 payload, gw *GithubWebhook) error { + repoPayload := payload2.(RepositoryPayload) + if repoPayload.Action == "rename" { + if err := checkIfLocalPresent(gw, repoPayload.Repository.ID, repoPayload.Repository.Name, repoPayload.Repository.CloneURL); err != nil { + return err + } + + os.Rename(filepath.Join(gitTree, gw.repoNames[repoPayload.Repository.ID]), filepath.Join(gitTree, repoPayload.Repository.Name)) + gw.repoNames[repoPayload.Repository.ID] = repoPayload.Repository.Name + } + return nil +} + +func updateLocal(payload2 payload, gw *GithubWebhook) error { + pushPayload := payload2.(PushPayload) + + if err := checkIfLocalPresent(gw, pushPayload.Repository.ID, pushPayload.Repository.Name, pushPayload.Repository.CloneURL); err != nil { + return err + } + + return exec.Command("git", + "-C", filepath.Join(gitTree, pushPayload.Repository.Name), + "pull", + "origin").Run() +}