package main import ( "context" "errors" "fmt" "log" "net/http" "os" "os/exec" "os/signal" "sync" "time" // we use pflag package because it understands double dashes flags "github.com/spf13/pflag" ) var ( servePort string isHelp bool cmd string cmdArgs []string commandExecDir string ) // some args parsing func init() { flags.StringVarP(&servePort, "port", "p", "8080", "Port to listen for incoming connections.") flags.BoolVarP(&isHelp, "help", "h", false, "Prints this help message and exits.") flags.StringVarP(&cmd, "cmd", "c", "", "REQUIRED: The command to execute.") flags.StringSliceVarP(&cmdArgs, "args", "a", []string{}, "Arguments for the command. Can be provided multiple times or as comma-separated string.") flags.StringVarP(&commandExecDir, "dir", "d", "", "The Directory to execute the command.\nDefaults to the directory the tool was called on.") flags.Parse() } // handles the webhook requests func handler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // so I don't forget to close it -> eventhough it's not read. cmd := exec.CommandContext(r.Context(), cmd, cmdArgs...) // set directory only if it's present. if commandExecDir != "" { cmd.Dir = commandExecDir } out, err := cmd.CombinedOutput() if err != nil { log.Println(err) fmt.Fprint(w, err.Error()) w.WriteHeader(http.StatusBadGateway) } output := string(out) log.Println("Command output:\n" + output) fmt.Fprint(w, output) w.WriteHeader(http.StatusOK) } func main() { if isHelp { printHelp() // printing help is treated as a successful execution. os.Exit(0) } err := validateParams() if err != nil { log.Fatalln(err) } // use the default mux because it implements the Handler interface // which we need for the server struct. mux := http.NewServeMux() mux.HandleFunc("/", handler) // only add one handler on root path // custom server struct to set custom timeouts for better performance. server := &http.Server{ Handler: mux, Addr: fmt.Sprintf(":%s", servePort), WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, IdleTimeout: 120 * time.Second, } // server start and stop management var wg sync.WaitGroup wg.Add(2) go startHTTPServer(server, &wg) go listenToStopHTTPServer(server, &wg) wg.Wait() } func startHTTPServer(server *http.Server, wg *sync.WaitGroup) { defer wg.Done() log.Printf("The HTTP server is running: http://localhost:%s/\n", servePort) if err := server.ListenAndServe(); err != nil { if errors.Is(err, http.ErrServerClosed) { log.Println("Shutting down the server...") return } log.Fatalf("An unexpected error happened while running the HTTP server: %v\n", err) } } func listenToStopHTTPServer(server *http.Server, wg *sync.WaitGroup) { defer wg.Done() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) <-stop // block til signal is captured. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Fatalf("An error happened on the shutdown of the server: %v", err) } } func printHelp() { fmt.Println(`A small server to listen for requests to execute some particular code. USAGE: whook [FLAGS] --cmd [COMMAND [--args [ARGS]] FLAGS:`) flags.PrintDefaults() } // validateParams if required params are present. func validateParams() error { if cmd == "" { return errors.New("missing required 'cmd' parameter") } return nil }