github.com/agrison/harpoon@v0.0.0-20180819075247-a667a15fd0eb/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "crypto/hmac" 6 "crypto/sha1" 7 "encoding/hex" 8 "encoding/json" 9 "flag" 10 "fmt" 11 "io/ioutil" 12 "net/http" 13 "os" 14 "os/exec" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/NoahShen/gotunnelme/src/gotunnelme" // for tunneling 20 "github.com/fatih/color" 21 "github.com/gorilla/mux" 22 ) 23 24 const ( 25 headerEvent = "X-GitHub-Event" // HTTP header where the webhook event is stored 26 headerSignature = "X-Hub-Signature" // HTTP header where the sha1 signature of the payload is stored 27 ) 28 29 var ( 30 config = tomlConfig{} // the program config 31 verbose = false // weither we should log the output of the command 32 verboseTunnel = false // weither we should log the output of the tunneling 33 configFile = "" 34 gitHubSecretToken = os.Getenv("GITHUB_HOOK_SECRET_TOKEN") // the webhook secret token, used to verify signature 35 ) 36 37 // HookHandler receive hooks from GitHub. 38 func HookHandler(w http.ResponseWriter, r *http.Request) { 39 r.ParseForm() 40 41 // read the HTTP request body 42 payload, err := ioutil.ReadAll(r.Body) 43 if err != nil { 44 fmt.Fprintln(os.Stderr, color.RedString("Error: "+err.Error())) 45 BadRequestHandler(w, r) 46 return 47 } 48 49 // validate signature 50 if gitHubSecretToken != "" { 51 sign := r.Header.Get(headerSignature) 52 53 // to compute the HMAC in order to check for equality with what has been sent by GitHub 54 mac := hmac.New(sha1.New, []byte(gitHubSecretToken)) 55 mac.Write(payload) 56 expectedHash := hex.EncodeToString(mac.Sum(nil)) 57 receivedHash := sign[5:] // remove 'sha1=' 58 59 // signature mismatch, do not process 60 if !hmac.Equal([]byte(receivedHash), []byte(expectedHash)) { 61 color.Set(color.FgRed) 62 fmt.Fprintf(os.Stderr, "Mismatch between expected (%s) and received (%s) hash.", expectedHash, receivedHash) 63 color.Set(color.Faint) 64 BadRequestHandler(w, r) 65 return 66 } 67 } 68 69 var eventPayload HookWithRepository 70 json.Unmarshal(payload, &eventPayload) 71 72 // verify that this is an event that we should process 73 // event := r.Header.Get(headerEvent) 74 event := eventPayload.EventName 75 if event == "ping" { 76 return // always respond 200 to pings 77 } 78 79 // check whether we're interested in that event 80 if shouldHandleEvent(config.Events, event, eventPayload) { 81 handleEvent(event, eventPayload, []byte(payload)) 82 } else { 83 if verbose { 84 color.Set(color.FgRed) 85 fmt.Fprintf(os.Stderr, "Discarding %s on %s with ref %s.\n", 86 color.CyanString(event), color.YellowString(eventPayload.Project.PathWithNamespace), color.YellowString(eventPayload.Ref)) 87 color.Set(color.Faint) 88 BadRequestHandler(w, r) 89 return // 400 Bad Request 90 } 91 } 92 93 } 94 95 func shouldHandleEvent(events map[string]event, event string, eventPayload HookWithRepository) bool { 96 if _, ok := events[event+":"+eventPayload.Project.PathWithNamespace+":"+eventPayload.Ref]; ok { 97 return true 98 } else if _, ok := events[event+":"+eventPayload.Project.PathWithNamespace+":all"]; ok { 99 return true 100 } else if _, ok := events[event+":all:all"]; ok { 101 return true 102 } 103 return false 104 } 105 106 // handleEvent handles any event. 107 func handleEvent(event string, hook HookWithRepository, payload []byte) { 108 // show related commits if push event 109 if event == "push" { 110 var pushEvent HookPush 111 json.Unmarshal(payload, &pushEvent) 112 fmt.Println(event, "detected on", color.YellowString(hook.Project.PathWithNamespace), 113 "with ref", color.YellowString(hook.Ref), "with the following commits:") 114 for _, commit := range pushEvent.Commits { 115 fmt.Printf("\t%s - %s by %s\n", commit.Timestamp, color.CyanString(commit.Message), color.BlueString(commit.Author.Name)) 116 } 117 } 118 119 // prepare the command 120 eventKey := event + ":" + hook.Project.PathWithNamespace + ":" + hook.Ref 121 if _, ok := config.Events[eventKey]; !ok { 122 eventKey = event + ":" + hook.Project.PathWithNamespace + ":all" 123 } 124 if _, ok := config.Events[eventKey]; !ok { 125 eventKey = event + ":all:all" 126 } 127 128 cmd := exec.Command(config.Events[eventKey].Cmd, 129 strings.Split(config.Events[eventKey].Args, " ")...) 130 131 // in case of -verbose we log the output of the executed command 132 if verbose { 133 cmdReader, err := cmd.StdoutPipe() 134 if err != nil { 135 fmt.Fprintln(os.Stderr, "Error creating StdoutPipe for Cmd", err) 136 return 137 } 138 scanner := bufio.NewScanner(cmdReader) 139 go func() { 140 for scanner.Scan() { 141 color.White("> " + scanner.Text() + "\n") 142 } 143 }() 144 cmdReader, err = cmd.StderrPipe() 145 if err != nil { 146 fmt.Fprintln(os.Stderr, "Error creating StderrPipe for Cmd", err) 147 return 148 } 149 scanner = bufio.NewScanner(cmdReader) 150 go func() { 151 for scanner.Scan() { 152 color.Yellow("> " + scanner.Text() + "\n") 153 } 154 }() 155 } 156 157 // launch it 158 err := cmd.Start() 159 if err != nil { 160 color.Set(color.FgRed) 161 fmt.Fprintln(os.Stderr, "Error starting Cmd ", cmd.Path, cmd.Args, err) 162 color.Set(color.Faint) 163 return 164 } 165 } 166 167 // BadRequestHandler handles bad requests. Status 400 and JSON error message. 168 func BadRequestHandler(w http.ResponseWriter, r *http.Request) { 169 w.WriteHeader(http.StatusBadRequest) 170 w.Header().Set("Content-Type", "application/json; charset=utf-8") 171 w.Write([]byte(`{"message": "I don't know what you're talking about"}`)) 172 } 173 174 // HeyHandler handles GET request on /. 175 func HeyHandler(w http.ResponseWriter, r *http.Request) { 176 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 177 w.Write([]byte(`Hey, what's up?`)) 178 } 179 180 func main() { 181 flag.BoolVar(&verbose, "v", false, "Whether we output stuff.") 182 flag.BoolVar(&verboseTunnel, "vt", false, "Whether we output stuff regarding tunneling.") 183 flag.StringVar(&configFile, "c", "", "config file to load other than ./config.toml") 184 flag.Parse() 185 186 // load the config.toml 187 config = loadConfig() 188 addr := config.Addr + ":" + strconv.Itoa(config.Port) 189 color.White(` __ 190 / /_ ____ __________ ____ ____ ____ 191 / __ \/ __ ` + "`" + `/ ___/ __ \/ __ \/ __ \/ __ \ 192 / / / / /_/ / / / /_/ / /_/ / /_/ / / / / 193 /_/ /_/\__,_/_/ / .___/\____/\____/_/ /_/ 194 /_/ v2 195 `) 196 color.White("\tListening on " + addr) 197 readyToListen := false 198 199 if config.Tunnel { 200 if verboseTunnel { 201 gotunnelme.Debug = true 202 } 203 tunnel := gotunnelme.NewTunnel() 204 url, err := tunnel.GetUrl(config.TunnelName) 205 if err != nil { 206 panic("Could not get localtunnel.me URL. " + err.Error()) 207 } 208 go func() { 209 for !readyToListen { 210 time.Sleep(1 * time.Second) 211 } 212 color.Cyan("\tTunnel URL: " + url) 213 err := tunnel.CreateTunnel(config.Port) 214 if err != nil { 215 panic("Could not create tunnel. " + err.Error()) 216 } 217 }() 218 } 219 220 // router & server 221 r := mux.NewRouter() 222 r.HandleFunc("/", HookHandler).Methods("POST") 223 r.HandleFunc("/", HeyHandler).Methods("GET") 224 readyToListen = true 225 http.ListenAndServe(addr, r) 226 }