diff --git a/go.mod b/go.mod index 1106f7d..d38abe9 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/go-chi/chi/v5 v5.0.10 github.com/google/go-github/v57 v57.0.0 + github.com/kr/pretty v0.3.1 github.com/mattn/go-isatty v0.0.20 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/mitchellh/mapstructure v1.5.0 @@ -20,7 +21,9 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/net v0.17.0 // indirect diff --git a/go.sum b/go.sum index 8103875..e42f99b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -12,6 +13,10 @@ github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -21,10 +26,13 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/gosmee/app.go b/gosmee/app.go index 1873a85..e342c8f 100644 --- a/gosmee/app.go +++ b/gosmee/app.go @@ -23,10 +23,9 @@ func makeapp() *cli.App { app := &cli.App{ Name: "gosmee", Usage: "Forward SMEE url from an external endpoint to a local service", - UsageText: `gosmee can forward webhook from https://smee.io or from -itself to a local service. The server is the one from where the webhook -points to. The client runs on your laptop or behind a non publically -accessible endpoint and forward request to your local service`, + UsageText: `Gosmee can help you reroute webhooks either from https://smee.io or its own server to a local service. +Where the server is the source of the webhook, and the client, which you run on your laptop or behind a +non-publicly accessible endpoint, forward those requests to your local service.`, EnableBashCompletion: true, Version: strings.TrimSpace(string(Version)), Commands: []*cli.Command{ @@ -36,19 +35,7 @@ accessible endpoint and forward request to your local service`, Action: func(c *cli.Context) error { return replay(c) }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "github-token", - Usage: "GitHub token to use to replay payloads", - Required: true, - Aliases: []string{"t"}, - }, - &cli.BoolFlag{ - Name: "list-hooks", - Usage: "List hooks and its a IDs from a repository", - Aliases: []string{"L"}, - }, - }, + Flags: append(commonFlags, replayFlags...), }, { Name: "server", @@ -59,43 +46,7 @@ accessible endpoint and forward request to your local service`, } return serve(c) }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "public-url", - Usage: "Public URL to show to user, useful when you are behind a proxy.", - }, - &cli.IntFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: defaultServerPort, - Usage: "Port to listen on", - }, - &cli.BoolFlag{ - Name: "auto-cert", - Value: false, - Usage: "Automatically generate letsencrypt certs", - }, - &cli.StringFlag{ - Name: "footer", - Usage: "An HTML string to show in footer for copyright and author", - }, - &cli.StringFlag{ - Name: "address", - Aliases: []string{"a"}, - Value: defaultServerAddress, - Usage: "Address to listen on", - }, - &cli.StringFlag{ - Name: "tls-cert", - Usage: "TLS certificate file", - EnvVars: []string{"GOSMEE_TLS_CERT"}, - }, - &cli.StringFlag{ - Name: "tls-key", - Usage: "TLS key file", - EnvVars: []string{"GOSMEE_TLS_KEY"}, - }, - }, + Flags: serverFlags, }, { Name: "client", @@ -129,60 +80,22 @@ accessible endpoint and forward request to your local service`, decorate = false } cfg := goSmee{ - smeeURL: smeeURL, - targetURL: targetURL, - saveDir: c.String("saveDir"), - noReplay: c.Bool("noReplay"), - decorate: decorate, - ignoreEvents: c.StringSlice("ignore-event"), - channel: c.String("channel"), - targetCnxTimeout: c.Int("target-connection-timeout"), - insecureTLSVerify: c.Bool("insecure-skip-tls-verify"), + replayDataOpts: &replayDataOpts{ + smeeURL: smeeURL, + targetURL: targetURL, + saveDir: c.String("saveDir"), + noReplay: c.Bool("noReplay"), + decorate: decorate, + ignoreEvents: c.StringSlice("ignore-event"), + targetCnxTimeout: c.Int("target-connection-timeout"), + insecureTLSVerify: c.Bool("insecure-skip-tls-verify"), + }, + channel: c.String("channel"), } err := cfg.clientSetup() return err }, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "channel", - Aliases: []string{"c"}, - Usage: "gosmee channel to listen, only useful when you are not use smee.io", - Value: smeeChannel, - }, - &cli.StringSliceFlag{ - Name: "ignore-event", - Aliases: []string{"I"}, - Usage: "Ignore these events", - }, - &cli.StringFlag{ - Name: "saveDir", - Usage: "Save payloads to `DIR` populated with shell scripts to replay easily.", - Aliases: []string{"s"}, - EnvVars: []string{"GOSMEE_SAVEDIR"}, - }, - &cli.IntFlag{ - Name: "target-connection-timeout", - Usage: "How long to wait when forwarding the request to the service", - EnvVars: []string{"GOSMEE_TARGET_TIMEOUT"}, - Value: defaultTimeout, - }, - &cli.BoolFlag{ - Name: "noReplay", - Usage: "Do not replay payloads", - Aliases: []string{"n"}, - Value: false, - }, - &cli.BoolFlag{ - Name: "nocolor", - Usage: "Disable color output, automatically disabled when non tty", - EnvVars: []string{"NO_COLOR"}, - }, - &cli.BoolFlag{ - Name: "insecure-skip-tls-verify", - Value: false, - Usage: "If true, the target server's certificate will not be checked for validity. This will make your HTTPS connections insecure", - }, - }, + Flags: append(commonFlags, clientFlags...), }, { Name: "completion", diff --git a/gosmee/client.go b/gosmee/client.go index 5c9c810..6492f58 100644 --- a/gosmee/client.go +++ b/gosmee/client.go @@ -39,12 +39,8 @@ const smeeChannel = "messages" const tsFormat = "2006-01-02T15.04.01.000" type goSmee struct { - saveDir, smeeURL, targetURL string - decorate, noReplay bool - ignoreEvents []string - channel string - targetCnxTimeout int - insecureTLSVerify bool + replayDataOpts *replayDataOpts + channel string } type payloadMsg struct { @@ -144,10 +140,10 @@ func (c goSmee) parse(now time.Time, data []byte) (payloadMsg, error) { pm.timestamp = dt.Format(tsFormat) - if len(c.ignoreEvents) > 0 && pm.eventType != "" { - for _, v := range c.ignoreEvents { + if len(c.replayDataOpts.ignoreEvents) > 0 && pm.eventType != "" { + for _, v := range c.replayDataOpts.ignoreEvents { if v == pm.eventType { - fmt.Fprintf(os.Stdout, "%sskipping event %s as requested\n", c.emoji("!", "blue+b"), pm.eventType) + fmt.Fprintf(os.Stdout, "%sskipping event %s as requested\n", emoji("!", "blue+b", c.replayDataOpts.decorate), pm.eventType) return payloadMsg{}, nil } } @@ -156,8 +152,8 @@ func (c goSmee) parse(now time.Time, data []byte) (payloadMsg, error) { return pm, nil } -func (c goSmee) emoji(emoji, color string) string { - if !c.decorate { +func emoji(emoji, color string, decorate bool) string { + if !decorate { return "" } return ansi.Color(emoji, color) + " " @@ -165,8 +161,8 @@ func (c goSmee) emoji(emoji, color string) string { func (c goSmee) saveData(pm payloadMsg) error { // check if saveDir is created - if _, err := os.Stat(c.saveDir); os.IsNotExist(err) { - if err := os.MkdirAll(c.saveDir, 0o755); err != nil { + if _, err := os.Stat(c.replayDataOpts.saveDir); os.IsNotExist(err) { + if err := os.MkdirAll(c.replayDataOpts.saveDir, 0o755); err != nil { return err } } @@ -178,7 +174,7 @@ func (c goSmee) saveData(pm payloadMsg) error { fbasepath = pm.timestamp } - jsonfile := fmt.Sprintf("%s/%s.json", c.saveDir, fbasepath) + jsonfile := fmt.Sprintf("%s/%s.json", c.replayDataOpts.saveDir, fbasepath) f, err := os.Create(jsonfile) if err != nil { return err @@ -190,9 +186,9 @@ func (c goSmee) saveData(pm payloadMsg) error { return err } - shscript := fmt.Sprintf("%s/%s.sh", c.saveDir, fbasepath) + shscript := fmt.Sprintf("%s/%s.sh", c.replayDataOpts.saveDir, fbasepath) - fmt.Fprintf(os.Stdout, "%s%s and %s has been saved\n", c.emoji("⌁", "yellow+b"), shscript, jsonfile) + fmt.Fprintf(os.Stdout, "%s%s and %s has been saved\n", emoji("⌁", "yellow+b", c.replayDataOpts.decorate), shscript, jsonfile) s, err := os.Create(shscript) if err != nil { return err @@ -212,7 +208,7 @@ func (c goSmee) saveData(pm payloadMsg) error { FileBase string }{ Headers: headers, - TargetURL: c.targetURL, + TargetURL: c.replayDataOpts.targetURL, ContentType: pm.contentType, FileBase: fbasepath, }); err != nil { @@ -223,12 +219,20 @@ func (c goSmee) saveData(pm payloadMsg) error { return os.Chmod(shscript, 0o755) } -func (c goSmee) replayData(pm payloadMsg) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.targetCnxTimeout)*time.Second) +type replayDataOpts struct { + insecureTLSVerify bool + targetCnxTimeout int + decorate, noReplay bool + saveDir, smeeURL, targetURL string + ignoreEvents []string +} + +func replayData(ropts *replayDataOpts, pm payloadMsg) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(ropts.targetCnxTimeout)*time.Second) defer cancel() //nolint: gosec - client := http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: !c.insecureTLSVerify}}} - req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.targetURL, strings.NewReader(string(pm.body))) + client := http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: !ropts.insecureTLSVerify}}} + req, err := http.NewRequestWithContext(ctx, http.MethodPost, ropts.targetURL, strings.NewReader(string(pm.body))) if err != nil { return err } @@ -256,23 +260,23 @@ func (c goSmee) replayData(pm payloadMsg) error { msg = fmt.Sprintf("%s %s", pm.eventID, msg) } - msg = fmt.Sprintf("%s %s replayed to %s, status: %s", pm.timestamp, msg, ansi.Color(c.targetURL, "green+ub"), ansi.Color(fmt.Sprintf("%d", resp.StatusCode), "blue+b")) + msg = fmt.Sprintf("%s %s replayed to %s, status: %s", pm.timestamp, msg, ansi.Color(ropts.targetURL, "green+ub"), ansi.Color(fmt.Sprintf("%d", resp.StatusCode), "blue+b")) if resp.StatusCode > 299 { msg = fmt.Sprintf("%s, error: %s", msg, resp.Status) } - fmt.Fprintf(os.Stdout, "%s%s\n", c.emoji("•", "magenta+b"), msg) + fmt.Fprintf(os.Stdout, "%s%s\n", emoji("•", "magenta+b", ropts.decorate), msg) return nil } func (c goSmee) clientSetup() error { version := strings.TrimSpace(string(Version)) - fmt.Fprintf(os.Stdout, "%sStarting gosmee version: %s\n", c.emoji("⇉", "green+b"), version) - client := sse.NewClient(c.smeeURL, sse.ClientMaxBufferSize(1<<20)) + fmt.Fprintf(os.Stdout, "%sStarting gosmee version: %s\n", emoji("⇉", "green+b", c.replayDataOpts.decorate), version) + client := sse.NewClient(c.replayDataOpts.smeeURL, sse.ClientMaxBufferSize(1<<20)) client.Headers["User-Agent"] = fmt.Sprintf("gosmee/%s", version) // this is to get nginx to work client.Headers["X-Accel-Buffering"] = "no" - channel := filepath.Base(c.smeeURL) - if strings.HasPrefix(c.smeeURL, "https://smee.io") { + channel := filepath.Base(c.replayDataOpts.smeeURL) + if strings.HasPrefix(c.replayDataOpts.smeeURL, "https://smee.io") { channel = smeeChannel } err := client.Subscribe(channel, func(msg *sse.Event) { @@ -283,9 +287,9 @@ func (c goSmee) clientSetup() error { fmt.Fprintf(os.Stdout, "%s %sForwarding %s to %s\n", nowStr, - c.emoji("✓", "yellow+b"), - ansi.Color(c.smeeURL, "green+u"), - ansi.Color(c.targetURL, "green+u")) + emoji("✓", "yellow+b", c.replayDataOpts.decorate), + ansi.Color(c.replayDataOpts.smeeURL, "green+u"), + ansi.Color(c.replayDataOpts.targetURL, "green+u")) return } @@ -318,7 +322,7 @@ func (c goSmee) clientSetup() error { } if string(msg.Data) != "{}" { - if c.saveDir != "" { + if c.replayDataOpts.saveDir != "" { err := c.saveData(pm) if err != nil { fmt.Fprintf(os.Stdout, @@ -330,8 +334,8 @@ func (c goSmee) clientSetup() error { return } } - if !c.noReplay { - if err := c.replayData(pm); err != nil { + if !c.replayDataOpts.noReplay { + if err := replayData(c.replayDataOpts, pm); err != nil { fmt.Fprintf(os.Stdout, "%s %s forwarding message with headers '%s' - %s\n", nowStr, diff --git a/gosmee/flags.go b/gosmee/flags.go new file mode 100644 index 0000000..52e2c5e --- /dev/null +++ b/gosmee/flags.go @@ -0,0 +1,100 @@ +package gosmee + +import "github.com/urfave/cli/v2" + +var commonFlags = []cli.Flag{ + &cli.StringSliceFlag{ + Name: "ignore-event", + Aliases: []string{"I"}, + Usage: "Ignore these events", + }, + &cli.StringFlag{ + Name: "saveDir", + Usage: "Save payloads to `DIR` populated with shell scripts to replay easily.", + Aliases: []string{"s"}, + EnvVars: []string{"GOSMEE_SAVEDIR"}, + }, + &cli.IntFlag{ + Name: "target-connection-timeout", + Usage: "How long to wait when forwarding the request to the service", + EnvVars: []string{"GOSMEE_TARGET_TIMEOUT"}, + Value: defaultTimeout, + }, + &cli.BoolFlag{ + Name: "noReplay", + Usage: "Do not replay payloads", + Aliases: []string{"n"}, + Value: false, + }, + &cli.BoolFlag{ + Name: "nocolor", + Usage: "Disable color output, automatically disabled when non tty", + EnvVars: []string{"NO_COLOR"}, + }, + &cli.BoolFlag{ + Name: "insecure-skip-tls-verify", + Value: false, + Usage: "If true, the target server's certificate will not be checked for validity. This will make your HTTPS connections insecure", + }, +} + +var replayFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "github-token", + Usage: "GitHub token to use to replay payloads", + Required: true, + Aliases: []string{"t"}, + }, + &cli.BoolFlag{ + Name: "list-hooks", + Usage: "List hooks and its a IDs from a repository", + Aliases: []string{"L"}, + }, +} + +var clientFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "channel", + Aliases: []string{"c"}, + Usage: "gosmee channel to listen, only useful when you are not use smee.io", + Value: smeeChannel, + }, +} + +var serverFlags = []cli.Flag{ + &cli.StringFlag{ + Name: "public-url", + Usage: "Public URL to show to user, useful when you are behind a proxy.", + }, + &cli.IntFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: defaultServerPort, + Usage: "Port to listen on", + }, + &cli.BoolFlag{ + Name: "auto-cert", + Value: false, + Usage: "Automatically generate letsencrypt certs", + }, + &cli.StringFlag{ + Name: "footer", + Usage: "An HTML string to show in footer for copyright and author", + }, + &cli.StringFlag{ + Name: "address", + Aliases: []string{"a"}, + Value: defaultServerAddress, + Usage: "Address to listen on", + }, + &cli.StringFlag{ + Name: "tls-cert", + Usage: "TLS certificate file", + EnvVars: []string{"GOSMEE_TLS_CERT"}, + }, + &cli.StringFlag{ + Name: "tls-key", + Usage: "TLS key file", + EnvVars: []string{"GOSMEE_TLS_KEY"}, + }, +} diff --git a/gosmee/replay.go b/gosmee/replay.go index 5035578..365256f 100644 --- a/gosmee/replay.go +++ b/gosmee/replay.go @@ -3,28 +3,75 @@ package gosmee import ( "context" "fmt" + "net/url" "os" "strconv" "strings" "github.com/google/go-github/v57/github" + "github.com/mattn/go-isatty" + "github.com/mgutz/ansi" "github.com/urfave/cli/v2" ) type replayOpts struct { - cliCtx *cli.Context - client *github.Client - repo string - org string + replayDataOpts *replayDataOpts + cliCtx *cli.Context + client *github.Client + repo string + org string } func (r *replayOpts) replayHooks(ctx context.Context, hookid int64) error { - deliveries, _, err := r.client.Repositories.ListHookDeliveries(ctx, r.org, r.repo, hookid, nil) - if err != nil { - return fmt.Errorf("cannot list deliveries: %w", err) - } - for _, hd := range deliveries { - fmt.Printf("hd.Request.Headers: %v\n", hd.Request.Headers) + opt := &github.ListCursorOptions{} + for { + deliveries, resp, err := r.client.Repositories.ListHookDeliveries(ctx, r.org, r.repo, hookid, opt) + if err != nil { + return fmt.Errorf("cannot list deliveries: %w", err) + } + + for _, hd := range deliveries { + delivery, _, err := r.client.Repositories.GetHookDelivery(ctx, r.org, r.repo, hookid, hd.GetID()) + if err != nil { + return fmt.Errorf("cannot get delivery: %w", err) + } + pm := payloadMsg{} + var ok bool + if pm.contentType, ok = delivery.Request.Headers["Content-Type"]; !ok { + pm.contentType = "application/json" + } + pm.body = delivery.Request.GetRawPayload() + pm.headers = delivery.Request.GetHeaders() + + // get the event type + if pv, ok := pm.headers["X-GitHub-Event"]; ok { + // github action don't like it + replace := strings.NewReplacer(":", "-", " ", "_", "/", "_") + pv = replace.Replace(strings.ToLower(pv)) + // remove all non-alphanumeric characters and don't let directory traversal + pv = pmEventRe.FindString(pv) + pm.eventType = pv + } + if pd, ok := pm.headers["X-GitHub-Delivery"]; ok { + pm.eventID = pd + } + dt := delivery.DeliveredAt.GetTime() + pm.timestamp = dt.Format(tsFormat) + + if err := replayData(r.replayDataOpts, pm); err != nil { + fmt.Fprintf(os.Stdout, + "%s forwarding message with headers '%s' - %s\n", + ansi.Color("ERROR", "red+b"), + pm.headers, + err.Error()) + continue + } + } + + if resp.NextPage == 0 { + break + } + opt.Cursor = resp.Cursor } return nil } @@ -67,10 +114,10 @@ func replay(c *cli.Context) error { repo: org, org: repo, } - if c.IsSet("list-hooks") { return ropt.listHooks(ctx) } + _hookID := c.Args().Get(1) _hookID = strings.TrimSpace(_hookID) if _hookID == "" { @@ -85,5 +132,37 @@ func replay(c *cli.Context) error { if hookID == 0 { return fmt.Errorf("hook-id is required, use --list-hooks to get the hook id") } + + // TODO: remove duplication from client and here + var targetURL string + if os.Getenv("GOSMEE_TARGET_URL") != "" { + targetURL = os.Getenv("GOSMEE_TARGET_URL") + } else { + if c.NArg() != 3 { + return fmt.Errorf("missing the target url where to forward the webhook, ie: http://localhost:8080") + } + targetURL = c.Args().Get(2) + } + if _, err := url.Parse(targetURL); err != nil { + return fmt.Errorf("target url %s is not a valid url %w", targetURL, err) + } + decorate := true + if !isatty.IsTerminal(os.Stdout.Fd()) { + ansi.DisableColors(true) + decorate = false + } + if c.Bool("nocolor") { + ansi.DisableColors(true) + decorate = false + } + ropt.replayDataOpts = &replayDataOpts{ + targetURL: targetURL, + saveDir: c.String("saveDir"), + noReplay: c.Bool("noReplay"), + decorate: decorate, + ignoreEvents: c.StringSlice("ignore-event"), + targetCnxTimeout: c.Int("target-connection-timeout"), + insecureTLSVerify: c.Bool("insecure-skip-tls-verify"), + } return ropt.replayHooks(ctx, hookID) } diff --git a/gosmee/server.go b/gosmee/server.go index 3096992..16e9d5d 100644 --- a/gosmee/server.go +++ b/gosmee/server.go @@ -14,7 +14,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/mgutz/ansi" "github.com/r3labs/sse/v2" "github.com/urfave/cli/v2" "golang.org/x/crypto/acme/autocert" @@ -114,7 +113,6 @@ func serve(c *cli.Context) error { errorIt(w, r, http.StatusBadRequest, err) return } - // convert headers to map[string]string and accumlate for log entry headers := "" payload := map[string]interface{}{} for k, v := range r.Header { @@ -138,7 +136,6 @@ func serve(c *cli.Context) error { fmt.Fprintf(w, "{\"status\": %d, \"channel\": \"%s\", \"message\": \"ok\"}\n", http.StatusAccepted, channel) fmt.Fprintf(os.Stdout, "%s Published %s%s on channel %s\n", now.Format("2006-01-02T15.04.01.000"), middleware.GetReqID(r.Context()), headers, channel) }) - config := goSmee{} autoCert := c.Bool("auto-cert") certFile := c.String("tls-cert") @@ -153,8 +150,7 @@ func serve(c *cli.Context) error { publicURL = fmt.Sprintf("%s%s", publicURL, portAddr) } - fmt.Fprintf(os.Stdout, "%sServing for webhooks on %s\n", config.emoji("✓", "yellow+b"), - ansi.Color(publicURL, "green+u")) + fmt.Fprintf(os.Stdout, "Serving for webhooks on %s\n", publicURL) if sslEnabled { //nolint:gosec diff --git a/vendor/github.com/kr/pretty/.gitignore b/vendor/github.com/kr/pretty/.gitignore new file mode 100644 index 0000000..b2d4781 --- /dev/null +++ b/vendor/github.com/kr/pretty/.gitignore @@ -0,0 +1,5 @@ +[568].out +_go* +_test* +_obj +/.idea diff --git a/vendor/github.com/kr/pretty/License b/vendor/github.com/kr/pretty/License new file mode 100644 index 0000000..480a328 --- /dev/null +++ b/vendor/github.com/kr/pretty/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/kr/pretty/Readme b/vendor/github.com/kr/pretty/Readme new file mode 100644 index 0000000..c589fc6 --- /dev/null +++ b/vendor/github.com/kr/pretty/Readme @@ -0,0 +1,9 @@ +package pretty + + import "github.com/kr/pretty" + + Package pretty provides pretty-printing for Go values. + +Documentation + + http://godoc.org/github.com/kr/pretty diff --git a/vendor/github.com/kr/pretty/diff.go b/vendor/github.com/kr/pretty/diff.go new file mode 100644 index 0000000..40a09dc --- /dev/null +++ b/vendor/github.com/kr/pretty/diff.go @@ -0,0 +1,295 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" +) + +type sbuf []string + +func (p *sbuf) Printf(format string, a ...interface{}) { + s := fmt.Sprintf(format, a...) + *p = append(*p, s) +} + +// Diff returns a slice where each element describes +// a difference between a and b. +func Diff(a, b interface{}) (desc []string) { + Pdiff((*sbuf)(&desc), a, b) + return desc +} + +// wprintfer calls Fprintf on w for each Printf call +// with a trailing newline. +type wprintfer struct{ w io.Writer } + +func (p *wprintfer) Printf(format string, a ...interface{}) { + fmt.Fprintf(p.w, format+"\n", a...) +} + +// Fdiff writes to w a description of the differences between a and b. +func Fdiff(w io.Writer, a, b interface{}) { + Pdiff(&wprintfer{w}, a, b) +} + +type Printfer interface { + Printf(format string, a ...interface{}) +} + +// Pdiff prints to p a description of the differences between a and b. +// It calls Printf once for each difference, with no trailing newline. +// The standard library log.Logger is a Printfer. +func Pdiff(p Printfer, a, b interface{}) { + d := diffPrinter{ + w: p, + aVisited: make(map[visit]visit), + bVisited: make(map[visit]visit), + } + d.diff(reflect.ValueOf(a), reflect.ValueOf(b)) +} + +type Logfer interface { + Logf(format string, a ...interface{}) +} + +// logprintfer calls Fprintf on w for each Printf call +// with a trailing newline. +type logprintfer struct{ l Logfer } + +func (p *logprintfer) Printf(format string, a ...interface{}) { + p.l.Logf(format, a...) +} + +// Ldiff prints to l a description of the differences between a and b. +// It calls Logf once for each difference, with no trailing newline. +// The standard library testing.T and testing.B are Logfers. +func Ldiff(l Logfer, a, b interface{}) { + Pdiff(&logprintfer{l}, a, b) +} + +type diffPrinter struct { + w Printfer + l string // label + + aVisited map[visit]visit + bVisited map[visit]visit +} + +func (w diffPrinter) printf(f string, a ...interface{}) { + var l string + if w.l != "" { + l = w.l + ": " + } + w.w.Printf(l+f, a...) +} + +func (w diffPrinter) diff(av, bv reflect.Value) { + if !av.IsValid() && bv.IsValid() { + w.printf("nil != %# v", formatter{v: bv, quote: true}) + return + } + if av.IsValid() && !bv.IsValid() { + w.printf("%# v != nil", formatter{v: av, quote: true}) + return + } + if !av.IsValid() && !bv.IsValid() { + return + } + + at := av.Type() + bt := bv.Type() + if at != bt { + w.printf("%v != %v", at, bt) + return + } + + if av.CanAddr() && bv.CanAddr() { + avis := visit{av.UnsafeAddr(), at} + bvis := visit{bv.UnsafeAddr(), bt} + var cycle bool + + // Have we seen this value before? + if vis, ok := w.aVisited[avis]; ok { + cycle = true + if vis != bvis { + w.printf("%# v (previously visited) != %# v", formatter{v: av, quote: true}, formatter{v: bv, quote: true}) + } + } else if _, ok := w.bVisited[bvis]; ok { + cycle = true + w.printf("%# v != %# v (previously visited)", formatter{v: av, quote: true}, formatter{v: bv, quote: true}) + } + w.aVisited[avis] = bvis + w.bVisited[bvis] = avis + if cycle { + return + } + } + + switch kind := at.Kind(); kind { + case reflect.Bool: + if a, b := av.Bool(), bv.Bool(); a != b { + w.printf("%v != %v", a, b) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if a, b := av.Int(), bv.Int(); a != b { + w.printf("%d != %d", a, b) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if a, b := av.Uint(), bv.Uint(); a != b { + w.printf("%d != %d", a, b) + } + case reflect.Float32, reflect.Float64: + if a, b := av.Float(), bv.Float(); a != b { + w.printf("%v != %v", a, b) + } + case reflect.Complex64, reflect.Complex128: + if a, b := av.Complex(), bv.Complex(); a != b { + w.printf("%v != %v", a, b) + } + case reflect.Array: + n := av.Len() + for i := 0; i < n; i++ { + w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i)) + } + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + if a, b := av.Pointer(), bv.Pointer(); a != b { + w.printf("%#x != %#x", a, b) + } + case reflect.Interface: + w.diff(av.Elem(), bv.Elem()) + case reflect.Map: + ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys()) + for _, k := range ak { + w := w.relabel(fmt.Sprintf("[%#v]", k)) + w.printf("%q != (missing)", av.MapIndex(k)) + } + for _, k := range both { + w := w.relabel(fmt.Sprintf("[%#v]", k)) + w.diff(av.MapIndex(k), bv.MapIndex(k)) + } + for _, k := range bk { + w := w.relabel(fmt.Sprintf("[%#v]", k)) + w.printf("(missing) != %q", bv.MapIndex(k)) + } + case reflect.Ptr: + switch { + case av.IsNil() && !bv.IsNil(): + w.printf("nil != %# v", formatter{v: bv, quote: true}) + case !av.IsNil() && bv.IsNil(): + w.printf("%# v != nil", formatter{v: av, quote: true}) + case !av.IsNil() && !bv.IsNil(): + w.diff(av.Elem(), bv.Elem()) + } + case reflect.Slice: + lenA := av.Len() + lenB := bv.Len() + if lenA != lenB { + w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB) + break + } + for i := 0; i < lenA; i++ { + w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i)) + } + case reflect.String: + if a, b := av.String(), bv.String(); a != b { + w.printf("%q != %q", a, b) + } + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i)) + } + default: + panic("unknown reflect Kind: " + kind.String()) + } +} + +func (d diffPrinter) relabel(name string) (d1 diffPrinter) { + d1 = d + if d.l != "" && name[0] != '[' { + d1.l += "." + } + d1.l += name + return d1 +} + +// keyEqual compares a and b for equality. +// Both a and b must be valid map keys. +func keyEqual(av, bv reflect.Value) bool { + if !av.IsValid() && !bv.IsValid() { + return true + } + if !av.IsValid() || !bv.IsValid() || av.Type() != bv.Type() { + return false + } + switch kind := av.Kind(); kind { + case reflect.Bool: + a, b := av.Bool(), bv.Bool() + return a == b + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + a, b := av.Int(), bv.Int() + return a == b + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + a, b := av.Uint(), bv.Uint() + return a == b + case reflect.Float32, reflect.Float64: + a, b := av.Float(), bv.Float() + return a == b + case reflect.Complex64, reflect.Complex128: + a, b := av.Complex(), bv.Complex() + return a == b + case reflect.Array: + for i := 0; i < av.Len(); i++ { + if !keyEqual(av.Index(i), bv.Index(i)) { + return false + } + } + return true + case reflect.Chan, reflect.UnsafePointer, reflect.Ptr: + a, b := av.Pointer(), bv.Pointer() + return a == b + case reflect.Interface: + return keyEqual(av.Elem(), bv.Elem()) + case reflect.String: + a, b := av.String(), bv.String() + return a == b + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + if !keyEqual(av.Field(i), bv.Field(i)) { + return false + } + } + return true + default: + panic("invalid map key type " + av.Type().String()) + } +} + +func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) { + for _, av := range a { + inBoth := false + for _, bv := range b { + if keyEqual(av, bv) { + inBoth = true + both = append(both, av) + break + } + } + if !inBoth { + ak = append(ak, av) + } + } + for _, bv := range b { + inBoth := false + for _, av := range a { + if keyEqual(av, bv) { + inBoth = true + break + } + } + if !inBoth { + bk = append(bk, bv) + } + } + return +} diff --git a/vendor/github.com/kr/pretty/formatter.go b/vendor/github.com/kr/pretty/formatter.go new file mode 100644 index 0000000..8e6969c --- /dev/null +++ b/vendor/github.com/kr/pretty/formatter.go @@ -0,0 +1,355 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" + "strconv" + "text/tabwriter" + + "github.com/kr/text" + "github.com/rogpeppe/go-internal/fmtsort" +) + +type formatter struct { + v reflect.Value + force bool + quote bool +} + +// Formatter makes a wrapper, f, that will format x as go source with line +// breaks and tabs. Object f responds to the "%v" formatting verb when both the +// "#" and " " (space) flags are set, for example: +// +// fmt.Sprintf("%# v", Formatter(x)) +// +// If one of these two flags is not set, or any other verb is used, f will +// format x according to the usual rules of package fmt. +// In particular, if x satisfies fmt.Formatter, then x.Format will be called. +func Formatter(x interface{}) (f fmt.Formatter) { + return formatter{v: reflect.ValueOf(x), quote: true} +} + +func (fo formatter) String() string { + return fmt.Sprint(fo.v.Interface()) // unwrap it +} + +func (fo formatter) passThrough(f fmt.State, c rune) { + s := "%" + for i := 0; i < 128; i++ { + if f.Flag(i) { + s += string(rune(i)) + } + } + if w, ok := f.Width(); ok { + s += fmt.Sprintf("%d", w) + } + if p, ok := f.Precision(); ok { + s += fmt.Sprintf(".%d", p) + } + s += string(c) + fmt.Fprintf(f, s, fo.v.Interface()) +} + +func (fo formatter) Format(f fmt.State, c rune) { + if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') { + w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0) + p := &printer{tw: w, Writer: w, visited: make(map[visit]int)} + p.printValue(fo.v, true, fo.quote) + w.Flush() + return + } + fo.passThrough(f, c) +} + +type printer struct { + io.Writer + tw *tabwriter.Writer + visited map[visit]int + depth int +} + +func (p *printer) indent() *printer { + q := *p + q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0) + q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'}) + return &q +} + +func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) { + if showType { + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, "(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } +} + +// printValue must keep track of already-printed pointer values to avoid +// infinite recursion. +type visit struct { + v uintptr + typ reflect.Type +} + +func (p *printer) catchPanic(v reflect.Value, method string) { + if r := recover(); r != nil { + if v.Kind() == reflect.Ptr && v.IsNil() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + return + } + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(PANIC=calling method ") + io.WriteString(p, strconv.Quote(method)) + io.WriteString(p, ": ") + fmt.Fprint(p, r) + writeByte(p, ')') + } +} + +func (p *printer) printValue(v reflect.Value, showType, quote bool) { + if p.depth > 10 { + io.WriteString(p, "!%v(DEPTH EXCEEDED)") + return + } + + if v.IsValid() && v.CanInterface() { + i := v.Interface() + if goStringer, ok := i.(fmt.GoStringer); ok { + defer p.catchPanic(v, "GoString") + io.WriteString(p, goStringer.GoString()) + return + } + } + + switch v.Kind() { + case reflect.Bool: + p.printInline(v, v.Bool(), showType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p.printInline(v, v.Int(), showType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.printInline(v, v.Uint(), showType) + case reflect.Float32, reflect.Float64: + p.printInline(v, v.Float(), showType) + case reflect.Complex64, reflect.Complex128: + fmt.Fprintf(p, "%#v", v.Complex()) + case reflect.String: + p.fmtString(v.String(), quote) + case reflect.Map: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + sm := fmtsort.Sort(v) + for i := 0; i < v.Len(); i++ { + k := sm.Key[i] + mv := sm.Value[i] + pp.printValue(k, false, true) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct := t.Elem().Kind() == reflect.Interface + pp.printValue(mv, showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Struct: + t := v.Type() + if v.CanAddr() { + addr := v.UnsafeAddr() + vis := visit{addr, t} + if vd, ok := p.visited[vis]; ok && vd < p.depth { + p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false) + break // don't print v again + } + p.visited[vis] = p.depth + } + + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.NumField(); i++ { + showTypeInStruct := true + if f := t.Field(i); f.Name != "" { + io.WriteString(pp, f.Name) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = labelType(f.Type) + } + pp.printValue(getField(v, i), showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.NumField()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Interface: + switch e := v.Elem(); { + case e.Kind() == reflect.Invalid: + io.WriteString(p, "nil") + case e.IsValid(): + pp := *p + pp.depth++ + pp.printValue(e, showType, true) + default: + io.WriteString(p, v.Type().String()) + io.WriteString(p, "(nil)") + } + case reflect.Array, reflect.Slice: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + if v.Kind() == reflect.Slice && v.IsNil() && showType { + io.WriteString(p, "(nil)") + break + } + if v.Kind() == reflect.Slice && v.IsNil() { + io.WriteString(p, "nil") + break + } + writeByte(p, '{') + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.Len(); i++ { + showTypeInSlice := t.Elem().Kind() == reflect.Interface + pp.printValue(v.Index(i), showTypeInSlice, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + writeByte(p, '}') + case reflect.Ptr: + e := v.Elem() + if !e.IsValid() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + } else { + pp := *p + pp.depth++ + writeByte(pp, '&') + pp.printValue(e, true, true) + } + case reflect.Chan: + x := v.Pointer() + if showType { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, ")(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } + case reflect.Func: + io.WriteString(p, v.Type().String()) + io.WriteString(p, " {...}") + case reflect.UnsafePointer: + p.printInline(v, v.Pointer(), showType) + case reflect.Invalid: + io.WriteString(p, "nil") + } +} + +func canInline(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return !canExpand(t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if canExpand(t.Field(i).Type) { + return false + } + } + return true + case reflect.Interface: + return false + case reflect.Array, reflect.Slice: + return !canExpand(t.Elem()) + case reflect.Ptr: + return false + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + return false + } + return true +} + +func canExpand(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map, reflect.Struct, + reflect.Interface, reflect.Array, reflect.Slice, + reflect.Ptr: + return true + } + return false +} + +func labelType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Interface, reflect.Struct: + return true + } + return false +} + +func (p *printer) fmtString(s string, quote bool) { + if quote { + s = strconv.Quote(s) + } + io.WriteString(p, s) +} + +func writeByte(w io.Writer, b byte) { + w.Write([]byte{b}) +} + +func getField(v reflect.Value, i int) reflect.Value { + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + return val +} diff --git a/vendor/github.com/kr/pretty/pretty.go b/vendor/github.com/kr/pretty/pretty.go new file mode 100644 index 0000000..b4ca583 --- /dev/null +++ b/vendor/github.com/kr/pretty/pretty.go @@ -0,0 +1,108 @@ +// Package pretty provides pretty-printing for Go values. This is +// useful during debugging, to avoid wrapping long output lines in +// the terminal. +// +// It provides a function, Formatter, that can be used with any +// function that accepts a format string. It also provides +// convenience wrappers for functions in packages fmt and log. +package pretty + +import ( + "fmt" + "io" + "log" + "reflect" +) + +// Errorf is a convenience wrapper for fmt.Errorf. +// +// Calling Errorf(f, x, y) is equivalent to +// fmt.Errorf(f, Formatter(x), Formatter(y)). +func Errorf(format string, a ...interface{}) error { + return fmt.Errorf(format, wrap(a, false)...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf. +// +// Calling Fprintf(w, f, x, y) is equivalent to +// fmt.Fprintf(w, f, Formatter(x), Formatter(y)). +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) { + return fmt.Fprintf(w, format, wrap(a, false)...) +} + +// Log is a convenience wrapper for log.Printf. +// +// Calling Log(x, y) is equivalent to +// log.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Log(a ...interface{}) { + log.Print(wrap(a, true)...) +} + +// Logf is a convenience wrapper for log.Printf. +// +// Calling Logf(f, x, y) is equivalent to +// log.Printf(f, Formatter(x), Formatter(y)). +func Logf(format string, a ...interface{}) { + log.Printf(format, wrap(a, false)...) +} + +// Logln is a convenience wrapper for log.Printf. +// +// Calling Logln(x, y) is equivalent to +// log.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Logln(a ...interface{}) { + log.Println(wrap(a, true)...) +} + +// Print pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Print(a ...interface{}) (n int, errno error) { + return fmt.Print(wrap(a, true)...) +} + +// Printf is a convenience wrapper for fmt.Printf. +// +// Calling Printf(f, x, y) is equivalent to +// fmt.Printf(f, Formatter(x), Formatter(y)). +func Printf(format string, a ...interface{}) (n int, errno error) { + return fmt.Printf(format, wrap(a, false)...) +} + +// Println pretty-prints its operands and writes to standard output. +// +// Calling Println(x, y) is equivalent to +// fmt.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Println(a ...interface{}) (n int, errno error) { + return fmt.Println(wrap(a, true)...) +} + +// Sprint is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprint(x, y) is equivalent to +// fmt.Sprint(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Sprint(a ...interface{}) string { + return fmt.Sprint(wrap(a, true)...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprintf(f, x, y) is equivalent to +// fmt.Sprintf(f, Formatter(x), Formatter(y)). +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, wrap(a, false)...) +} + +func wrap(a []interface{}, force bool) []interface{} { + w := make([]interface{}, len(a)) + for i, x := range a { + w[i] = formatter{v: reflect.ValueOf(x), force: force} + } + return w +} diff --git a/vendor/github.com/kr/pretty/zero.go b/vendor/github.com/kr/pretty/zero.go new file mode 100644 index 0000000..abb5b6f --- /dev/null +++ b/vendor/github.com/kr/pretty/zero.go @@ -0,0 +1,41 @@ +package pretty + +import ( + "reflect" +) + +func nonzero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() != 0 + case reflect.Float32, reflect.Float64: + return v.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() != complex(0, 0) + case reflect.String: + return v.String() != "" + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if nonzero(getField(v, i)) { + return true + } + } + return false + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if nonzero(v.Index(i)) { + return true + } + } + return false + case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func: + return !v.IsNil() + case reflect.UnsafePointer: + return v.Pointer() != 0 + } + return true +} diff --git a/vendor/github.com/kr/text/License b/vendor/github.com/kr/text/License new file mode 100644 index 0000000..480a328 --- /dev/null +++ b/vendor/github.com/kr/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/kr/text/Readme b/vendor/github.com/kr/text/Readme new file mode 100644 index 0000000..7e6e7c0 --- /dev/null +++ b/vendor/github.com/kr/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/vendor/github.com/kr/text/doc.go b/vendor/github.com/kr/text/doc.go new file mode 100644 index 0000000..cf4c198 --- /dev/null +++ b/vendor/github.com/kr/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/vendor/github.com/kr/text/indent.go b/vendor/github.com/kr/text/indent.go new file mode 100644 index 0000000..4ebac45 --- /dev/null +++ b/vendor/github.com/kr/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/vendor/github.com/kr/text/wrap.go b/vendor/github.com/kr/text/wrap.go new file mode 100644 index 0000000..b09bb03 --- /dev/null +++ b/vendor/github.com/kr/text/wrap.go @@ -0,0 +1,86 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func Wrap(s string, lim int) string { + return string(WrapBytes([]byte(s), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapBytes(b []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim || i == n-1 { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/vendor/github.com/rogpeppe/go-internal/LICENSE b/vendor/github.com/rogpeppe/go-internal/LICENSE new file mode 100644 index 0000000..49ea0f9 --- /dev/null +++ b/vendor/github.com/rogpeppe/go-internal/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/rogpeppe/go-internal/fmtsort/mapelem.go b/vendor/github.com/rogpeppe/go-internal/fmtsort/mapelem.go new file mode 100644 index 0000000..4e4c294 --- /dev/null +++ b/vendor/github.com/rogpeppe/go-internal/fmtsort/mapelem.go @@ -0,0 +1,23 @@ +//go:build go1.12 +// +build go1.12 + +package fmtsort + +import "reflect" + +const brokenNaNs = false + +func mapElems(mapValue reflect.Value) ([]reflect.Value, []reflect.Value) { + // Note: this code is arranged to not panic even in the presence + // of a concurrent map update. The runtime is responsible for + // yelling loudly if that happens. See issue 33275. + n := mapValue.Len() + key := make([]reflect.Value, 0, n) + value := make([]reflect.Value, 0, n) + iter := mapValue.MapRange() + for iter.Next() { + key = append(key, iter.Key()) + value = append(value, iter.Value()) + } + return key, value +} diff --git a/vendor/github.com/rogpeppe/go-internal/fmtsort/mapelem_1.11.go b/vendor/github.com/rogpeppe/go-internal/fmtsort/mapelem_1.11.go new file mode 100644 index 0000000..873bf7f --- /dev/null +++ b/vendor/github.com/rogpeppe/go-internal/fmtsort/mapelem_1.11.go @@ -0,0 +1,24 @@ +//go:build !go1.12 +// +build !go1.12 + +package fmtsort + +import "reflect" + +const brokenNaNs = true + +func mapElems(mapValue reflect.Value) ([]reflect.Value, []reflect.Value) { + key := mapValue.MapKeys() + value := make([]reflect.Value, 0, len(key)) + for _, k := range key { + v := mapValue.MapIndex(k) + if !v.IsValid() { + // Note: we can't retrieve the value, probably because + // the key is NaN, so just do the best we can and + // add a zero value of the correct type in that case. + v = reflect.Zero(mapValue.Type().Elem()) + } + value = append(value, v) + } + return key, value +} diff --git a/vendor/github.com/rogpeppe/go-internal/fmtsort/sort.go b/vendor/github.com/rogpeppe/go-internal/fmtsort/sort.go new file mode 100644 index 0000000..0fb5187 --- /dev/null +++ b/vendor/github.com/rogpeppe/go-internal/fmtsort/sort.go @@ -0,0 +1,210 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package fmtsort provides a general stable ordering mechanism +// for maps, on behalf of the fmt and text/template packages. +// It is not guaranteed to be efficient and works only for types +// that are valid map keys. +package fmtsort + +import ( + "reflect" + "sort" +) + +// Note: Throughout this package we avoid calling reflect.Value.Interface as +// it is not always legal to do so and it's easier to avoid the issue than to face it. + +// SortedMap represents a map's keys and values. The keys and values are +// aligned in index order: Value[i] is the value in the map corresponding to Key[i]. +type SortedMap struct { + Key []reflect.Value + Value []reflect.Value +} + +func (o *SortedMap) Len() int { return len(o.Key) } +func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 } +func (o *SortedMap) Swap(i, j int) { + o.Key[i], o.Key[j] = o.Key[j], o.Key[i] + o.Value[i], o.Value[j] = o.Value[j], o.Value[i] +} + +// Sort accepts a map and returns a SortedMap that has the same keys and +// values but in a stable sorted order according to the keys, modulo issues +// raised by unorderable key values such as NaNs. +// +// The ordering rules are more general than with Go's < operator: +// +// - when applicable, nil compares low +// - ints, floats, and strings order by < +// - NaN compares less than non-NaN floats +// - bool compares false before true +// - complex compares real, then imag +// - pointers compare by machine address +// - channel values compare by machine address +// - structs compare each field in turn +// - arrays compare each element in turn. +// Otherwise identical arrays compare by length. +// - interface values compare first by reflect.Type describing the concrete type +// and then by concrete value as described in the previous rules. +// +func Sort(mapValue reflect.Value) *SortedMap { + if mapValue.Type().Kind() != reflect.Map { + return nil + } + key, value := mapElems(mapValue) + sorted := &SortedMap{ + Key: key, + Value: value, + } + sort.Stable(sorted) + return sorted +} + +// compare compares two values of the same type. It returns -1, 0, 1 +// according to whether a > b (1), a == b (0), or a < b (-1). +// If the types differ, it returns -1. +// See the comment on Sort for the comparison rules. +func compare(aVal, bVal reflect.Value) int { + aType, bType := aVal.Type(), bVal.Type() + if aType != bType { + return -1 // No good answer possible, but don't return 0: they're not equal. + } + switch aVal.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + a, b := aVal.Int(), bVal.Int() + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + a, b := aVal.Uint(), bVal.Uint() + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } + case reflect.String: + a, b := aVal.String(), bVal.String() + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } + case reflect.Float32, reflect.Float64: + return floatCompare(aVal.Float(), bVal.Float()) + case reflect.Complex64, reflect.Complex128: + a, b := aVal.Complex(), bVal.Complex() + if c := floatCompare(real(a), real(b)); c != 0 { + return c + } + return floatCompare(imag(a), imag(b)) + case reflect.Bool: + a, b := aVal.Bool(), bVal.Bool() + switch { + case a == b: + return 0 + case a: + return 1 + default: + return -1 + } + case reflect.Ptr: + a, b := aVal.Pointer(), bVal.Pointer() + switch { + case a < b: + return -1 + case a > b: + return 1 + default: + return 0 + } + case reflect.Chan: + if c, ok := nilCompare(aVal, bVal); ok { + return c + } + ap, bp := aVal.Pointer(), bVal.Pointer() + switch { + case ap < bp: + return -1 + case ap > bp: + return 1 + default: + return 0 + } + case reflect.Struct: + for i := 0; i < aVal.NumField(); i++ { + if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 { + return c + } + } + return 0 + case reflect.Array: + for i := 0; i < aVal.Len(); i++ { + if c := compare(aVal.Index(i), bVal.Index(i)); c != 0 { + return c + } + } + return 0 + case reflect.Interface: + if c, ok := nilCompare(aVal, bVal); ok { + return c + } + c := compare(reflect.ValueOf(aVal.Elem().Type()), reflect.ValueOf(bVal.Elem().Type())) + if c != 0 { + return c + } + return compare(aVal.Elem(), bVal.Elem()) + default: + // Certain types cannot appear as keys (maps, funcs, slices), but be explicit. + panic("bad type in compare: " + aType.String()) + } +} + +// nilCompare checks whether either value is nil. If not, the boolean is false. +// If either value is nil, the boolean is true and the integer is the comparison +// value. The comparison is defined to be 0 if both are nil, otherwise the one +// nil value compares low. Both arguments must represent a chan, func, +// interface, map, pointer, or slice. +func nilCompare(aVal, bVal reflect.Value) (int, bool) { + if aVal.IsNil() { + if bVal.IsNil() { + return 0, true + } + return -1, true + } + if bVal.IsNil() { + return 1, true + } + return 0, false +} + +// floatCompare compares two floating-point values. NaNs compare low. +func floatCompare(a, b float64) int { + switch { + case isNaN(a): + return -1 // No good answer if b is a NaN so don't bother checking. + case isNaN(b): + return 1 + case a < b: + return -1 + case a > b: + return 1 + } + return 0 +} + +func isNaN(a float64) bool { + return a != a +} diff --git a/vendor/modules.txt b/vendor/modules.txt index feb5f9a..0480a48 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -20,6 +20,12 @@ github.com/google/go-github/v57/github # github.com/google/go-querystring v1.1.0 ## explicit; go 1.10 github.com/google/go-querystring/query +# github.com/kr/pretty v0.3.1 +## explicit; go 1.12 +github.com/kr/pretty +# github.com/kr/text v0.2.0 +## explicit +github.com/kr/text # github.com/mattn/go-colorable v0.1.13 ## explicit; go 1.15 github.com/mattn/go-colorable @@ -35,6 +41,9 @@ github.com/mitchellh/mapstructure # github.com/r3labs/sse/v2 v2.10.0 ## explicit; go 1.13 github.com/r3labs/sse/v2 +# github.com/rogpeppe/go-internal v1.9.0 +## explicit; go 1.17 +github.com/rogpeppe/go-internal/fmtsort # github.com/russross/blackfriday/v2 v2.1.0 ## explicit github.com/russross/blackfriday/v2