From 96070e93b31cdba55b5e5a9068aec4757dd517b9 Mon Sep 17 00:00:00 2001 From: Evgenii Shuvalov Date: Thu, 15 Aug 2024 11:02:41 +0300 Subject: [PATCH 1/2] iter14 features --- 1.txt | 1 + 2.txt | 1 + cmd/agent/main.go | 5 - internal/agent/agent.go | 89 +++++++++------ internal/agent/exporter.go | 23 +++- internal/agent/exporter_test.go | 4 +- internal/compression/compressor.go | 1 + internal/entities/errors.go | 2 + internal/entities/secret.go | 32 ++++++ .../{middleware.go => compression.go} | 51 --------- ...middleware_test.go => compression_test.go} | 22 ---- internal/middleware/request_logger.go | 57 ++++++++++ internal/middleware/request_logger_test.go | 26 +++++ internal/middleware/security.go | 104 ++++++++++++++++++ internal/middleware/security_test.go | 1 + internal/server/handlers_test.go | 2 +- internal/server/router.go | 11 ++ internal/server/server.go | 28 ++--- internal/services/signer.go | 49 +++++++++ internal/services/signer_test.go | 75 +++++++++++++ 20 files changed, 452 insertions(+), 132 deletions(-) create mode 100644 1.txt create mode 100644 2.txt create mode 100644 internal/entities/secret.go rename internal/middleware/{middleware.go => compression.go} (55%) rename internal/middleware/{middleware_test.go => compression_test.go} (89%) create mode 100644 internal/middleware/request_logger.go create mode 100644 internal/middleware/request_logger_test.go create mode 100644 internal/middleware/security.go create mode 100644 internal/middleware/security_test.go create mode 100644 internal/services/signer.go create mode 100644 internal/services/signer_test.go diff --git a/1.txt b/1.txt new file mode 100644 index 0000000..ebadb28 --- /dev/null +++ b/1.txt @@ -0,0 +1 @@ +[91 123 34 105 100 34 58 34 65 108 108 111 99 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 49 54 48 52 48 125 44 123 34 105 100 34 58 34 66 117 99 107 72 97 115 104 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 55 55 49 53 125 44 123 34 105 100 34 58 34 70 114 101 101 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 56 53 125 44 123 34 105 100 34 58 34 71 67 67 80 85 70 114 97 99 116 105 111 110 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 71 67 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 56 51 49 55 57 50 125 44 123 34 105 100 34 58 34 72 101 97 112 65 108 108 111 99 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 49 54 48 52 48 125 44 123 34 105 100 34 58 34 72 101 97 112 73 100 108 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 50 56 57 49 55 55 54 125 44 123 34 105 100 34 58 34 72 101 97 112 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 57 52 50 48 56 48 125 44 123 34 105 100 34 58 34 72 101 97 112 79 98 106 101 99 116 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 54 55 52 125 44 123 34 105 100 34 58 34 72 101 97 112 82 101 108 101 97 115 101 100 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 50 56 57 49 55 55 54 125 44 123 34 105 100 34 58 34 72 101 97 112 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 56 51 51 56 53 54 125 44 123 34 105 100 34 58 34 76 97 115 116 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 76 111 111 107 117 112 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 77 67 97 99 104 101 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 57 54 48 48 125 44 123 34 105 100 34 58 34 77 67 97 99 104 101 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 53 54 48 48 125 44 123 34 105 100 34 58 34 77 83 112 97 110 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 51 55 54 48 125 44 123 34 105 100 34 58 34 77 83 112 97 110 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 52 56 57 54 48 125 44 123 34 105 100 34 58 34 77 97 108 108 111 99 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 56 53 57 125 44 123 34 105 100 34 58 34 78 101 120 116 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 52 49 57 52 51 48 52 125 44 123 34 105 100 34 58 34 78 117 109 70 111 114 99 101 100 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 78 117 109 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 79 116 104 101 114 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 56 54 55 57 49 55 125 44 123 34 105 100 34 58 34 80 97 117 115 101 84 111 116 97 108 78 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 83 116 97 99 107 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 54 48 52 52 56 125 44 123 34 105 100 34 58 34 83 116 97 99 107 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 54 48 52 52 56 125 44 123 34 105 100 34 58 34 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 54 57 54 54 50 56 56 125 44 123 34 105 100 34 58 34 84 111 116 97 108 65 108 108 111 99 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 49 54 48 52 48 125 44 123 34 105 100 34 58 34 82 97 110 100 111 109 86 97 108 117 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 46 53 48 52 52 55 50 50 51 55 51 49 49 51 52 48 51 125 44 123 34 105 100 34 58 34 80 111 108 108 67 111 117 110 116 34 44 34 116 121 112 101 34 58 34 99 111 117 110 116 101 114 34 44 34 100 101 108 116 97 34 58 51 125 93] \ No newline at end of file diff --git a/2.txt b/2.txt new file mode 100644 index 0000000..ebadb28 --- /dev/null +++ b/2.txt @@ -0,0 +1 @@ +[91 123 34 105 100 34 58 34 65 108 108 111 99 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 49 54 48 52 48 125 44 123 34 105 100 34 58 34 66 117 99 107 72 97 115 104 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 55 55 49 53 125 44 123 34 105 100 34 58 34 70 114 101 101 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 56 53 125 44 123 34 105 100 34 58 34 71 67 67 80 85 70 114 97 99 116 105 111 110 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 71 67 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 56 51 49 55 57 50 125 44 123 34 105 100 34 58 34 72 101 97 112 65 108 108 111 99 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 49 54 48 52 48 125 44 123 34 105 100 34 58 34 72 101 97 112 73 100 108 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 50 56 57 49 55 55 54 125 44 123 34 105 100 34 58 34 72 101 97 112 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 57 52 50 48 56 48 125 44 123 34 105 100 34 58 34 72 101 97 112 79 98 106 101 99 116 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 54 55 52 125 44 123 34 105 100 34 58 34 72 101 97 112 82 101 108 101 97 115 101 100 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 50 56 57 49 55 55 54 125 44 123 34 105 100 34 58 34 72 101 97 112 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 56 51 51 56 53 54 125 44 123 34 105 100 34 58 34 76 97 115 116 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 76 111 111 107 117 112 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 77 67 97 99 104 101 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 57 54 48 48 125 44 123 34 105 100 34 58 34 77 67 97 99 104 101 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 53 54 48 48 125 44 123 34 105 100 34 58 34 77 83 112 97 110 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 51 55 54 48 125 44 123 34 105 100 34 58 34 77 83 112 97 110 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 52 56 57 54 48 125 44 123 34 105 100 34 58 34 77 97 108 108 111 99 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 49 56 53 57 125 44 123 34 105 100 34 58 34 78 101 120 116 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 52 49 57 52 51 48 52 125 44 123 34 105 100 34 58 34 78 117 109 70 111 114 99 101 100 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 78 117 109 71 67 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 79 116 104 101 114 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 56 54 55 57 49 55 125 44 123 34 105 100 34 58 34 80 97 117 115 101 84 111 116 97 108 78 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 125 44 123 34 105 100 34 58 34 83 116 97 99 107 73 110 117 115 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 54 48 52 52 56 125 44 123 34 105 100 34 58 34 83 116 97 99 107 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 54 48 52 52 56 125 44 123 34 105 100 34 58 34 83 121 115 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 54 57 54 54 50 56 56 125 44 123 34 105 100 34 58 34 84 111 116 97 108 65 108 108 111 99 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 51 49 54 48 52 48 125 44 123 34 105 100 34 58 34 82 97 110 100 111 109 86 97 108 117 101 34 44 34 116 121 112 101 34 58 34 103 97 117 103 101 34 44 34 118 97 108 117 101 34 58 48 46 53 48 52 52 55 50 50 51 55 51 49 49 51 52 48 51 125 44 123 34 105 100 34 58 34 80 111 108 108 67 111 117 110 116 34 44 34 116 121 112 101 34 58 34 99 111 117 110 116 101 114 34 44 34 100 101 108 116 97 34 58 51 125 93] \ No newline at end of file diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 87e3c3c..a29c041 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -15,10 +15,5 @@ func main() { logging.LogFatal(err) } - err = agnt.ParseFlags() - if err != nil { - logging.LogFatal(err) - } - agnt.Run() } diff --git a/internal/agent/agent.go b/internal/agent/agent.go index fd1bf67..cc27719 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -2,12 +2,15 @@ package agent import ( "fmt" + "net/http" + "strings" "sync" "time" "github.com/caarlos0/env/v11" "github.com/ex0rcist/metflix/internal/entities" "github.com/ex0rcist/metflix/internal/logging" + "github.com/ex0rcist/metflix/internal/services" "github.com/ex0rcist/metflix/internal/utils" "github.com/spf13/pflag" ) @@ -24,6 +27,7 @@ type Config struct { Address entities.Address `env:"ADDRESS"` PollInterval int `env:"POLL_INTERVAL"` ReportInterval int `env:"REPORT_INTERVAL"` + Secret entities.Secret `env:"KEY"` } func New() (*Agent, error) { @@ -33,41 +37,25 @@ func New() (*Agent, error) { ReportInterval: 10, } - stats := NewStats() - exporter := NewMetricsExporter(&config.Address, nil) + err := parseConfig(config) + if err != nil { + return nil, err + } + + var signer services.Signer + if len(config.Secret) > 0 { + signer = services.NewSignerService(config.Secret) + } + + exporter := NewMetricsExporter(&config.Address, http.DefaultTransport, signer) return &Agent{ Config: config, - Stats: stats, + Stats: NewStats(), Exporter: exporter, }, nil } -func (a *Agent) ParseFlags() error { - address := a.Config.Address - - pflag.VarP(&address, "address", "a", "address:port for HTTP API requests") - - pflag.IntVarP(&a.Config.PollInterval, "poll-interval", "p", a.Config.PollInterval, "interval (s) for polling stats") - pflag.IntVarP(&a.Config.ReportInterval, "report-interval", "r", a.Config.ReportInterval, "interval (s) for polling stats") - - pflag.Parse() - - // because VarP gets non-pointer value, set it manually - pflag.Visit(func(f *pflag.Flag) { - switch f.Name { - case "address": - a.Config.Address = address - } - }) - - if err := env.Parse(a.Config); err != nil { - return err - } - - return nil -} - func (a *Agent) Run() { logging.LogInfo(a.Config.String()) logging.LogInfo("agent ready") @@ -156,8 +144,45 @@ func (a *Agent) reportStats() { } func (c Config) String() string { - return fmt.Sprintf( - "agent config: address=%v; poll-interval=%v; report-interval=%v", - c.Address, c.PollInterval, c.ReportInterval, - ) + + str := []string{ + fmt.Sprintf("address=%s", c.Address), + fmt.Sprintf("poll-interval=%v", c.PollInterval), + fmt.Sprintf("report-interval=%v", c.ReportInterval), + } + + if len(c.Secret) > 0 { + str = append(str, fmt.Sprintf("secret=%v", c.Secret)) + } + + return "agent config: " + strings.Join(str, "; ") +} + +func parseConfig(config *Config) error { + address := config.Address + pflag.VarP(&address, "address", "a", "address:port for HTTP API requests") + + secret := config.Secret + pflag.VarP(&secret, "secret", "k", "a key to sign outgoing data") + + pflag.IntVarP(&config.PollInterval, "poll-interval", "p", config.PollInterval, "interval (s) for polling stats") + pflag.IntVarP(&config.ReportInterval, "report-interval", "r", config.ReportInterval, "interval (s) for polling stats") + + pflag.Parse() + + // because VarP gets non-pointer value, set it manually + pflag.Visit(func(f *pflag.Flag) { + switch f.Name { + case "address": + config.Address = address + case "secret": + config.Secret = secret + } + }) + + if err := env.Parse(config); err != nil { + return err + } + + return nil } diff --git a/internal/agent/exporter.go b/internal/agent/exporter.go index 536b298..805d1a7 100644 --- a/internal/agent/exporter.go +++ b/internal/agent/exporter.go @@ -6,12 +6,14 @@ import ( "fmt" "io" "net/http" + "strings" "time" "github.com/ex0rcist/metflix/internal/compression" "github.com/ex0rcist/metflix/internal/entities" "github.com/ex0rcist/metflix/internal/logging" "github.com/ex0rcist/metflix/internal/metrics" + "github.com/ex0rcist/metflix/internal/services" "github.com/ex0rcist/metflix/internal/utils" "github.com/rs/zerolog/log" ) @@ -26,16 +28,13 @@ type Exporter interface { type MetricsExporter struct { baseURL *entities.Address client *http.Client + signer services.Signer buffer []metrics.MetricExchange err error } -func NewMetricsExporter(baseURL *entities.Address, httpTransport http.RoundTripper) *MetricsExporter { - if httpTransport == nil { - httpTransport = http.DefaultTransport - } - +func NewMetricsExporter(baseURL *entities.Address, httpTransport http.RoundTripper, signer services.Signer) *MetricsExporter { client := &http.Client{ Timeout: 2 * time.Second, Transport: httpTransport, @@ -44,6 +43,7 @@ func NewMetricsExporter(baseURL *entities.Address, httpTransport http.RoundTripp return &MetricsExporter{ baseURL: baseURL, client: client, + signer: signer, } } @@ -128,6 +128,16 @@ func (e *MetricsExporter) doSend() error { req.Header.Set("Content-Encoding", "gzip") req.Header.Set("X-Request-Id", requestID) + if e.signer != nil { + signature, err := e.signer.CalculateSignature(payload.Bytes()) + if err != nil { + logging.LogErrorCtx(ctx, entities.ErrMetricReport, "error during signing", err.Error()) + return err + } + + req.Header.Set("HashSHA256", signature) + } + logRequest(ctx, url, req.Header, body) resp, err := e.client.Do(req) @@ -146,7 +156,8 @@ func (e *MetricsExporter) doSend() error { logResponse(ctx, resp, respBody) if resp.StatusCode != http.StatusOK { - logging.LogErrorCtx(ctx, entities.ErrMetricReport, "error reporting stats", resp.Status, string(respBody)) + formatedBody := strings.ReplaceAll(string(respBody), "\n", "") + logging.LogErrorCtx(ctx, entities.ErrMetricReport, "error reporting stats", resp.Status, formatedBody) return err } diff --git a/internal/agent/exporter_test.go b/internal/agent/exporter_test.go index cceabae..ae3c840 100644 --- a/internal/agent/exporter_test.go +++ b/internal/agent/exporter_test.go @@ -26,7 +26,7 @@ func TestNewMetricsExporter(t *testing.T) { require.NotPanics(func() { address := entities.Address("localhost") - NewMetricsExporter(&address, nil) + NewMetricsExporter(&address, nil, nil) }) } @@ -58,7 +58,7 @@ func TestApiClientReport(t *testing.T) { address := entities.Address("localhost:8080") - api := NewMetricsExporter(&address, RoundTripFunc(rtf)) + api := NewMetricsExporter(&address, RoundTripFunc(rtf), nil) // TODO: add signer mock api.Add("test", metrics.Counter(42)) err := api.Send() require.NoError(t, err) diff --git a/internal/compression/compressor.go b/internal/compression/compressor.go index 6eaec34..f79c3d7 100644 --- a/internal/compression/compressor.go +++ b/internal/compression/compressor.go @@ -31,6 +31,7 @@ func NewCompressor(w http.ResponseWriter, ctx context.Context) *Compressor { func (c *Compressor) Write(resp []byte) (int, error) { contentType := c.Header().Get("Content-Type") + if _, ok := c.supportedContent[contentType]; !ok { logging.LogDebugCtx(c.context, "compression not supported for "+contentType) return c.ResponseWriter.Write(resp) diff --git a/internal/entities/errors.go b/internal/entities/errors.go index eb76b7d..1125380 100644 --- a/internal/entities/errors.go +++ b/internal/entities/errors.go @@ -25,6 +25,8 @@ var ( ErrEncodingInternal = errors.New("internal encoding error") ErrEncodingUnsupported = errors.New("requsted encoding is not supported") + ErrNoSignature = errors.New("no signature provided") + ErrUnexpected = errors.New("unexpected error") ) diff --git a/internal/entities/secret.go b/internal/entities/secret.go new file mode 100644 index 0000000..771fd99 --- /dev/null +++ b/internal/entities/secret.go @@ -0,0 +1,32 @@ +package entities + +import ( + "strings" + + "github.com/rs/zerolog/log" +) + +type Secret string + +func (s *Secret) Set(src string) error { + if len([]byte(src)) < 32 { + log.Warn().Msg("secret is too short") + } + + *s = Secret(src) + + return nil +} + +func (s Secret) Type() string { + return "string" +} + +func (s Secret) String() string { + if len(s) <= 2 { + return string(s) + } + + masked := strings.Repeat("*", len(s)-2) + return string(s[0]) + masked + string(s[len(s)-1]) +} diff --git a/internal/middleware/middleware.go b/internal/middleware/compression.go similarity index 55% rename from internal/middleware/middleware.go rename to internal/middleware/compression.go index 3ee22f5..1155c17 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/compression.go @@ -4,53 +4,12 @@ import ( "errors" "net/http" "strings" - "time" "github.com/ex0rcist/metflix/internal/compression" "github.com/ex0rcist/metflix/internal/entities" "github.com/ex0rcist/metflix/internal/logging" - "github.com/ex0rcist/metflix/internal/utils" - "github.com/go-chi/chi/middleware" - "github.com/rs/zerolog/log" ) -func RequestsLogger(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - requestID := findOrCreateRequestID(r) - - // setup child logger for middleware - logger := log.Logger.With(). - Str("rid", requestID). - Logger() - - // log started - logger.Info(). - Str("method", r.Method). - Str("url", r.URL.String()). - Str("remote-addr", r.RemoteAddr). // middleware.RealIP - Msg("Started") - - logger.Debug(). - Msgf("request: %s", utils.HeadersToStr(r.Header)) - - // execute - ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) - ctx := logger.WithContext(r.Context()) - next.ServeHTTP(ww, r.WithContext(ctx)) - - logger.Debug(). - Msgf("response: %s", utils.HeadersToStr(ww.Header())) - - // log completed - logger.Info(). - Float64("elapsed", time.Since(start).Seconds()). - Int("status", ww.Status()). - Int("size", ww.BytesWritten()). - Msg("Completed") - }) -} - func DecompressRequest(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -98,16 +57,6 @@ func CompressResponse(next http.Handler) http.Handler { }) } -func findOrCreateRequestID(r *http.Request) string { - requestID := r.Header.Get("X-Request-Id") - - if requestID == "" { - requestID = utils.GenerateRequestID() - } - - return requestID -} - func needGzipEncoding(r *http.Request) bool { if len(r.Header.Get("Accept-Encoding")) == 0 { return false diff --git a/internal/middleware/middleware_test.go b/internal/middleware/compression_test.go similarity index 89% rename from internal/middleware/middleware_test.go rename to internal/middleware/compression_test.go index c61105f..d0667e3 100644 --- a/internal/middleware/middleware_test.go +++ b/internal/middleware/compression_test.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "net/http/httptest" - "testing" ) @@ -165,27 +164,6 @@ func TestCompressResponse_NoCompressionRequested(t *testing.T) { } } -// findOrCreateRequestID tests -func TestFindOrCreateRequestID_ExistingID(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("X-Request-Id", "existing-id") - - requestID := findOrCreateRequestID(req) - if requestID != "existing-id" { - t.Fatalf("expected 'existing-id', got %s", requestID) - } -} - -func TestFindOrCreateRequestID_NewID(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - - requestID := findOrCreateRequestID(req) - if requestID == "" { - t.Fatalf("expected non-empty request ID") - } -} - -// needGzipEncoding tests func TestNeedGzipEncoding_Supported(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Accept-Encoding", "gzip") diff --git a/internal/middleware/request_logger.go b/internal/middleware/request_logger.go new file mode 100644 index 0000000..4a01f6f --- /dev/null +++ b/internal/middleware/request_logger.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "net/http" + "time" + + "github.com/ex0rcist/metflix/internal/utils" + "github.com/go-chi/chi/middleware" + "github.com/rs/zerolog/log" +) + +func RequestsLogger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + requestID := findOrCreateRequestID(r) + + // setup child logger for middleware + logger := log.Logger.With(). + Str("rid", requestID). + Logger() + + // log started + logger.Info(). + Str("method", r.Method). + Str("url", r.URL.String()). + Str("remote-addr", r.RemoteAddr). // middleware.RealIP + Msg("Started") + + logger.Debug(). + Msgf("request: %s", utils.HeadersToStr(r.Header)) + + // execute + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + ctx := logger.WithContext(r.Context()) + next.ServeHTTP(ww, r.WithContext(ctx)) + + logger.Debug(). + Msgf("response: %s", utils.HeadersToStr(ww.Header())) + + // log completed + logger.Info(). + Float64("elapsed", time.Since(start).Seconds()). + Int("status", ww.Status()). + Int("size", ww.BytesWritten()). + Msg("Completed") + }) +} + +func findOrCreateRequestID(r *http.Request) string { + requestID := r.Header.Get("X-Request-Id") + + if requestID == "" { + requestID = utils.GenerateRequestID() + } + + return requestID +} diff --git a/internal/middleware/request_logger_test.go b/internal/middleware/request_logger_test.go new file mode 100644 index 0000000..86d4c9b --- /dev/null +++ b/internal/middleware/request_logger_test.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestFindOrCreateRequestID_ExistingID(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("X-Request-Id", "existing-id") + + requestID := findOrCreateRequestID(req) + if requestID != "existing-id" { + t.Fatalf("expected 'existing-id', got %s", requestID) + } +} + +func TestFindOrCreateRequestID_NewID(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + + requestID := findOrCreateRequestID(req) + if requestID == "" { + t.Fatalf("expected non-empty request ID") + } +} diff --git a/internal/middleware/security.go b/internal/middleware/security.go new file mode 100644 index 0000000..d0e7835 --- /dev/null +++ b/internal/middleware/security.go @@ -0,0 +1,104 @@ +package middleware + +import ( + "bytes" + "fmt" + "io" + "net/http" + + "github.com/ex0rcist/metflix/internal/entities" + "github.com/ex0rcist/metflix/internal/logging" + "github.com/ex0rcist/metflix/internal/services" + "github.com/go-chi/chi/middleware" +) + +// CustomResponseWriter is a wrapper around http.ResponseWriter that captures the response body +type CustomResponseWriter struct { + http.ResponseWriter + body *bytes.Buffer +} + +func (w *CustomResponseWriter) Write(b []byte) (int, error) { + return w.body.Write(b) +} + +func SignResponse(next http.Handler, secret entities.Secret) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if len(secret) == 0 { // skip middleware entirely + next.ServeHTTP(w, r) + return + } + + // Wrap the ResponseWriter with chi's middleware.WrapResponseWriter + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + // Create a buffer to capture the response body + bodyBuffer := &bytes.Buffer{} + + // Create a custom ResponseWriter to capture the response body + crw := &CustomResponseWriter{ResponseWriter: ww, body: bodyBuffer} + + // Pass the custom ResponseWriter to the next handler + next.ServeHTTP(crw, r) + + signer := services.NewSignerService(secret) + signature, _ := signer.CalculateSignature(bodyBuffer.Bytes()) + + w.Header().Set("HashSHA256", signature) + + // Write the captured body to the original ResponseWriter + _, err := w.Write(bodyBuffer.Bytes()) + if err != nil { + logging.LogErrorCtx(ctx, fmt.Errorf("got empty signature for request")) + return + } + }) +} + +func CheckSignedRequest(next http.Handler, secret entities.Secret) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if len(secret) == 0 { // skip middleware entirely + next.ServeHTTP(w, r) + return + } + + protected := map[string]struct{}{"POST": {}, "PUT": {}, "PATCH": {}} + if _, ok := protected[r.Method]; !ok { + logging.LogDebugCtx(ctx, "no need to check sign for that method") + next.ServeHTTP(w, r) + return + } + + hash := r.Header.Get("HashSHA256") + if len(hash) == 0 { + logging.LogErrorCtx(ctx, fmt.Errorf("got empty signature for request")) + http.Error(w, "failed to verify signature", http.StatusBadRequest) + return + } + + bodyBytes, err := io.ReadAll(r.Body) + r.Body.Close() // must close + if err != nil { + logging.LogErrorCtx(ctx, fmt.Errorf("failed to read request body")) + http.Error(w, "failed to read request body", http.StatusInternalServerError) + return + } + + signer := services.NewSignerService(secret) + ok, _ := signer.VerifySignature(bodyBytes, hash) + if !ok { + logging.LogErrorCtx(ctx, fmt.Errorf("failed to verify request signature")) + http.Error(w, "Failed to verify signature", http.StatusBadRequest) + return + } + + logging.LogDebugCtx(ctx, "got correct signature") + + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + next.ServeHTTP(w, r) + }) +} diff --git a/internal/middleware/security_test.go b/internal/middleware/security_test.go new file mode 100644 index 0000000..c870d7c --- /dev/null +++ b/internal/middleware/security_test.go @@ -0,0 +1 @@ +package middleware diff --git a/internal/server/handlers_test.go b/internal/server/handlers_test.go index e4bc318..7ee6cd3 100644 --- a/internal/server/handlers_test.go +++ b/internal/server/handlers_test.go @@ -43,7 +43,7 @@ func createTestRouter() (http.Handler, *storage.ServiceMock, *services.PingerMoc sm := &storage.ServiceMock{} pm := &services.PingerMock{} - router := NewRouter(sm, pm) + router := NewRouter(sm, pm, entities.Secret("")) return router, sm, pm } diff --git a/internal/server/router.go b/internal/server/router.go index 8c39eae..5a9d536 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -6,6 +6,7 @@ import ( chimdlw "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" + "github.com/ex0rcist/metflix/internal/entities" "github.com/ex0rcist/metflix/internal/middleware" "github.com/ex0rcist/metflix/internal/services" "github.com/ex0rcist/metflix/internal/storage" @@ -14,6 +15,7 @@ import ( func NewRouter( storageService storage.StorageService, pingerService services.Pinger, + secret entities.Secret, ) http.Handler { router := chi.NewRouter() @@ -21,9 +23,18 @@ func NewRouter( router.Use(chimdlw.StripSlashes) router.Use(middleware.RequestsLogger) + + router.Use(func(next http.Handler) http.Handler { + return middleware.CheckSignedRequest(next, secret) + }) + router.Use(middleware.DecompressRequest) router.Use(middleware.CompressResponse) + router.Use(func(next http.Handler) http.Handler { + return middleware.SignResponse(next, secret) + }) + router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) // no default body })) diff --git a/internal/server/server.go b/internal/server/server.go index d0ee47e..1458df6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -27,6 +27,7 @@ type Config struct { StorePath string `env:"FILE_STORAGE_PATH"` RestoreOnStart bool `env:"RESTORE"` DatabaseDSN string `env:"DATABASE_DSN"` + Secret entities.Secret `env:"KEY"` } func New() (*Server, error) { @@ -49,7 +50,7 @@ func New() (*Server, error) { storageService := storage.NewService(dataStorage) pingerService := services.NewPingerService(dataStorage) - router := NewRouter(storageService, pingerService) + router := NewRouter(storageService, pingerService, config.Secret) httpServer := &http.Server{ Addr: config.Address.String(), @@ -89,6 +90,10 @@ func (s *Server) String() string { str = append(str, fmt.Sprintf("database=%s", s.config.DatabaseDSN)) } + if len(s.config.Secret) > 0 { + str = append(str, fmt.Sprintf("secret=%s", s.config.Secret)) + } + return "server config: " + strings.Join(str, "; ") } @@ -112,11 +117,14 @@ func parseFlags(config *Config, progname string, args []string) error { address := config.Address flags.VarP(&address, "address", "a", "address:port for HTTP API requests") + secret := config.Secret + flags.VarP(&secret, "secret", "k", "a key to sign outgoing data") + // define flags - storeInterval := flags.IntP("store-interval", "i", config.StoreInterval, "interval (s) for dumping metrics to the disk, zero value means saving after each request") - storePath := flags.StringP("store-file", "f", config.StorePath, "path to file to store metrics") - restoreOnStart := flags.BoolP("restore", "r", config.RestoreOnStart, "whether to restore state on startup") - databaseDSN := flags.StringP("database", "d", config.DatabaseDSN, "PostgreSQL database DSN") + flags.IntVarP(&config.StoreInterval, "store-interval", "i", config.StoreInterval, "interval (s) for dumping metrics to the disk, zero value means saving after each request") + flags.StringVarP(&config.StorePath, "store-file", "f", config.StorePath, "path to file to store metrics") + flags.BoolVarP(&config.RestoreOnStart, "restore", "r", config.RestoreOnStart, "whether to restore state on startup") + flags.StringVarP(&config.DatabaseDSN, "database", "d", config.DatabaseDSN, "PostgreSQL database DSN") err := flags.Parse(args) if err != nil { @@ -128,14 +136,8 @@ func parseFlags(config *Config, progname string, args []string) error { switch f.Name { case "address": config.Address = address - case "store-interval": - config.StoreInterval = *storeInterval - case "store-file": - config.StorePath = *storePath - case "restore": - config.RestoreOnStart = *restoreOnStart - case "database": - config.DatabaseDSN = *databaseDSN + case "secret": + config.Secret = secret } }) diff --git a/internal/services/signer.go b/internal/services/signer.go new file mode 100644 index 0000000..7df0269 --- /dev/null +++ b/internal/services/signer.go @@ -0,0 +1,49 @@ +package services + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + + "github.com/ex0rcist/metflix/internal/entities" +) + +var _ Signer = SignerService{} + +type Signer interface { + CalculateSignature(data []byte) (string, error) + VerifySignature(data []byte, hash string) (bool, error) +} + +type SignerService struct { + secret []byte +} + +func NewSignerService(secret entities.Secret) SignerService { + return SignerService{secret: []byte(secret)} +} + +func (s SignerService) CalculateSignature(data []byte) (string, error) { + mac := hmac.New(sha256.New, s.secret) + + _, err := mac.Write(data) + if err != nil { + return "", err + } + digest := mac.Sum(nil) + + return hex.EncodeToString(digest), nil +} + +func (s SignerService) VerifySignature(data []byte, hash string) (bool, error) { + if len(hash) == 0 { + return false, entities.ErrNoSignature + } + + expected, err := s.CalculateSignature(data) + if err != nil { + return false, err + } + + return expected == hash, nil +} diff --git a/internal/services/signer_test.go b/internal/services/signer_test.go new file mode 100644 index 0000000..13fbe67 --- /dev/null +++ b/internal/services/signer_test.go @@ -0,0 +1,75 @@ +package services + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +const secret = "abc" + +func TestCalculateSignature(t *testing.T) { + tests := []struct { + name string + data []byte + err error + expected string + }{ + { + name: "Sign successful", + data: []byte("test data"), + err: nil, + expected: "81173bfb4fd8a1691cf591b6fd17e72087d7efa1861227c10e8a493ec8d8c4f0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + signer := NewSignerService(secret) + + hash, err := signer.CalculateSignature(tt.data) + + require.ErrorIs(err, tt.err) + require.Equal(tt.expected, hash) + }) + } +} + +func TestVerifySignature(t *testing.T) { + tests := []struct { + name string + data []byte + hash string + valid bool + err error + }{ + { + name: "Verify signature", + data: []byte("test data"), + hash: "81173bfb4fd8a1691cf591b6fd17e72087d7efa1861227c10e8a493ec8d8c4f0", + valid: true, + err: nil, + }, + + { + name: "Bad signature", + data: []byte("test data"), + hash: "81173bfb4fd8a1691cf591b6fd17e72087d7efa1861227c10e8a493ec8d8c4f1", + valid: false, + err: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + signer := NewSignerService(secret) + + valid, err := signer.VerifySignature(tt.data, tt.hash) + + require.ErrorIs(err, tt.err) + require.Equal(tt.valid, valid) + }) + } +} From 4bc0c16f1fbb4a24530e71bfba1eca69dbf9f4b3 Mon Sep 17 00:00:00 2001 From: Evgenii Shuvalov Date: Wed, 21 Aug 2024 23:28:42 +0300 Subject: [PATCH 2/2] iter14 fixes --- internal/middleware/security.go | 14 +-- internal/middleware/security_test.go | 158 +++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 7 deletions(-) diff --git a/internal/middleware/security.go b/internal/middleware/security.go index d0e7835..df7fbd3 100644 --- a/internal/middleware/security.go +++ b/internal/middleware/security.go @@ -31,16 +31,16 @@ func SignResponse(next http.Handler, secret entities.Secret) http.Handler { return } - // Wrap the ResponseWriter with chi's middleware.WrapResponseWriter + // wrap the ResponseWriter with chi's middleware.WrapResponseWriter ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) - // Create a buffer to capture the response body + // create a buffer to capture the response body bodyBuffer := &bytes.Buffer{} - // Create a custom ResponseWriter to capture the response body + // create a custom ResponseWriter to capture the response body crw := &CustomResponseWriter{ResponseWriter: ww, body: bodyBuffer} - // Pass the custom ResponseWriter to the next handler + // pass the custom ResponseWriter to the next handler next.ServeHTTP(crw, r) signer := services.NewSignerService(secret) @@ -48,7 +48,7 @@ func SignResponse(next http.Handler, secret entities.Secret) http.Handler { w.Header().Set("HashSHA256", signature) - // Write the captured body to the original ResponseWriter + // write the captured body to the original ResponseWriter _, err := w.Write(bodyBuffer.Bytes()) if err != nil { logging.LogErrorCtx(ctx, fmt.Errorf("got empty signature for request")) @@ -75,8 +75,8 @@ func CheckSignedRequest(next http.Handler, secret entities.Secret) http.Handler hash := r.Header.Get("HashSHA256") if len(hash) == 0 { - logging.LogErrorCtx(ctx, fmt.Errorf("got empty signature for request")) - http.Error(w, "failed to verify signature", http.StatusBadRequest) + // just pass it through for backward compatibility + next.ServeHTTP(w, r) return } diff --git a/internal/middleware/security_test.go b/internal/middleware/security_test.go index c870d7c..24ce616 100644 --- a/internal/middleware/security_test.go +++ b/internal/middleware/security_test.go @@ -1 +1,159 @@ package middleware + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ex0rcist/metflix/internal/entities" + "github.com/ex0rcist/metflix/internal/services" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type MockSignerService struct { + mock.Mock +} + +func (m *MockSignerService) CalculateSignature(data []byte) (string, error) { + args := m.Called(data) + return args.String(0), args.Error(1) +} + +func (m *MockSignerService) VerifySignature(data []byte, signature string) (bool, error) { + args := m.Called(data, signature) + return args.Bool(0), args.Error(1) +} + +func TestSignResponseMiddleware(t *testing.T) { + secret := entities.Secret("my-secret-key") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("test response")) + require.NoError(t, err) + }) + + signedHandler := SignResponse(handler, secret) + + req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + rr := httptest.NewRecorder() + + signedHandler.ServeHTTP(rr, req) + + result := rr.Result() + defer result.Body.Close() + + hash := result.Header.Get("HashSHA256") + assert.NotEmpty(t, hash) + + body, _ := io.ReadAll(result.Body) + assert.Equal(t, "test response", string(body)) +} + +func TestSignResponseMiddlewareWithoutSecret(t *testing.T) { + secret := entities.Secret("") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("test response")) + require.NoError(t, err) + }) + + signedHandler := SignResponse(handler, secret) + + req := httptest.NewRequest(http.MethodGet, "http://example.com", nil) + rr := httptest.NewRecorder() + + signedHandler.ServeHTTP(rr, req) + + result := rr.Result() + defer result.Body.Close() + + hash := result.Header.Get("HashSHA256") + assert.Empty(t, hash) + + body, _ := io.ReadAll(result.Body) + assert.Equal(t, "test response", string(body)) +} + +func TestCheckSignedRequestMiddleware(t *testing.T) { + secret := entities.Secret("my-secret-key") + signer := services.NewSignerService(secret) + body := []byte("test request") + + signature, err := signer.CalculateSignature(body) + require.NoError(t, err) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("ok")) + require.NoError(t, err) + }) + + checkSignedHandler := CheckSignedRequest(handler, secret) + + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewReader(body)) + req.Header.Set("HashSHA256", signature) + + rr := httptest.NewRecorder() + checkSignedHandler.ServeHTTP(rr, req) + + result := rr.Result() + defer result.Body.Close() + + assert.Equal(t, http.StatusOK, result.StatusCode) + + respBody, _ := io.ReadAll(result.Body) + assert.Equal(t, "ok", string(respBody)) +} + +func TestCheckSignedRequestMiddlewareInvalidSignature(t *testing.T) { + secret := entities.Secret("my-secret-key") + body := []byte("test request") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("ok")) + require.NoError(t, err) + }) + + checkSignedHandler := CheckSignedRequest(handler, secret) + + req := httptest.NewRequest(http.MethodPost, "http://example.com", bytes.NewReader(body)) + req.Header.Set("HashSHA256", "invalid-signature") + + rr := httptest.NewRecorder() + checkSignedHandler.ServeHTTP(rr, req) + + result := rr.Result() + defer result.Body.Close() + + assert.Equal(t, http.StatusBadRequest, result.StatusCode) + + respBody, _ := io.ReadAll(result.Body) + assert.Equal(t, "Failed to verify signature\n", string(respBody)) +} + +func TestCheckSignedRequestMiddlewareWithoutSecret(t *testing.T) { + secret := entities.Secret("") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte("ok")) + require.NoError(t, err) + }) + + checkSignedHandler := CheckSignedRequest(handler, secret) + + req := httptest.NewRequest(http.MethodPost, "http://example.com", nil) + + rr := httptest.NewRecorder() + checkSignedHandler.ServeHTTP(rr, req) + + result := rr.Result() + defer result.Body.Close() + + assert.Equal(t, http.StatusOK, result.StatusCode) + + respBody, _ := io.ReadAll(result.Body) + assert.Equal(t, "ok", string(respBody)) +}