From 5ddddde2de54b6401ff3438375dbe020898d8a33 Mon Sep 17 00:00:00 2001 From: Marwan Sulaiman Date: Fri, 26 May 2023 14:20:45 +0200 Subject: [PATCH 1/2] Restore compatibility with non-github.com hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts 890239bcfeda7ada7655084b89c76202348c3ed6 Co-authored-by: Mislav Marohnić --- go.mod | 19 +++++++- go.sum | 49 ++++++++++++++++++++ webhook/create_webhook.go | 97 +++++++++++++++++++++++++++++++++++++++ webhook/forward.go | 93 +++++++++---------------------------- 4 files changed, 186 insertions(+), 72 deletions(-) create mode 100644 webhook/create_webhook.go diff --git a/go.mod b/go.mod index b755f56..119b93d 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,27 @@ require ( ) require ( + github.com/cli/safeexec v1.0.0 // indirect + github.com/cli/shurcooL-graphql v0.0.3 // indirect + github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/muesli/termenv v0.12.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect + golang.org/x/net v0.7.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) -require github.com/gorilla/websocket v1.5.0 +require ( + github.com/cli/go-gh/v2 v2.0.0 + github.com/gorilla/websocket v1.5.0 +) replace golang.org/x/crypto => github.com/cli/crypto v0.0.0-20210929142629-6be313f59b03 diff --git a/go.sum b/go.sum index 03749af..a2d13c1 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,63 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/cli/go-gh/v2 v2.0.0 h1:JAgQY7VNHletsO0Eqr+/PzF7fF5QEjhY2t2+Tev3vmk= +github.com/cli/go-gh/v2 v2.0.0/go.mod h1:2/ox3Dnc8wDBT5bnTAH1aKGy6Qt1ztlFBe10EufnvoA= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/shurcooL-graphql v0.0.3 h1:CtpPxyGDs136/+ZeyAfUKYmcQBjDlq5aqnrDCW5Ghh8= +github.com/cli/shurcooL-graphql v0.0.3/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= +github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= +github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/webhook/create_webhook.go b/webhook/create_webhook.go new file mode 100644 index 0000000..0b73138 --- /dev/null +++ b/webhook/create_webhook.go @@ -0,0 +1,97 @@ +package webhook + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/cli/go-gh/v2/pkg/api" + "github.com/cli/go-gh/v2/pkg/auth" +) + +type createHookRequest struct { + Name string `json:"name"` + Events []string `json:"events"` + Active bool `json:"active"` + Config hookConfig `json:"config"` +} + +type hookConfig struct { + ContentType string `json:"content_type"` + InsecureSSL string `json:"insecure_ssl"` + URL string `json:"url"` + Secret string `json:"secret,omitempty"` +} + +type createHookResponse struct { + Active bool `json:"active"` + Config hookConfig `json:"config"` + Events []string `json:"events"` + ID int `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + WsURL string `json:"ws_url"` +} + +// createHook issues a request against the GitHub API to create a dev webhook +func createHook(o *hookOptions) (string, func() error, error) { + apiClient, err := api.NewRESTClient(api.ClientOptions{ + Host: o.GitHubHost, + AuthToken: o.authToken, + }) + if err != nil { + return "", nil, fmt.Errorf("error creating rest client: %w", err) + } + path := fmt.Sprintf("repos/%s/hooks", o.Repo) + if o.Org != "" { + path = fmt.Sprintf("orgs/%s/hooks", o.Org) + } + + req := createHookRequest{ + Name: "cli", + Events: o.EventTypes, + Active: false, + Config: hookConfig{ + ContentType: "json", + InsecureSSL: "0", + Secret: o.Secret, + }, + } + + reqBytes, err := json.Marshal(req) + if err != nil { + return "", nil, err + } + var res createHookResponse + err = apiClient.Post(path, bytes.NewReader(reqBytes), &res) + if err != nil { + var apierr *api.HTTPError + if errors.As(err, &apierr) && apierr.StatusCode == http.StatusForbidden { + return "", nil, fmt.Errorf("you do not have access to this feature") + } + return "", nil, fmt.Errorf("error creating webhook: %w", err) + } + + // reset path for activation. + path += "/" + strconv.Itoa(res.ID) + + return res.WsURL, func() error { + err = apiClient.Patch(path, strings.NewReader(`{"active": true}`), nil) + if err != nil { + return fmt.Errorf("error activating webhook: %w", err) + } + return nil + }, nil +} + +func authTokenForHost(host string) (string, error) { + token, _ := auth.TokenForHost(host) + if token == "" { + return "", fmt.Errorf("gh auth token not found for host %q", host) + } + return token, nil +} diff --git a/webhook/forward.go b/webhook/forward.go index f259e9e..d7fd2a4 100644 --- a/webhook/forward.go +++ b/webhook/forward.go @@ -8,7 +8,6 @@ import ( "log" "net/http" "os" - "os/exec" "strings" "time" @@ -17,23 +16,19 @@ import ( "github.com/spf13/cobra" ) -const ( - webhookForwarderProdURL = "wss://webhook-forwarder.github.com" -) - type hookOptions struct { - Out io.Writer - ErrOut io.Writer - WebhookForwarder string - EventTypes []string - Repo string - Org string - Secret string + Out io.Writer + ErrOut io.Writer + GitHubHost string + authToken string + EventTypes []string + Repo string + Org string + Secret string } // NewCmdForward returns a forward command. func NewCmdForward(runF func(*hookOptions) error) *cobra.Command { - var githubHostname string var localURL string opts := &hookOptions{ Out: os.Stdout, @@ -62,25 +57,20 @@ func NewCmdForward(runF func(*hookOptions) error) *cobra.Command { return runF(opts) } - token, err := authTokenForHost(githubHostname) + var err error + opts.authToken, err = authTokenForHost(opts.GitHubHost) if err != nil { return fmt.Errorf("fatal: error fetching gh token: %w", err) - } else if token == "" { + } else if opts.authToken == "" { return errors.New("fatal: you must be authenticated with gh to run this command") } - if opts.WebhookForwarder == "" { - opts.WebhookForwarder = webhookForwarderProdURL - } - wsURL := strings.TrimSuffix(opts.WebhookForwarder, "/") + "/forward" - chp := createHookParams{ - Events: opts.EventTypes, - Repo: opts.Repo, - Org: opts.Org, - Secret: opts.Secret, + wsURL, activate, err := createHook(opts) + if err != nil { + return err } for i := 0; i < 3; i++ { - if err = runFwd(opts.Out, localURL, token, wsURL, chp); err != nil { + if err = runFwd(opts.Out, localURL, opts.authToken, wsURL, activate); err != nil { if websocket.IsCloseError(err, websocket.CloseNormalClosure) { return nil } @@ -92,7 +82,7 @@ func NewCmdForward(runF func(*hookOptions) error) *cobra.Command { cmd.Flags().StringSliceVarP(&opts.EventTypes, "events", "E", nil, "Names of the event `types` to forward. Use `*` to forward all events.") cmd.MarkFlagRequired("events") cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Name of the repo where the webhook is installed") - cmd.Flags().StringVarP(&githubHostname, "github-host", "H", "github.com", "GitHub host name") + cmd.Flags().StringVarP(&opts.GitHubHost, "github-host", "H", "github.com", "GitHub host name") cmd.Flags().StringVarP(&localURL, "url", "U", "", "Address of the local server to receive events. If omitted, events will be printed to stdout.") cmd.Flags().StringVarP(&opts.Org, "org", "O", "", "Name of the org where the webhook is installed") cmd.Flags().StringVarP(&opts.Secret, "secret", "S", "", "Webhook secret for incoming events") @@ -104,9 +94,9 @@ type wsEventReceived struct { Body []byte } -func runFwd(out io.Writer, url, token, wsURL string, chp createHookParams) error { +func runFwd(out io.Writer, url, token, wsURL string, activateHook func() error) error { for i := 0; i < 3; i++ { - err := handleWebsocket(out, url, token, wsURL, chp) + err := handleWebsocket(out, url, token, wsURL, activateHook) if err != nil { // If the error is a server disconnect (1006), retry connecting if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) { @@ -119,40 +109,18 @@ func runFwd(out io.Writer, url, token, wsURL string, chp createHookParams) error return fmt.Errorf("unable to connect to webhooks server, forwarding stopped") } -type createHookParams struct { - Events []string - Repo string - Org string - Secret string -} - -type eventBody struct{ Type, Message string } - // handleWebsocket mediates between websocket server and local web server -func handleWebsocket(out io.Writer, url, token, wsURL string, chp createHookParams) error { +func handleWebsocket(out io.Writer, url, token, wsURL string, activateHook func() error) error { c, err := dial(token, wsURL) if err != nil { return fmt.Errorf("error dialing to ws server: %w", err) } defer c.Close() - err = c.WriteJSON(chp) - if err != nil { - return fmt.Errorf("error sending create hook params: %w", err) - } - - c.SetReadDeadline(time.Now().Add(5 * time.Second)) - var eb eventBody - err = c.ReadJSON(&eb) - if err != nil { - return fmt.Errorf("error reading create hook response: %w", err) - } - if eb.Type == "error" { - return fmt.Errorf("error creating hook: %s", eb.Message) - } - c.SetReadDeadline(time.Time{}) - fmt.Fprintf(out, "Forwarding Webhook events from GitHub...\n") + if err := activateHook(); err != nil { + return fmt.Errorf("error activating hook: %w", err) + } for { var ev wsEventReceived @@ -232,20 +200,3 @@ func forwardEvent(url string, ev wsEventReceived) (*httpEventForward, error) { Body: body, }, nil } - -func authTokenForHost(host string) (string, error) { - ghExe := os.Getenv("GH_PATH") - if ghExe == "" { - var err error - ghExe, err = exec.LookPath("gh") - if err != nil { - return "", err - } - } - cmd := exec.Command(ghExe, "auth", "token", "--secure-storage", "--hostname", host) - result, err := cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(result)), nil -} From 4b303c3d26a2d2075ee189bb0238d93ef1612cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 26 May 2023 16:15:38 +0200 Subject: [PATCH 2/2] :nail_care: --- main.go | 2 +- webhook/create_webhook.go | 29 ++++++----- webhook/forward.go | 104 ++++++++++++++++++-------------------- 3 files changed, 67 insertions(+), 68 deletions(-) diff --git a/main.go b/main.go index e002fc9..c3ec128 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( ) func main() { - if err := webhook.NewCmdForward(nil).Execute(); err != nil { + if err := webhook.NewCmdForward().Execute(); err != nil { os.Exit(1) } } diff --git a/webhook/create_webhook.go b/webhook/create_webhook.go index 0b73138..794b8ce 100644 --- a/webhook/create_webhook.go +++ b/webhook/create_webhook.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "strconv" "strings" "github.com/cli/go-gh/v2/pkg/api" @@ -37,28 +36,37 @@ type createHookResponse struct { WsURL string `json:"ws_url"` } +type hookOptions struct { + gitHubHost string + authToken string + eventTypes []string + repo string + org string + secret string +} + // createHook issues a request against the GitHub API to create a dev webhook func createHook(o *hookOptions) (string, func() error, error) { apiClient, err := api.NewRESTClient(api.ClientOptions{ - Host: o.GitHubHost, + Host: o.gitHubHost, AuthToken: o.authToken, }) if err != nil { - return "", nil, fmt.Errorf("error creating rest client: %w", err) + return "", nil, fmt.Errorf("error creating REST client: %w", err) } - path := fmt.Sprintf("repos/%s/hooks", o.Repo) - if o.Org != "" { - path = fmt.Sprintf("orgs/%s/hooks", o.Org) + path := fmt.Sprintf("repos/%s/hooks", o.repo) + if o.org != "" { + path = fmt.Sprintf("orgs/%s/hooks", o.org) } req := createHookRequest{ Name: "cli", - Events: o.EventTypes, + Events: o.eventTypes, Active: false, Config: hookConfig{ ContentType: "json", InsecureSSL: "0", - Secret: o.Secret, + Secret: o.secret, }, } @@ -76,11 +84,8 @@ func createHook(o *hookOptions) (string, func() error, error) { return "", nil, fmt.Errorf("error creating webhook: %w", err) } - // reset path for activation. - path += "/" + strconv.Itoa(res.ID) - return res.WsURL, func() error { - err = apiClient.Patch(path, strings.NewReader(`{"active": true}`), nil) + err := apiClient.Patch(res.URL, strings.NewReader(`{"active": true}`), nil) if err != nil { return fmt.Errorf("error activating webhook: %w", err) } diff --git a/webhook/forward.go b/webhook/forward.go index d7fd2a4..b64d527 100644 --- a/webhook/forward.go +++ b/webhook/forward.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "log" "net/http" "os" "strings" @@ -16,24 +15,17 @@ import ( "github.com/spf13/cobra" ) -type hookOptions struct { - Out io.Writer - ErrOut io.Writer - GitHubHost string - authToken string - EventTypes []string - Repo string - Org string - Secret string -} - // NewCmdForward returns a forward command. -func NewCmdForward(runF func(*hookOptions) error) *cobra.Command { - var localURL string - opts := &hookOptions{ - Out: os.Stdout, - ErrOut: os.Stderr, - } +func NewCmdForward() *cobra.Command { + var ( + localURL string + eventTypes []string + targetRepo string + targetOrg string + githubHost string + webhookSecret string + ) + cmd := &cobra.Command{ Use: "forward --events= [--url=]", Short: "Receive test events locally", @@ -45,47 +37,39 @@ func NewCmdForward(runF func(*hookOptions) error) *cobra.Command { $ gh webhook forward --events=issues --org=github --url="http://localhost:9999/webhooks" `), RunE: func(*cobra.Command, []string) error { - if opts.Repo == "" && opts.Org == "" { + if targetRepo == "" && targetOrg == "" { return errors.New("`--repo` or `--org` flag required") } - if localURL == "" { - fmt.Fprintln(opts.ErrOut, "note: no `--url` specified; printing webhook payloads to stdout") - } - - if runF != nil { - return runF(opts) - } - - var err error - opts.authToken, err = authTokenForHost(opts.GitHubHost) + authToken, err := authTokenForHost(githubHost) if err != nil { return fmt.Errorf("fatal: error fetching gh token: %w", err) - } else if opts.authToken == "" { - return errors.New("fatal: you must be authenticated with gh to run this command") } - wsURL, activate, err := createHook(opts) + wsURL, activate, err := createHook(&hookOptions{ + gitHubHost: githubHost, + eventTypes: eventTypes, + authToken: authToken, + repo: targetRepo, + org: targetOrg, + secret: webhookSecret, + }) if err != nil { return err } - for i := 0; i < 3; i++ { - if err = runFwd(opts.Out, localURL, opts.authToken, wsURL, activate); err != nil { - if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - return nil - } - } - } - return err + + return runFwd(os.Stdout, localURL, authToken, wsURL, activate) }, } - cmd.Flags().StringSliceVarP(&opts.EventTypes, "events", "E", nil, "Names of the event `types` to forward. Use `*` to forward all events.") - cmd.MarkFlagRequired("events") - cmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Name of the repo where the webhook is installed") - cmd.Flags().StringVarP(&opts.GitHubHost, "github-host", "H", "github.com", "GitHub host name") + + cmd.Flags().StringSliceVarP(&eventTypes, "events", "E", nil, "Names of the event `types` to forward. Use `*` to forward all events.") + _ = cmd.MarkFlagRequired("events") + cmd.Flags().StringVarP(&targetRepo, "repo", "R", "", "Name of the repo where the webhook is installed") + cmd.Flags().StringVarP(&githubHost, "github-host", "H", "github.com", "GitHub host name") cmd.Flags().StringVarP(&localURL, "url", "U", "", "Address of the local server to receive events. If omitted, events will be printed to stdout.") - cmd.Flags().StringVarP(&opts.Org, "org", "O", "", "Name of the org where the webhook is installed") - cmd.Flags().StringVarP(&opts.Secret, "secret", "S", "", "Webhook secret for incoming events") + cmd.Flags().StringVarP(&targetOrg, "org", "O", "", "Name of the org where the webhook is installed") + cmd.Flags().StringVarP(&webhookSecret, "secret", "S", "", "Webhook secret for incoming events") + return cmd } @@ -95,6 +79,9 @@ type wsEventReceived struct { } func runFwd(out io.Writer, url, token, wsURL string, activateHook func() error) error { + if url == "" { + fmt.Fprintln(os.Stderr, "notice: no `--url` specified; printing webhook payloads to stdout") + } for i := 0; i < 3; i++ { err := handleWebsocket(out, url, token, wsURL, activateHook) if err != nil { @@ -102,6 +89,8 @@ func runFwd(out io.Writer, url, token, wsURL string, activateHook func() error) if websocket.IsCloseError(err, websocket.CloseAbnormalClosure) { time.Sleep(5 * time.Second) continue + } else if websocket.IsCloseError(err, websocket.CloseNormalClosure) { + return nil } return err } @@ -117,7 +106,7 @@ func handleWebsocket(out io.Writer, url, token, wsURL string, activateHook func( } defer c.Close() - fmt.Fprintf(out, "Forwarding Webhook events from GitHub...\n") + fmt.Fprintln(os.Stderr, "Forwarding Webhook events from GitHub...") if err := activateHook(); err != nil { return fmt.Errorf("error activating hook: %w", err) } @@ -129,9 +118,9 @@ func handleWebsocket(out io.Writer, url, token, wsURL string, activateHook func( return fmt.Errorf("error receiving json event: %w", err) } - resp, err := forwardEvent(url, ev) + resp, err := forwardEvent(out, url, ev) if err != nil { - fmt.Fprintf(out, "Error forwarding event: %v\n", err) + fmt.Fprintf(os.Stderr, "warning: error forwarding event: %v\n", err) continue } @@ -164,13 +153,18 @@ type httpEventForward struct { } // forwardEvent forwards events to the server running on the local port specified by the user -func forwardEvent(url string, ev wsEventReceived) (*httpEventForward, error) { - event := ev.Header.Get("X-GitHub-Event") - event = strings.ReplaceAll(event, "\n", "") - event = strings.ReplaceAll(event, "\r", "") - log.Printf("[LOG] received the following event: %v \n", event) +func forwardEvent(w io.Writer, url string, ev wsEventReceived) (*httpEventForward, error) { if url == "" { - fmt.Printf("%s\n", ev.Body) + event := ev.Header.Get("X-GitHub-Event") + event = strings.ReplaceAll(event, "\n", "") + event = strings.ReplaceAll(event, "\r", "") + fmt.Fprintf(os.Stderr, "[LOG] received event %q\n", event) + if _, err := w.Write(ev.Body); err != nil { + return nil, err + } + if _, err := w.Write([]byte("\n")); err != nil { + return nil, err + } return &httpEventForward{Status: 200, Header: make(http.Header), Body: []byte("OK")}, nil }