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  }