From d9a6dfb3269c2feeb2c914c55876822da8aab178 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sat, 21 Oct 2023 20:57:45 +0300 Subject: [PATCH 01/55] =?UTF-8?q?feat:=20agent=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B0=D1=80=D0=B3=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20k=20=D0=B8=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/agent/config.go | 2 ++ cmd/agent/config_test.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/cmd/agent/config.go b/cmd/agent/config.go index 3e61290b..4130dc5b 100644 --- a/cmd/agent/config.go +++ b/cmd/agent/config.go @@ -15,6 +15,7 @@ type config struct { Addr string `env:"ADDRESS" flag:"~a" desc:"(строка) адрес сервера в формате host:port"` ReportInterval uint `env:"REPORT_INTERVAL" flag:"~r" desc:"(число, секунды) частота отправки данных на сервер"` PollingInterval uint `env:"POLLING_INTERVAL" flag:"~p" desc:"(число, секунды) частота отпроса метрик"` + Key string `env:"KEY" flag:"~p" desc:"(строка) секретный ключ подписи"` } var defaultConfig = config{ @@ -34,6 +35,7 @@ func mustConfigure(defaults config) (cfg config) { flag.UintVar(&cfg.PollingInterval, "p", defaults.PollingInterval, "число, частота опроса метрик") flag.UintVar(&cfg.ReportInterval, "r", defaults.ReportInterval, "число, частота отправки данных на сервер") flag.StringVar(&cfg.Addr, "a", defaults.Addr, "строка, адрес сервера в формате host:port") + flag.StringVar(&cfg.Key, "k", defaults.Key, "строка, секретный ключ подписи") flag.Parse() diff --git a/cmd/agent/config_test.go b/cmd/agent/config_test.go index 084fb423..492b3bd2 100644 --- a/cmd/agent/config_test.go +++ b/cmd/agent/config_test.go @@ -34,6 +34,20 @@ func Test_configure(t *testing.T) { commandLine: "agent -a localhost:8092", wantCfg: config{Addr: "localhost:8092", ReportInterval: 2, PollingInterval: 1}, }, + { + name: "Указать ключ через командную строку", + defaults: config{Key: "will be rewrited"}, + env: nil, + commandLine: "agent -k abcde", + wantCfg: config{Key: "abcde"}, + }, + { + name: "Указать ключ через переменные окружения", + defaults: config{Key: "will be rewrited"}, + env: map[string]string{"KEY": "qwerty"}, + commandLine: "agent -k abcde", + wantCfg: config{Key: "qwerty"}, + }, { name: "Указать интервалы через командную строку", defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, @@ -95,6 +109,7 @@ func Test_configure(t *testing.T) { os.Unsetenv("ADDRESS") os.Unsetenv("REPORT_INTERVAL") os.Unsetenv("POLLING_INTERVAL") + os.Unsetenv("KEY") if tt.env != nil { for k, v := range tt.env { From a0a122815b392936f5d1ae2f9bb95d7d32838a72 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 07:15:44 +0300 Subject: [PATCH 02/55] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B1=D0=B0=D0=B3=20retry=20=D1=81?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=BF=D1=83=D1=81=D0=BA=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=BE=D0=B2,=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D1=8B=20continue=20=D1=81=D1=82=D0=BE=D1=8F=D0=BB=20?= =?UTF-8?q?=D1=82=D0=B0=D0=BA,=20=D1=87=D1=82=D0=BE=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=85=D0=BE=D0=B4=D0=B8=D0=BB=D0=B8=20=D0=B7=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BC=D0=B5=D0=B6=D0=B4=D1=83=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=D0=B0=D0=BC=D0=B8,=20?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=81=D0=B0=D0=BC=D0=B0=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B5=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D0=BA=D0=B0=D0=BB=D0=B0=D1=81=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=20=D1=8D=D1=82=D0=BE=D0=BC=20=D0=BA=D0=BE=D0=BB=D0=BB?= =?UTF-8?q?=D0=B1=D0=B5=D0=BA=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D1=81=D1=8C=20=D0=B8=20=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B8=20=D0=B2=D1=8B=D0=B3=D0=BB=D1=8F=D0=B4=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/retry/do.go | 1 - lib/retry/retry_test.go | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/retry/do.go b/lib/retry/do.go index 38a24ac6..270f2d86 100644 --- a/lib/retry/do.go +++ b/lib/retry/do.go @@ -45,7 +45,6 @@ func This(f func() error, opts ...Option) error { for _, c := range options.callbacks { c(i, err) } - continue } err = f() diff --git a/lib/retry/retry_test.go b/lib/retry/retry_test.go index b839cdfc..edf6f6d6 100644 --- a/lib/retry/retry_test.go +++ b/lib/retry/retry_test.go @@ -60,3 +60,20 @@ func Test_Callbacks(t *testing.T) { assert.Equal(t, 3, counter, "Коллбеки должны были запуститься три раза") }) } + +func Test_CountOfRuns(t *testing.T) { + t.Run("четыре запуска, четыре прохода по сновной функции", func(t *testing.T) { + + counter := 0 + callee := func() error { + counter++ + return retry.Retriable(errors.New("test error")) + } + + retry.This(callee, + retry.Attempts(3), + retry.DelaySeconds(1, 1, 1)) + + assert.Equal(t, 4, counter, "Коллбеки должны были запуститься три раза") + }) +} From 4128aacdba596bf0ff1cf4c797af4ce3159f14fd Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 07:46:26 +0300 Subject: [PATCH 03/55] =?UTF-8?q?feat:=20internal=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=20sign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/sign/check.go | 6 +++++ internal/sign/headers.go | 3 +++ internal/sign/sign.go | 34 +++++++++++++++++++++++++ internal/sign/sign_test.go | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 internal/sign/check.go create mode 100644 internal/sign/headers.go create mode 100644 internal/sign/sign.go create mode 100644 internal/sign/sign_test.go diff --git a/internal/sign/check.go b/internal/sign/check.go new file mode 100644 index 00000000..1efbdf49 --- /dev/null +++ b/internal/sign/check.go @@ -0,0 +1,6 @@ +package sign + +// Check проверяет подпись +func Check() { + +} diff --git a/internal/sign/headers.go b/internal/sign/headers.go new file mode 100644 index 00000000..729ec1b3 --- /dev/null +++ b/internal/sign/headers.go @@ -0,0 +1,3 @@ +package sign + +const SignHeaderName = "HashSHA256" diff --git a/internal/sign/sign.go b/internal/sign/sign.go new file mode 100644 index 00000000..c0cf8fbc --- /dev/null +++ b/internal/sign/sign.go @@ -0,0 +1,34 @@ +package sign + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "hash" +) + +// Hasher задает используемую функцию хеширования, создается каждый +// раз новая под каждый запрос +var Hasher func() hash.Hash = sha256.New + +// Encoder будет переводить хеш в строку, по умолчанию исползуется base64 +var Encoder func([]byte) string = base64.StdEncoding.EncodeToString + +func Bytes(data []byte, key []byte) (string, error) { + h := hmac.New(Hasher, key) + _, err := h.Write(data) + if err != nil { + return "", err + } + + return Encoder(h.Sum(nil)), nil +} + +// TODO +// +// В целом можно было бы организовать как пакет base64, например: +// есть какой-то класс довольно универсальный, типа Signer +// но в реальности мы пользуемся его конкретной реализацией, например +// sign.Sha256.WithKey("") +// и можно передавать в настройках, например этот sign.Sha256, +// а может его даже можно создать как экземпляр вместе с ключом diff --git a/internal/sign/sign_test.go b/internal/sign/sign_test.go new file mode 100644 index 00000000..ac11fe31 --- /dev/null +++ b/internal/sign/sign_test.go @@ -0,0 +1,51 @@ +package sign_test + +import ( + "crypto/sha256" + "encoding/base64" + "hash" + "testing" + + "github.com/thefrol/kysh-kysh-meow/internal/sign" +) + +func TestBytes(t *testing.T) { + type args struct { + data []byte + key []byte + } + tests := []struct { + name string + hasher func() hash.Hash + encoder func([]byte) string + args args + want string + wantErr bool + }{ + { + name: "", + hasher: sha256.New, + encoder: base64.StdEncoding.EncodeToString, + args: args{data: []byte("Выпей ещё чайку"), key: []byte("с этими булочками")}, + want: "7RWbJONK8sV0rF6W4gJGIa2+Erh9szjEVuzFbDXtddk=", + wantErr: false, + }, + } + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + //зададим сначала энкодер и хешер + sign.Hasher = tt.hasher + sign.Encoder = tt.encoder + + got, err := sign.Bytes(tt.args.data, tt.args.key) + if (err != nil) != tt.wantErr { + t.Errorf("Bytes() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Bytes() = %v, want %v", got, tt.want) + } + }) + } +} From 8b8131ae257ee6da325632f8bb75c36408b995e3 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 07:47:20 +0300 Subject: [PATCH 04/55] =?UTF-8?q?feat:=20report=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D1=8B=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BC=D0=B8=D0=B4=D0=BB=D0=B2=D0=B0?= =?UTF-8?q?=D1=80=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/agent/agent.go | 4 +++ internal/report/middleware/signing.go | 35 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 internal/report/middleware/signing.go diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 78825f14..eca6739b 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -7,6 +7,7 @@ import ( "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/report" + reportmw "github.com/thefrol/kysh-kysh-meow/internal/report/middleware" "github.com/thefrol/kysh-kysh-meow/lib/scheduler" ) @@ -20,6 +21,9 @@ func main() { // в массив metrica.Metrica var s report.Stats + // + report.AddMiddleware(reportmw.Signing(config.Key)) + // запуск планировщика c := scheduler.New() //собираем данные раз в pollingInterval diff --git a/internal/report/middleware/signing.go b/internal/report/middleware/signing.go new file mode 100644 index 00000000..52a5c0a7 --- /dev/null +++ b/internal/report/middleware/signing.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "bytes" + "io" + + "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/sign" +) + +func Signing(key string) func(c *resty.Client, r *resty.Request) error { + return func(c *resty.Client, r *resty.Request) error { + br, ok := r.Body.(io.Reader) + if !ok { + log.Error().Msg("Не могу подписать данные, нужно чтобы в теле сообщение был ридер") + } + + data := make([]byte, 500) // todo придумать как тут указать хороший слайс + + n, err := br.Read(data) + if err != nil { + return nil + } + + s, err := sign.Bytes(data[:n], []byte(key)) + if err != nil { + return err + } + r.Header.Set(sign.SignHeaderName, s) + + r.SetBody(bytes.NewBuffer(data[:n])) + return nil + } +} From 14ef1668f191b419f45db2ea61a387d488de79a8 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 08:41:08 +0300 Subject: [PATCH 05/55] =?UTF-8?q?feat:=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B01=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=20preparedRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/send.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/report/send.go b/internal/report/send.go index 30c6a20a..7966bbf1 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -36,10 +36,11 @@ func Send(metricas []metrica.Metrica, url string) error { // у нас существует очень важный контракт, // что тело сюда передается в формате io.Reader, // тогда могут работать разные мидлвари + preparedRequest := defaultClient.R().SetBody(buf) var resp *resty.Response sendCall := func() error { var err error - resp, err = defaultClient.R().SetBody(buf).Post(url) + resp, err = preparedRequest.Post(url) return err } From ea403cebca0bc41a76b43dbb849c5742a76fffd4 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 09:07:53 +0300 Subject: [PATCH 06/55] =?UTF-8?q?fix:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D0=BC=D0=B8=D0=B4=D0=BB=D0=B2=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=20=D0=BD=D0=B0=20bytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/compress.go | 51 +++++++++++++++++-------------------- internal/report/send.go | 8 +++--- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/internal/report/compress.go b/internal/report/compress.go index 3960f5c7..1a5e3fd9 100644 --- a/internal/report/compress.go +++ b/internal/report/compress.go @@ -4,7 +4,6 @@ import ( "bytes" "compress/gzip" "fmt" - "io" "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" @@ -16,6 +15,19 @@ func init() { AddMiddleware(ApplyGZIP(compressMinLenght, gzip.BestCompression)) } +// Вообще рести не очень подходит под клиента тут, если я собираюсь использовать +// пакет retry вместо resty.WithRetry(), потому что поведение может +// быть довольно неожиданным. +// +// Например, если мы мидвари запускаем перед каждый запуском, а тело сообщения +// уже сформировано, то он может подписать сначала данные чистые, потом +// подпишет сжатые. +// +// Мне бы хотелось иметь такой пакет, где запрос составляется один раз, и далее +// без изменений отправляется. Или я должен тут мидлвари переписывать, или +// отказываться от мидлварей вообще, что тоже вариант, конечно. Или свой фреймворк +// мидлварей писать, чего я не хочу + func ApplyGZIP(minLenght int, level int) func(c *resty.Client, r *resty.Request) error { return func(c *resty.Client, r *resty.Request) error { // проверяем, что контент уже не закодирован каким-нибудт другим мидлварью или кодом @@ -24,56 +36,41 @@ func ApplyGZIP(minLenght int, level int) func(c *resty.Client, r *resty.Request) return nil } - v, ok := r.Body.(io.Reader) + v, ok := r.Body.([]byte) if !ok { - log.Warn().Str("location", "agent/middleware/gzip").Msg("Тело сообщение передано не в формате io.Reader, а значит мы не можем его заархивировать. Вообще мы хотим передавать тело сообщения именно ридером") + log.Warn().Str("location", "agent/middleware/gzip").Msg("Тело сообщение передано не в формате []byte, а значит мы не можем его заархивировать. Вообще мы хотим передавать тело сообщения именно массивом байт") return nil } - // теперь мы прочитаем minLenght байт из буфера - // Если тело к этому моменту закончится, то отправляем без сжатия - // А если нет, то пишем в gzip уже прочитанное, и все остальное - - t := make([]byte, minLenght) - n, _ := io.ReadAtLeast(v, t, minLenght) - if n < minLenght { - // не забудем обрезать буфер. Мы возьмем оттуда - // только первые n символов, потому что остальные будут просто нулями - // вплоть до minLenght - r.SetBody(bytes.NewBuffer(t[:n])) + // посмотрим, если нам стоит сжимать сообщение, проверим его длинну + if len(v) < minLenght { log.Info().Msg("Request too short for compressing, sending as is") return nil } // тут мы часть буфера прочитали, и хотим прочитать оставшееся, и скомпрессировать все вместе - b := new(bytes.Buffer) // todo мы можем писать сразу в тело сообщения я думаю при желании, возможноо.... И если не будем выводить инфу по сжатию + b := bytes.NewBuffer(make([]byte, 0, 500)) //todo нужна какая-то константа gz, err := gzip.NewWriterLevel(b, level) if err != nil { return fmt.Errorf("cant create compressor") } - bytesBefore := n // посчитаем успешность нашей компрессии, для этого запомним сколько байт было изначально - - _, err = gz.Write(t) + _, err = gz.Write(v) if err != nil { - return fmt.Errorf("cant write min lenght buffer back to zipper") + return fmt.Errorf("cant write to zip") } - nAfter, err := io.Copy(gz, v) - if err != nil { - return fmt.Errorf("cant write to gzip writer") - } - bytesBefore += int(nAfter) + gz.Close() r.Header.Add("Content-Encoding", "gzip") - r.SetBody(b) + r.SetBody(b.Bytes()) gz.Close() log.Info(). - Int("size_before", bytesBefore). + Int("size_before", len(v)). Int("size_after", b.Len()). - Float64("compression_ratio", float64(b.Len())/float64(bytesBefore)). + Float64("compression_ratio", float64(b.Len())/float64(len(v))). Msg("Компрессор закончил работать") return nil diff --git a/internal/report/send.go b/internal/report/send.go index 7966bbf1..603184a5 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -1,7 +1,6 @@ package report import ( - "bytes" "encoding/json" "errors" "fmt" @@ -27,16 +26,15 @@ var defaultClient = resty.New() // todo .SetJSONMarshaler(easyjson.Marshal()) // // При возникнвении ошибок возвращается только последняя func Send(metricas []metrica.Metrica, url string) error { - buf := new(bytes.Buffer) - err := json.NewEncoder(buf).Encode(metricas) + b, err := json.Marshal(metricas) if err != nil { log.Error().Str("location", "internal/report").Msgf("Не могу замаршалить массив метрик по приничине %+v", err) return err } // у нас существует очень важный контракт, - // что тело сюда передается в формате io.Reader, + // что тело сюда передается в формате []byte, // тогда могут работать разные мидлвари - preparedRequest := defaultClient.R().SetBody(buf) + preparedRequest := defaultClient.R().SetBody(b) var resp *resty.Response sendCall := func() error { var err error From 1a45660c5732d0dac254449193bb31390c417879 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 10:13:35 +0300 Subject: [PATCH 07/55] =?UTF-8?q?feat:=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=D1=81=D1=8F=20=D0=BE=D1=82=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B4=D0=BB=D0=B2=D0=B0=D1=80=D0=B5=D0=B9=20=D0=B2=20=D0=B0?= =?UTF-8?q?=D0=B3=D0=B5=D0=BD=D1=82=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D1=81=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=BE=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D1=8F,=20=D0=BA=D0=B0=D0=BA=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D1=8C.=20=D0=A2=D0=B0?= =?UTF-8?q?=D0=BA=20=D0=B1=D1=8B=D1=81=D1=82=D1=80=D0=B5=D0=B5,=20=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/agent/agent.go | 8 ++- internal/report/compress.go | 85 ++++++++------------------- internal/report/middleware/signing.go | 35 ----------- internal/report/send.go | 53 ++++++++++++----- internal/report/signing.go | 7 +++ 5 files changed, 74 insertions(+), 114 deletions(-) delete mode 100644 internal/report/middleware/signing.go create mode 100644 internal/report/signing.go diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index eca6739b..8271687e 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -1,13 +1,13 @@ package main import ( + "compress/gzip" "fmt" "path" "time" "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/report" - reportmw "github.com/thefrol/kysh-kysh-meow/internal/report/middleware" "github.com/thefrol/kysh-kysh-meow/lib/scheduler" ) @@ -21,8 +21,10 @@ func main() { // в массив metrica.Metrica var s report.Stats - // - report.AddMiddleware(reportmw.Signing(config.Key)) + // Настроим отправку + report.SetSigningKey(config.Key) + report.CompressLevel = gzip.BestCompression + report.CompressMinLenght = 100 // запуск планировщика c := scheduler.New() diff --git a/internal/report/compress.go b/internal/report/compress.go index 1a5e3fd9..22203b8a 100644 --- a/internal/report/compress.go +++ b/internal/report/compress.go @@ -5,74 +5,39 @@ import ( "compress/gzip" "fmt" - "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" ) -const compressMinLenght = 20 +var CompressMinLenght uint = 100 -func init() { - AddMiddleware(ApplyGZIP(compressMinLenght, gzip.BestCompression)) -} - -// Вообще рести не очень подходит под клиента тут, если я собираюсь использовать -// пакет retry вместо resty.WithRetry(), потому что поведение может -// быть довольно неожиданным. -// -// Например, если мы мидвари запускаем перед каждый запуском, а тело сообщения -// уже сформировано, то он может подписать сначала данные чистые, потом -// подпишет сжатые. -// -// Мне бы хотелось иметь такой пакет, где запрос составляется один раз, и далее -// без изменений отправляется. Или я должен тут мидлвари переписывать, или -// отказываться от мидлварей вообще, что тоже вариант, конечно. Или свой фреймворк -// мидлварей писать, чего я не хочу - -func ApplyGZIP(minLenght int, level int) func(c *resty.Client, r *resty.Request) error { - return func(c *resty.Client, r *resty.Request) error { - // проверяем, что контент уже не закодирован каким-нибудт другим мидлварью или кодом - if r.Header.Values("Content-Encoding") != nil { - log.Warn().Str("location", "agent/middleware/gzip").Fields(r.Header).Msg("Запрос на сервер уже сжат") - return nil - } - - v, ok := r.Body.([]byte) - if !ok { - log.Warn().Str("location", "agent/middleware/gzip").Msg("Тело сообщение передано не в формате []byte, а значит мы не можем его заархивировать. Вообще мы хотим передавать тело сообщения именно массивом байт") - return nil - } - - // посмотрим, если нам стоит сжимать сообщение, проверим его длинну - if len(v) < minLenght { - log.Info().Msg("Request too short for compressing, sending as is") - return nil - } +// CompressLevel устанавливает профиль сжатия gzip, по умолчанию равен +// gzip.BestCompression +var CompressLevel int = gzip.BestCompression - // тут мы часть буфера прочитали, и хотим прочитать оставшееся, и скомпрессировать все вместе +// compress возвращает сжатый массив байт +func compress(data []byte) ([]byte, error) { - b := bytes.NewBuffer(make([]byte, 0, 500)) //todo нужна какая-то константа - gz, err := gzip.NewWriterLevel(b, level) - if err != nil { - return fmt.Errorf("cant create compressor") - } - - _, err = gz.Write(v) - if err != nil { - return fmt.Errorf("cant write to zip") - } + b := bytes.NewBuffer(make([]byte, 0, 500)) //todo нужна какая-то константа + gz, err := gzip.NewWriterLevel(b, CompressLevel) + if err != nil { + return nil, fmt.Errorf("cant create compressor") + } - gz.Close() + _, err = gz.Write(data) + if err != nil { + return nil, fmt.Errorf("cant write to zip") + } - r.Header.Add("Content-Encoding", "gzip") - r.SetBody(b.Bytes()) - gz.Close() + err = gz.Close() + if err != nil { + return nil, fmt.Errorf("ошибка компрессии и закрытия компрессора") + } - log.Info(). - Int("size_before", len(v)). - Int("size_after", b.Len()). - Float64("compression_ratio", float64(b.Len())/float64(len(v))). - Msg("Компрессор закончил работать") + log.Info(). + Int("size_before", len(data)). + Int("size_after", b.Len()). + Float64("compression_ratio", float64(b.Len())/float64(len(data))). + Msg("Компрессор закончил работать") - return nil - } + return b.Bytes(), nil } diff --git a/internal/report/middleware/signing.go b/internal/report/middleware/signing.go deleted file mode 100644 index 52a5c0a7..00000000 --- a/internal/report/middleware/signing.go +++ /dev/null @@ -1,35 +0,0 @@ -package middleware - -import ( - "bytes" - "io" - - "github.com/go-resty/resty/v2" - "github.com/rs/zerolog/log" - "github.com/thefrol/kysh-kysh-meow/internal/sign" -) - -func Signing(key string) func(c *resty.Client, r *resty.Request) error { - return func(c *resty.Client, r *resty.Request) error { - br, ok := r.Body.(io.Reader) - if !ok { - log.Error().Msg("Не могу подписать данные, нужно чтобы в теле сообщение был ридер") - } - - data := make([]byte, 500) // todo придумать как тут указать хороший слайс - - n, err := br.Read(data) - if err != nil { - return nil - } - - s, err := sign.Bytes(data[:n], []byte(key)) - if err != nil { - return err - } - r.Header.Set(sign.SignHeaderName, s) - - r.SetBody(bytes.NewBuffer(data[:n])) - return nil - } -} diff --git a/internal/report/send.go b/internal/report/send.go index 603184a5..558b26f4 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -9,6 +9,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/metrica" + "github.com/thefrol/kysh-kysh-meow/internal/sign" "github.com/thefrol/kysh-kysh-meow/lib/retry" "github.com/thefrol/kysh-kysh-meow/lib/retry/fails" ) @@ -26,15 +27,46 @@ var defaultClient = resty.New() // todo .SetJSONMarshaler(easyjson.Marshal()) // // При возникнвении ошибок возвращается только последняя func Send(metricas []metrica.Metrica, url string) error { + /* + 1. Замаршалить метрики в b + 2. Скомпрессировать полученные данные и заменить b, + установить заголовок Content-Encoding + 3. Создать подпись на основе b, установить заголовок Sha256 + 4. Попробовать отправить preparedRequest, если не получится, то ничего страшного + 5. Если получилось, обнуляем pollCounter + */ + preparedRequest := defaultClient.R() + + var b []byte // тут будет тело, которое в итоге запишем в сообщение + b, err := json.Marshal(metricas) if err != nil { log.Error().Str("location", "internal/report").Msgf("Не могу замаршалить массив метрик по приничине %+v", err) return err } - // у нас существует очень важный контракт, - // что тело сюда передается в формате []byte, - // тогда могут работать разные мидлвари - preparedRequest := defaultClient.R().SetBody(b) + + if len(b) >= int(CompressMinLenght) { + b, err = compress(b) + if err != nil { + return fmt.Errorf("ошибка компрессии: %w", err) + } + preparedRequest.Header.Set("Content-Encoding", "gzip") + } + + if len(signingKey) != 0 { + s, err := sign.Bytes(b, []byte(signingKey)) + if err != nil { + return fmt.Errorf("ошибка подписывания: %w", err) + } + preparedRequest.Header.Set(sign.SignHeaderName, s) + log.Info().Str("sign", s).Msg("Тело сообщения подписано") + + // мда, канеш цену за отсуствие мидлвари приходится платить + // в таких не вполне очевидных ветвлениях + } + + // подготавливаем запрос, в который теперь не будут вмешиваться мидвари + preparedRequest.SetBody(b) var resp *resty.Response sendCall := func() error { var err error @@ -59,7 +91,7 @@ func Send(metricas []metrica.Metrica, url string) error { } defer resp.RawBody().Close() - log.Info().Str("location", "internal/report").Msgf("Метрики отправлены. Статус ответа %v, размер %v", resp.StatusCode(), resp.Size()) + log.Info().Str("location", "internal/report").Msgf("Метрики отправлены. Статус ответа %v, размер ответа %v", resp.StatusCode(), resp.Size()) if resp.StatusCode() != http.StatusOK { log.Info(). @@ -75,14 +107,3 @@ func Send(metricas []metrica.Metrica, url string) error { return nil } - -// AddMiddleware встраивает мидлварь в цепочку отправки сообщений. Все обработчики получают доступ -// к рести клиенту и текущему подготавливаемому запросу. Таким образом можно сделать дополнительное поггирование, -// или сжатие -// -// пример: report.AddMiddleware(GZIP) -func AddMiddleware(middlewares ...func(c *resty.Client, r *resty.Request) error) { - for _, m := range middlewares { - defaultClient.OnBeforeRequest(m) - } -} diff --git a/internal/report/signing.go b/internal/report/signing.go new file mode 100644 index 00000000..0a5da884 --- /dev/null +++ b/internal/report/signing.go @@ -0,0 +1,7 @@ +package report + +var signingKey []byte + +func SetSigningKey(key string) { + signingKey = []byte(key) +} From b1b514c85853a8344fce7ce7d63e9760e7ae53ca Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 10:59:41 +0300 Subject: [PATCH 08/55] =?UTF-8?q?fix:=20report=20=D0=B2=D1=8B=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=D1=82=D1=8C=20pollCount=20=D0=B2=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/fetch.go | 5 +++-- .../report/internal/pollcount/pollcount.go | 19 +++++++++++++++++++ internal/report/pollcount.go | 16 ---------------- internal/report/send.go | 3 ++- 4 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 internal/report/internal/pollcount/pollcount.go delete mode 100644 internal/report/pollcount.go diff --git a/internal/report/fetch.go b/internal/report/fetch.go index 5c0bffdd..f6940740 100644 --- a/internal/report/fetch.go +++ b/internal/report/fetch.go @@ -6,6 +6,7 @@ import ( "time" "github.com/thefrol/kysh-kysh-meow/internal/metrica" + "github.com/thefrol/kysh-kysh-meow/internal/report/internal/pollcount" ) // Fetch собирает метрики мамяти и сохраняет их во временное хранилище @@ -16,11 +17,11 @@ func Fetch() Stats { s := Stats{ memStats: &m, randomValue: randomGauge(), - pollCount: metrica.Counter(pollCount), + pollCount: metrica.Counter(pollcount.Get()), } // Добавить ко счетчику опросов - incrementPollCount() + pollcount.Increment() return s } diff --git a/internal/report/internal/pollcount/pollcount.go b/internal/report/internal/pollcount/pollcount.go new file mode 100644 index 00000000..c5fcbccd --- /dev/null +++ b/internal/report/internal/pollcount/pollcount.go @@ -0,0 +1,19 @@ +// Этот пакет содержит логику работы счетчика PollCount в агенте +// Он специально лежит в internal, чтобы нельзя было поменять из агента +// значение счетчика +package pollcount + +var pollCount int64 + +// Drop сбрасывает значение счетчика опросов памяти +func Drop() { + pollCount = 0 +} + +func Increment() { + pollCount += 1 +} + +func Get() int64 { + return pollCount +} diff --git a/internal/report/pollcount.go b/internal/report/pollcount.go deleted file mode 100644 index 18b6a687..00000000 --- a/internal/report/pollcount.go +++ /dev/null @@ -1,16 +0,0 @@ -package report - -import ( - "github.com/thefrol/kysh-kysh-meow/internal/metrica" -) - -var pollCount metrica.Counter - -// DropPoll сбрасывает значение счетчика опросов памяти в указанном хранилище -// dropCounter сбраcывает счетчик -func dropPollCount() { - pollCount = 0 // todo поллкаунт может стать вообще внутренней штукой модуля report если их объдинить, тогда агенту вообще не надо будет об этом париться -} -func incrementPollCount() { - pollCount += metrica.Counter(1) -} diff --git a/internal/report/send.go b/internal/report/send.go index 558b26f4..2e6f1514 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -9,6 +9,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/metrica" + "github.com/thefrol/kysh-kysh-meow/internal/report/internal/pollcount" "github.com/thefrol/kysh-kysh-meow/internal/sign" "github.com/thefrol/kysh-kysh-meow/lib/retry" "github.com/thefrol/kysh-kysh-meow/lib/retry/fails" @@ -103,7 +104,7 @@ func Send(metricas []metrica.Metrica, url string) error { } // Если сервер принял, то сбрасываем счетчик - dropPollCount() + pollcount.Drop() return nil } From 876996e7739980658e9a9c74b25778ee5548ae7e Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 11:17:55 +0300 Subject: [PATCH 09/55] =?UTF-8?q?feat:=20=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20compress=20=D0=B2=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/agent/agent.go | 6 +++--- internal/{report => compress}/compress.go | 20 ++++++++++---------- internal/compress/levels.go | 8 ++++++++ internal/report/docs.go | 2 -- internal/report/send.go | 21 +++++++++++++++++++-- internal/report/signing.go | 7 ------- 6 files changed, 40 insertions(+), 24 deletions(-) rename internal/{report => compress}/compress.go (64%) create mode 100644 internal/compress/levels.go delete mode 100644 internal/report/signing.go diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 8271687e..88821b03 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -1,12 +1,12 @@ package main import ( - "compress/gzip" "fmt" "path" "time" "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/compress" "github.com/thefrol/kysh-kysh-meow/internal/report" "github.com/thefrol/kysh-kysh-meow/lib/scheduler" ) @@ -23,8 +23,8 @@ func main() { // Настроим отправку report.SetSigningKey(config.Key) - report.CompressLevel = gzip.BestCompression - report.CompressMinLenght = 100 + report.CompressLevel = compress.BestCompression + report.CompressMinLength = 100 // запуск планировщика c := scheduler.New() diff --git a/internal/report/compress.go b/internal/compress/compress.go similarity index 64% rename from internal/report/compress.go rename to internal/compress/compress.go index 22203b8a..b49eeab6 100644 --- a/internal/report/compress.go +++ b/internal/compress/compress.go @@ -1,4 +1,4 @@ -package report +package compress import ( "bytes" @@ -8,17 +8,11 @@ import ( "github.com/rs/zerolog/log" ) -var CompressMinLenght uint = 100 - -// CompressLevel устанавливает профиль сжатия gzip, по умолчанию равен -// gzip.BestCompression -var CompressLevel int = gzip.BestCompression - -// compress возвращает сжатый массив байт -func compress(data []byte) ([]byte, error) { +// Bytes возвращает сжатый массив байт +func Bytes(data []byte, level int) ([]byte, error) { b := bytes.NewBuffer(make([]byte, 0, 500)) //todo нужна какая-то константа - gz, err := gzip.NewWriterLevel(b, CompressLevel) + gz, err := gzip.NewWriterLevel(b, level) if err != nil { return nil, fmt.Errorf("cant create compressor") } @@ -41,3 +35,9 @@ func compress(data []byte) ([]byte, error) { return b.Bytes(), nil } + +var () + +// TODO +// +// все вот это вот я бы сделал отдельным пакетом compress diff --git a/internal/compress/levels.go b/internal/compress/levels.go new file mode 100644 index 00000000..7be80400 --- /dev/null +++ b/internal/compress/levels.go @@ -0,0 +1,8 @@ +package compress + +import "compress/gzip" + +var ( + BestCompression = gzip.BestCompression + BestSpeed = gzip.BestSpeed +) diff --git a/internal/report/docs.go b/internal/report/docs.go index 0416b3ac..3d222b67 100644 --- a/internal/report/docs.go +++ b/internal/report/docs.go @@ -12,8 +12,6 @@ // stats собирает информацию о использовании памяти, и сохраняет в хранилище // так же имеет методы для управления счетчиком количества опросов PollCount // -// # AddMiddleware() - добавляет мидлварь для клиента, делающего HTTP запросы -// // Типичное использование: // // s := report.Fetch() diff --git a/internal/report/send.go b/internal/report/send.go index 2e6f1514..e02cfe85 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -8,6 +8,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/compress" "github.com/thefrol/kysh-kysh-meow/internal/metrica" "github.com/thefrol/kysh-kysh-meow/internal/report/internal/pollcount" "github.com/thefrol/kysh-kysh-meow/internal/sign" @@ -46,8 +47,8 @@ func Send(metricas []metrica.Metrica, url string) error { return err } - if len(b) >= int(CompressMinLenght) { - b, err = compress(b) + if len(b) >= int(CompressMinLength) { + b, err = compress.Bytes(b, CompressLevel) if err != nil { return fmt.Errorf("ошибка компрессии: %w", err) } @@ -108,3 +109,19 @@ func Send(metricas []metrica.Metrica, url string) error { return nil } + +// CompressLevel устанавливает минимальное число байт в теле сообения, после которого +// начинается комапрессия +var CompressMinLength uint = 100 + +// CompressLevel устанавливает профиль сжатия gzip, по умолчанию равен +// gzip.BestCompression +var CompressLevel int = compress.BestCompression + +// signingKey содержит ключ подписывания +var signingKey []byte + +// SetSigningKey устанавливает ключ подписывания для отправляемых запросов +func SetSigningKey(key string) { + signingKey = []byte(key) +} diff --git a/internal/report/signing.go b/internal/report/signing.go deleted file mode 100644 index 0a5da884..00000000 --- a/internal/report/signing.go +++ /dev/null @@ -1,7 +0,0 @@ -package report - -var signingKey []byte - -func SetSigningKey(key string) { - signingKey = []byte(key) -} From fcfbefcf63ec9b43322b46e34f98b9a46db447fa Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 11:20:15 +0300 Subject: [PATCH 10/55] =?UTF-8?q?fix:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/fetch.go | 7 +++++++ internal/report/names.go | 8 -------- 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 internal/report/names.go diff --git a/internal/report/fetch.go b/internal/report/fetch.go index f6940740..9df8f840 100644 --- a/internal/report/fetch.go +++ b/internal/report/fetch.go @@ -9,6 +9,13 @@ import ( "github.com/thefrol/kysh-kysh-meow/internal/report/internal/pollcount" ) +const ( + // название рамндомной метрики среди всех данных, что мы собираем + randomValueName = "RandomValue" + // Счетчик поличества опросов + metricPollCount = "PollCount" +) + // Fetch собирает метрики мамяти и сохраняет их во временное хранилище func Fetch() Stats { m := runtime.MemStats{} diff --git a/internal/report/names.go b/internal/report/names.go deleted file mode 100644 index 39b05c03..00000000 --- a/internal/report/names.go +++ /dev/null @@ -1,8 +0,0 @@ -package report - -const ( - // название рамндомной метрики среди всех данных, что мы собираем - randomValueName = "RandomValue" - // Счеткич поличества опросов - metricPollCount = "PollCount" -) From d461710c6ab0e150cd098ccb5c4f0fd2e0b23313 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 19:29:49 +0300 Subject: [PATCH 11/55] =?UTF-8?q?feat:=20sign=20=D1=80=D0=B5=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8E=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/sign/check.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/sign/check.go b/internal/sign/check.go index 1efbdf49..83859390 100644 --- a/internal/sign/check.go +++ b/internal/sign/check.go @@ -1,6 +1,32 @@ package sign +import ( + "bytes" + "crypto/hmac" + "encoding/base64" + "errors" + "fmt" +) + +var ErrorSignsNotEqual = errors.New("подписи не совпали") + // Check проверяет подпись -func Check() { +func Check(data []byte, key []byte, sign string) error { + h := hmac.New(Hasher, key) + _, err := h.Write(data) + if err != nil { + return fmt.Errorf("не могу записать хеш") + } + + s := h.Sum(nil) + + decodedSign, err := base64.StdEncoding.DecodeString(sign) + if err != nil { + return fmt.Errorf("не могу декодировать подпись") + } + if !bytes.Equal(s, decodedSign) { + return ErrorSignsNotEqual + } + return nil } From 08351ca1e3669656d57eb359d757fc57f8d077e7 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 19:38:16 +0300 Subject: [PATCH 12/55] =?UTF-8?q?feat:=20server=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B2=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D1=83=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/config.go | 2 ++ cmd/server/config_test.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/cmd/server/config.go b/cmd/server/config.go index 18c00728..f8731a1a 100644 --- a/cmd/server/config.go +++ b/cmd/server/config.go @@ -16,6 +16,7 @@ type config struct { FileStoragePath string `env:"FILE_STORAGE_PATH"` Restore bool `env:"RESTORE"` DatabaseDSN string `env:"DATABASE_DSN"` + Key string `env:"KEY"` } var defaultConfig = config{ @@ -38,6 +39,7 @@ func mustConfigure(defaults config) (cfg config) { flag.StringVar(&cfg.FileStoragePath, "f", defaults.FileStoragePath, "[строка] путь к файлу, откуда будут читаться при запуске и куда будут сохраняться метрики полученные сервером, если файл пустой, то сохранение будет отменено") flag.BoolVar(&cfg.Restore, "r", defaultConfig.Restore, "[флаг] если установлен, загружает из файла ранее записанные метрики") flag.StringVar(&cfg.DatabaseDSN, "d", defaults.DatabaseDSN, "[строка] подключения к базе данных") + flag.StringVar(&cfg.Key, "k", defaults.Key, "строка, секретный ключ подписи") flag.Parse() err := env.Parse(&cfg) diff --git a/cmd/server/config_test.go b/cmd/server/config_test.go index c6aabc10..e4b8d40c 100644 --- a/cmd/server/config_test.go +++ b/cmd/server/config_test.go @@ -48,6 +48,20 @@ func Test_configure(t *testing.T) { commandLine: "serv -r", wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, }, + { + name: "Указать ключ через командную строку", + defaults: config{Key: "will be rewrited"}, + env: nil, + commandLine: "serv -k abcde", + wantCfg: config{Key: "abcde", Restore: true}, + }, + { + name: "Указать ключ через переменные окружения", + defaults: config{Key: "will be rewrited"}, + env: map[string]string{"KEY": "qwerty"}, + commandLine: "serv -k abcde", + wantCfg: config{Key: "qwerty", Restore: true}, + }, { name: "командной строкой указать файл куда писать", defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, @@ -117,6 +131,7 @@ func Test_configure(t *testing.T) { os.Unsetenv("STORE_INTERVAL") os.Unsetenv("FILE_STORAGE_PATH") os.Unsetenv("RESTORE") + os.Unsetenv("KEY") if tt.env != nil { for k, v := range tt.env { From 28f7a7ee21c5e42e75a19e85c9b2c9ee5c27de29 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 19:39:08 +0300 Subject: [PATCH 13/55] =?UTF-8?q?feat:=20server/middleware=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BC=D0=B8=D0=B4=D0=BB?= =?UTF-8?q?=D0=B2=D0=B0=D1=80=D1=8C=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=8F=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/signing.go | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 internal/server/middleware/signing.go diff --git a/internal/server/middleware/signing.go b/internal/server/middleware/signing.go new file mode 100644 index 00000000..4137eecf --- /dev/null +++ b/internal/server/middleware/signing.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "bytes" + "io" + "net/http" + + "github.com/thefrol/kysh-kysh-meow/internal/server/api" + "github.com/thefrol/kysh-kysh-meow/internal/sign" +) + +func Signing(key string) func(http.Handler) http.Handler { + keyBytes := []byte(key) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedSign := r.Header.Get(sign.SignHeaderName) + if receivedSign == "" { + api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Нет подписи") + return + } + + if r.GetBody == nil { + buf := bytes.NewBuffer(make([]byte, 0, 500)) + _, err := io.Copy(buf, r.Body) + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "Cant replace request boby, signing failed") + return + } + r.Body.Close() + + r.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(buf.Bytes())), nil + } + r.Body, _ = r.GetBody() + } + body, err := r.GetBody() + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "Cant get request boby, signing check failed") + return + } + defer body.Close() + + data := make([]byte, 500) + n, err := body.Read(data) + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "cant read body") + return + } + + if err := sign.Check(data[:n], keyBytes, receivedSign); err != nil { + api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Подпись не прошла проверку") + return + } + + next.ServeHTTP(w, r) + }) + } +} From 754dab3fe4110b4afed3414c0fecd154507039a7 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Sun, 22 Oct 2023 19:39:41 +0300 Subject: [PATCH 14/55] =?UTF-8?q?feat:=20router=20=D0=9F=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=8F=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B8=20=D0=BC=D0=B8=D0=B4=D0=BB=D0=B2=D0=B0=D1=80=D1=8C?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/server.go | 2 +- internal/server/router/router.go | 5 ++++- internal/server/router/router_test.go | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index f2be7240..def0f355 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -57,7 +57,7 @@ func main() { func Run(cfg config, s api.Operator) { // Запускаем сервер с поддержкой нежного выключения // вдохноввлено примерами роутера chi - server := http.Server{Addr: cfg.Addr, Handler: router.MeowRouter(s)} + server := http.Server{Addr: cfg.Addr, Handler: router.MeowRouter(s, cfg.Key)} // Server run context serverCtx, serverStopCtx := context.WithCancel(context.Background()) diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 2f0cd979..2c233520 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -15,12 +15,15 @@ import ( // стилизованные ответы. // // на входе получает store - объект хранилища, операции над которым он будет проворачивать -func MeowRouter(store api.Operator) (router chi.Router) { +func MeowRouter(store api.Operator, key string) (router chi.Router) { router = chi.NewRouter() // настраиваем мидлвари, логгер, распаковщик и запаковщик router.Use(middleware.MeowLogging()) + if key != "" { + router.Use(middleware.Signing(key)) + } router.Use(middleware.UnGZIP) router.Use(middleware.GZIP(middleware.GZIPDefault)) diff --git a/internal/server/router/router_test.go b/internal/server/router/router_test.go index 5d8c7a45..9261309b 100644 --- a/internal/server/router/router_test.go +++ b/internal/server/router/router_test.go @@ -272,7 +272,7 @@ func Test_MeowRouter(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(MeowRouter(storage.AsOperator(storage.New()))) + server := httptest.NewServer(MeowRouter(storage.AsOperator(storage.New()), "")) defer server.Close() client := resty.New() for _, u := range tt.prePosts { @@ -368,7 +368,7 @@ func Test_updateCounter(t *testing.T) { r := httptest.NewRequest(tt.method, tt.route, nil) w := httptest.NewRecorder() - MeowRouter(storage.AsOperator(storage.New())).ServeHTTP(w, r) + MeowRouter(storage.AsOperator(storage.New()), "").ServeHTTP(w, r) result := w.Result() defer result.Body.Close() @@ -492,7 +492,7 @@ func Test_updateGauge(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(tt.method, tt.route, nil) w := httptest.NewRecorder() - MeowRouter(storage.AsOperator(storage.New())).ServeHTTP(w, r) + MeowRouter(storage.AsOperator(storage.New()), "").ServeHTTP(w, r) result := w.Result() defer result.Body.Close() @@ -564,7 +564,7 @@ func Test_updateUnknownType(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httptest.NewRequest(tt.method, tt.route, nil) w := httptest.NewRecorder() - MeowRouter(storage.AsOperator(storage.New())).ServeHTTP(w, r) + MeowRouter(storage.AsOperator(storage.New()), "").ServeHTTP(w, r) result := w.Result() defer result.Body.Close() From 23891d71ea44132993ff3420a3a9f8c9dd23c045 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 09:31:13 +0300 Subject: [PATCH 15/55] =?UTF-8?q?fix:=20report=20=D0=A3=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B1=D1=8B?= =?UTF-8?q?=D1=82=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D1=82=D0=B0=D0=B9=D0=BF=20=D0=B2=20=D0=B0=D0=B3=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/send.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/report/send.go b/internal/report/send.go index e02cfe85..dfe20d73 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -38,6 +38,7 @@ func Send(metricas []metrica.Metrica, url string) error { 5. Если получилось, обнуляем pollCounter */ preparedRequest := defaultClient.R() + preparedRequest.Header.Set("Content-Type", "application/json") var b []byte // тут будет тело, которое в итоге запишем в сообщение @@ -74,6 +75,14 @@ func Send(metricas []metrica.Metrica, url string) error { var err error resp, err = preparedRequest.Post(url) return err + // bug + // + // Есть небольшая проблема с этим resty: + // Если будет ошибка распаковки сообщени после gzip, + // то она звучит вот так "gzip: invalid header", а это + // значит что мы воспримем это как ошибку отправки. + // то есть мы даже не знаем получил ли метрики сервер, + // а ошибка звучит как ошибка отправки } // запустим отправку с тремя попытками дополнительными From 3ef3f6c648efcd4ffd94c3829eb1c7e2d71a5d93 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 09:35:44 +0300 Subject: [PATCH 16/55] =?UTF-8?q?fix:=20report=20=D0=BF=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BD=D0=B0=20=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B5=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B8=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/send.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/report/send.go b/internal/report/send.go index dfe20d73..136b7f30 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -17,7 +17,7 @@ import ( ) var ( - ErrorsServerError = errors.New("ошибка сервера") + ErrorRequestRejected = errors.New("Запрос не принят, статус код не 200") ) var defaultClient = resty.New() // todo .SetJSONMarshaler(easyjson.Marshal()) @@ -110,7 +110,7 @@ func Send(metricas []metrica.Metrica, url string) error { Str("server_response", string(resp.Body())). Msg("Метрики отправлены, но не получены.") - return fmt.Errorf("%w: сервер вернул %v: %v", ErrorsServerError, resp.StatusCode(), resp.Body()) + return fmt.Errorf("%w: сервер вернул %v: %v", ErrorRequestRejected, resp.StatusCode(), string(resp.Body())) } // Если сервер принял, то сбрасываем счетчик From 2a98ebc61277ab51f5e29532e6962eb6903d24a4 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 10:50:46 +0300 Subject: [PATCH 17/55] =?UTF-8?q?feat:=20=D0=92=D1=8B=D0=B2=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=B8=20=D0=B7=D0=B0=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D0=BA=D0=B5=20=D0=B0=D1=80=D0=B3=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=BD?= =?UTF-8?q?=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B2=20?= =?UTF-8?q?=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B5=20=D0=B8=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/agent/agent.go | 4 ++++ cmd/server/server.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 88821b03..9ac5dd9e 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -2,7 +2,9 @@ package main import ( "fmt" + "os" "path" + "strings" "time" "github.com/rs/zerolog/log" @@ -14,6 +16,8 @@ import ( const updateRoute = "/updates" func main() { + log.Info().Msgf("Агент запущен строкой %v", strings.Join(os.Args, " ")) + config := mustConfigure(defaultConfig) // Метрики собираются во временное хранилище s, diff --git a/cmd/server/server.go b/cmd/server/server.go index def0f355..ee7c8cad 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -21,6 +22,8 @@ import ( ) func main() { + log.Info().Msgf("Сервер запущен строкой %v", strings.Join(os.Args, " ")) + cfg := mustConfigure(defaultConfig) // создаем хранилище From 9dec4ae0c838ac0452e25837ee87479a0cb6dee3 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 10:53:18 +0300 Subject: [PATCH 18/55] =?UTF-8?q?fix:=20server=20=D0=B2=D1=8B=D0=B2=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D1=82-=D1=82=D0=B0=D0=B9=D0=BF=20=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/log.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index dd28449f..c10da28d 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -38,6 +38,7 @@ func MeowLogging() func(http.Handler) http.Handler { log.Info(). Int("statusCode", wr.statusCode). + Str("Content-Type", wr.Header().Get("Content-Type")). Int("size", wr.bytesWritten). // todo add gzipped response flag Msg("Response ->") @@ -68,7 +69,7 @@ type writerWrapper struct { originalWriter http.ResponseWriter bytesWritten int statusCode int -} +} // todo этот класс так или иначе используется в каждой мидлвари func (ww *writerWrapper) Header() http.Header { return ww.originalWriter.Header() @@ -84,7 +85,9 @@ func (ww *writerWrapper) WriteHeader(statusCode int) { // я кое-что узнал в перерыве, что после использования Write() // заголовки нельзя больше переписать даже при помощи WriteHeader() // поэтому проверяем - log.Error().Str("location", "logger middleware").Msg("Попытка записи заголовков после использования функции Write(). Заголовки и статус уже не изменить") + log.Error(). + Str("location", "logger middleware"). + Msg("Попытка записи заголовков после использования функции Write(). Заголовки и статус уже не изменить") // TODO // From 7b4ba7258c98843cb18fc3971ffa9d333caee4b5 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 10:53:43 +0300 Subject: [PATCH 19/55] =?UTF-8?q?feat:=20middlewares=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=8F=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/signing.go | 79 ++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/internal/server/middleware/signing.go b/internal/server/middleware/signing.go index 4137eecf..26f6da03 100644 --- a/internal/server/middleware/signing.go +++ b/internal/server/middleware/signing.go @@ -5,6 +5,7 @@ import ( "io" "net/http" + "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/server/api" "github.com/thefrol/kysh-kysh-meow/internal/sign" ) @@ -13,9 +14,15 @@ func Signing(key string) func(http.Handler) http.Handler { keyBytes := []byte(key) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") // todo костылек убери лол + receivedSign := r.Header.Get(sign.SignHeaderName) if receivedSign == "" { - api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Нет подписи") + //api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Нет подписи") + //w.WriteHeader(http.StatusBadRequest) + //w.Write([]byte("no sign")) + //log.Info().Msg("Нет подписи") + next.ServeHTTP(w, r) return } @@ -48,11 +55,77 @@ func Signing(key string) func(http.Handler) http.Handler { } if err := sign.Check(data[:n], keyBytes, receivedSign); err != nil { - api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Подпись не прошла проверку") + log.Info().Str("receivedSign", receivedSign).Msg("подпись не прошла проверку") + api.HTTPErrorWithLogging(w, http.StatusNotFound, "Подпись не прошла проверку") return } - next.ServeHTTP(w, r) + // теперь займемся запросом: + // обернем в перехватчик врайтер и подпишем ответ + fakew := SignInterceptor{ + WriteInterceptor: WriteInterceptor{ + origWriter: w, + buf: bytes.NewBuffer(data), + }, + key: keyBytes, + // todo переиспользуем наш буфер, а моем наверное целиком весь буфер если его почистить + } + + next.ServeHTTP(&fakew, r) + + // todo в целом мы могли бы вместо Close тут разобраться с перехваченными данными + // Это было чуть более переиспользуемо и наверное понятно + }) } } + +type WriteInterceptor struct { + statusCode int + origWriter http.ResponseWriter + buf *bytes.Buffer +} + +func (w *WriteInterceptor) WriteHeader(code int) { + w.statusCode = code +} + +func (w *WriteInterceptor) Header() http.Header { + return w.origWriter.Header() +} + +type SignInterceptor struct { + WriteInterceptor + key []byte +} + +func (w WriteInterceptor) Write(data []byte) (int, error) { + return w.buf.Write(data) +} + +// Close говорит о том, что сообщение готовится к отправке, значит +// можно установить нужные хедеры и отправлять +func (w WriteInterceptor) Close() { + if w.statusCode != 0 { + w.origWriter.WriteHeader(w.statusCode) + } + + _, err := io.Copy(w.origWriter, w.buf) + if err != nil { + log.Error().Msgf("copy a response to originalWriter: %v", err) + } +} + +func (w SignInterceptor) Close() { + s, err := sign.Bytes(w.buf.Bytes(), w.key) + if err != nil { + log.Error().Msgf("cant sign response: %v", err) + } + + // копируем все из временного хранилища по назначению + w.WriteInterceptor.Header().Set(sign.SignHeaderName, s) + log.Info().Str("sign", s).Msg("Запрос подписан") + + w.WriteInterceptor.Close() + +} From 36cc6122bb602d0d0620717444d1403e95dc79fc Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 10:53:57 +0300 Subject: [PATCH 20/55] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B8=D0=B4=D0=B5=D0=B8=20=D1=80=D0=B5?= =?UTF-8?q?=D1=84=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/gzip.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/server/middleware/gzip.go b/internal/server/middleware/gzip.go index 28aa37b2..23137154 100644 --- a/internal/server/middleware/gzip.go +++ b/internal/server/middleware/gzip.go @@ -77,7 +77,7 @@ type CompressedWriter struct { ignore bool // true - не будем сжимать checked bool // true- прошел все проверки по другим параментрам кроме минамальной длинны и собирается быть сжат -} +} // todo а этот класс я бы вынес в отдельный пакет наверн func NewCompressedWriter(originalWriter http.ResponseWriter, funcOpts ...gzipFuncOpt) *CompressedWriter { From 7faffc24fcc79e37a779694f6d84644120b75979 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 23 Oct 2023 11:47:06 +0300 Subject: [PATCH 21/55] =?UTF-8?q?fix:=20report=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit по просьбе go vet --- internal/report/send.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/report/send.go b/internal/report/send.go index 136b7f30..46fdd25a 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -17,7 +17,7 @@ import ( ) var ( - ErrorRequestRejected = errors.New("Запрос не принят, статус код не 200") + ErrorRequestRejected = errors.New("запрос не принят, статус код не 200") ) var defaultClient = resty.New() // todo .SetJSONMarshaler(easyjson.Marshal()) From 27ef2a88e0a909becdfb47db1fe48c3ebbc1c872 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 07:40:08 +0300 Subject: [PATCH 22/55] =?UTF-8?q?fix:=20middleware=20=D0=B2=D1=8B=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B8=D1=82=D1=8C=20=D0=B2=20=D0=BE=D1=82=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B0=D0=BA=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BE=D0=B1=D0=B5=D1=80=D1=82=D0=BA=D1=83=20=D0=B2?= =?UTF-8?q?=D1=80=D0=B0=D0=B9=D1=82=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/signing.go | 43 +++------------------- lib/intercept/docs.go | 4 +++ lib/intercept/responsewriter.go | 51 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 38 deletions(-) create mode 100644 lib/intercept/docs.go create mode 100644 lib/intercept/responsewriter.go diff --git a/internal/server/middleware/signing.go b/internal/server/middleware/signing.go index 26f6da03..d294e6c7 100644 --- a/internal/server/middleware/signing.go +++ b/internal/server/middleware/signing.go @@ -8,6 +8,7 @@ import ( "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/server/api" "github.com/thefrol/kysh-kysh-meow/internal/sign" + "github.com/thefrol/kysh-kysh-meow/lib/intercept" ) func Signing(key string) func(http.Handler) http.Handler { @@ -63,11 +64,8 @@ func Signing(key string) func(http.Handler) http.Handler { // теперь займемся запросом: // обернем в перехватчик врайтер и подпишем ответ fakew := SignInterceptor{ - WriteInterceptor: WriteInterceptor{ - origWriter: w, - buf: bytes.NewBuffer(data), - }, - key: keyBytes, + WriteInterceptor: intercept.New(w, data), // todo если буфер слишком маленький + key: keyBytes, // todo переиспользуем наш буфер, а моем наверное целиком весь буфер если его почистить } @@ -80,44 +78,13 @@ func Signing(key string) func(http.Handler) http.Handler { } } -type WriteInterceptor struct { - statusCode int - origWriter http.ResponseWriter - buf *bytes.Buffer -} - -func (w *WriteInterceptor) WriteHeader(code int) { - w.statusCode = code -} - -func (w *WriteInterceptor) Header() http.Header { - return w.origWriter.Header() -} - type SignInterceptor struct { - WriteInterceptor + intercept.WriteInterceptor key []byte } -func (w WriteInterceptor) Write(data []byte) (int, error) { - return w.buf.Write(data) -} - -// Close говорит о том, что сообщение готовится к отправке, значит -// можно установить нужные хедеры и отправлять -func (w WriteInterceptor) Close() { - if w.statusCode != 0 { - w.origWriter.WriteHeader(w.statusCode) - } - - _, err := io.Copy(w.origWriter, w.buf) - if err != nil { - log.Error().Msgf("copy a response to originalWriter: %v", err) - } -} - func (w SignInterceptor) Close() { - s, err := sign.Bytes(w.buf.Bytes(), w.key) + s, err := sign.Bytes(w.Buf().Bytes(), w.key) if err != nil { log.Error().Msgf("cant sign response: %v", err) } diff --git a/lib/intercept/docs.go b/lib/intercept/docs.go new file mode 100644 index 00000000..fedc2ae8 --- /dev/null +++ b/lib/intercept/docs.go @@ -0,0 +1,4 @@ +// Содержит разные полезные утилиты для работы с хендлерами +// и мидлварями, например оборачивался для ResponseWriter, +// или для повторного использования (Request).body +package intercept diff --git a/lib/intercept/responsewriter.go b/lib/intercept/responsewriter.go new file mode 100644 index 00000000..427fd319 --- /dev/null +++ b/lib/intercept/responsewriter.go @@ -0,0 +1,51 @@ +package intercept + +import ( + "bytes" + "io" + "net/http" + + "github.com/rs/zerolog/log" +) + +type WriteInterceptor struct { + statusCode int + origWriter http.ResponseWriter + buf *bytes.Buffer +} + +func New(w http.ResponseWriter, data []byte) WriteInterceptor { + return WriteInterceptor{ + origWriter: w, + buf: bytes.NewBuffer(data), + } +} + +func (w *WriteInterceptor) WriteHeader(code int) { + w.statusCode = code +} + +func (w *WriteInterceptor) Header() http.Header { + return w.origWriter.Header() +} + +func (w WriteInterceptor) Write(data []byte) (int, error) { + return w.buf.Write(data) +} + +// Close говорит о том, что сообщение готовится к отправке, значит +// можно установить нужные хедеры и отправлять +func (w WriteInterceptor) Close() { + if w.statusCode != 0 { + w.origWriter.WriteHeader(w.statusCode) + } + + _, err := io.Copy(w.origWriter, w.buf) + if err != nil { + log.Error().Msgf("copy a response to originalWriter: %v", err) + } +} + +func (w WriteInterceptor) Buf() *bytes.Buffer { + return w.buf +} From a60269157b29750827a6040812364653cf1ae39f Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 10:33:49 +0300 Subject: [PATCH 23/55] =?UTF-8?q?fix:=20interceptor=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D1=83=D0=BB=D1=8F=D1=82=D1=8C=20=D0=BC=D0=B0=D1=81=D1=81=D0=B8?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D1=83=D1=84=D0=B5=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/signing.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/server/middleware/signing.go b/internal/server/middleware/signing.go index d294e6c7..a79e3582 100644 --- a/internal/server/middleware/signing.go +++ b/internal/server/middleware/signing.go @@ -68,6 +68,7 @@ func Signing(key string) func(http.Handler) http.Handler { key: keyBytes, // todo переиспользуем наш буфер, а моем наверное целиком весь буфер если его почистить } + defer fakew.Close() next.ServeHTTP(&fakew, r) From 6c6a24d7b6a8404445e6ccacd10497becbd890f6 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 10:34:26 +0300 Subject: [PATCH 24/55] =?UTF-8?q?fix:=20middleware=20=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BE=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=BA=D1=83=20=D0=B2=D1=80=D0=B0=D0=B9=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/responsewriter.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/intercept/responsewriter.go b/lib/intercept/responsewriter.go index 427fd319..a8d1c6f0 100644 --- a/lib/intercept/responsewriter.go +++ b/lib/intercept/responsewriter.go @@ -8,6 +8,7 @@ import ( "github.com/rs/zerolog/log" ) +// Необходимо закрыть type WriteInterceptor struct { statusCode int origWriter http.ResponseWriter @@ -17,7 +18,7 @@ type WriteInterceptor struct { func New(w http.ResponseWriter, data []byte) WriteInterceptor { return WriteInterceptor{ origWriter: w, - buf: bytes.NewBuffer(data), + buf: bytes.NewBuffer(data[:0]), // обнуляем массив, иначе пишет в конец, а читать будет с начала, и в выход выйдет мусор, что бы уже в буфере } } From 54408aaa0bc42320bbb01bdabd36921314e591fa Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 13:57:47 +0300 Subject: [PATCH 25/55] =?UTF-8?q?fix:=20handlers=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B1=D0=B0=D0=B3=20?= =?UTF-8?q?=D1=81=20=D1=83=D0=BF=D1=83=D1=89=D0=B5=D0=BD=D0=BD=D1=8B=D0=BC?= =?UTF-8?q?=20return?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/api/url_handlers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/server/api/url_handlers.go b/internal/server/api/url_handlers.go index 4c45cbc1..e45076b5 100644 --- a/internal/server/api/url_handlers.go +++ b/internal/server/api/url_handlers.go @@ -34,6 +34,7 @@ func HandleURLRequest(op Operation) http.HandlerFunc { if err != nil { // todo вот этот код встречается в соседних обертках if errors.Is(err, ErrorDeltaEmpty) || errors.Is(err, ErrorValueEmpty) { HTTPErrorWithLogging(w, http.StatusBadRequest, "Ошибка входных данных: %v", err) + return } else if errors.Is(err, ErrorNotFoundMetric) { HTTPErrorWithLogging(w, http.StatusNotFound, "Не найдена метрика %v с именем %v", in.MType, in.ID) return From 4f9ff6c1b344ee8bce2097b3da605085adab589a Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 13:59:04 +0300 Subject: [PATCH 26/55] =?UTF-8?q?fix:=20middleware=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B2=D0=B5=D1=81=D1=82=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B3?= =?UTF-8?q?=D0=B5=D1=80=20=D0=BD=D0=B0=20intercept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/log.go | 27 +++++++++++++++++++++++---- lib/intercept/responsewriter.go | 11 +++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index c10da28d..667be8a7 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -1,10 +1,12 @@ package middleware import ( + "fmt" "net/http" "time" "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/lib/intercept" ) func MeowLogging() func(http.Handler) http.Handler { @@ -12,11 +14,28 @@ func MeowLogging() func(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d := time.Duration(0) - wr := wrapWriter(w) + wr := intercept.New(w, make([]byte, 0, 1024)) + //defer wr.Close() // очень показательная ошибка! + // тут мы в дефер передали wr со всеми старыми значениями + // wr.StatusCode и всяких полей. + // а по логике моей структурки, я записываю этот StatusCode + // в ответ, приложение потом запишет туда 500 + // но в close() будет звучать все равно исходный 0, + // который был там записан во время вызова defer + // + // тут конечно очень большое поле для выбора как это фиксить + // можно передавать в дефер замыкание, можно вызывать клоуз + // по ссылки и тогда передавать по ссылке объект в дефер + // + // я выбрал вариант где Close() имеет ресивер по указателю, + // собсно в этом вся беда и была + defer wr.Close() + d = countTime(func() { // запустить обработку - next.ServeHTTP(wr, r) + next.ServeHTTP(&wr, r) }) + fmt.Println(wr.StatusCode()) // TODO // Возможно в одном ответе мы можем передавать просто две структуры! и в сообщении какие-то самые важные моменты @@ -37,9 +56,9 @@ func MeowLogging() func(http.Handler) http.Handler { Msg("Request ->") log.Info(). - Int("statusCode", wr.statusCode). + Int("statusCode", wr.StatusCode()). Str("Content-Type", wr.Header().Get("Content-Type")). - Int("size", wr.bytesWritten). + Int("size", wr.Buf().Len()). // todo add gzipped response flag Msg("Response ->") }) diff --git a/lib/intercept/responsewriter.go b/lib/intercept/responsewriter.go index a8d1c6f0..1b713130 100644 --- a/lib/intercept/responsewriter.go +++ b/lib/intercept/responsewriter.go @@ -16,6 +16,7 @@ type WriteInterceptor struct { } func New(w http.ResponseWriter, data []byte) WriteInterceptor { + // todo дай обработку на nil return WriteInterceptor{ origWriter: w, buf: bytes.NewBuffer(data[:0]), // обнуляем массив, иначе пишет в конец, а читать будет с начала, и в выход выйдет мусор, что бы уже в буфере @@ -23,7 +24,9 @@ func New(w http.ResponseWriter, data []byte) WriteInterceptor { } func (w *WriteInterceptor) WriteHeader(code int) { + w.statusCode = code + log.Info().Msgf("%v %v", w.statusCode, w) } func (w *WriteInterceptor) Header() http.Header { @@ -36,8 +39,8 @@ func (w WriteInterceptor) Write(data []byte) (int, error) { // Close говорит о том, что сообщение готовится к отправке, значит // можно установить нужные хедеры и отправлять -func (w WriteInterceptor) Close() { - if w.statusCode != 0 { +func (w *WriteInterceptor) Close() { + if w.statusCode != 0 { // todo перенести логику в геттер w.origWriter.WriteHeader(w.statusCode) } @@ -50,3 +53,7 @@ func (w WriteInterceptor) Close() { func (w WriteInterceptor) Buf() *bytes.Buffer { return w.buf } + +func (w WriteInterceptor) StatusCode() int { + return w.statusCode +} From 1aa6d6fbaa93de35edb197306635922fe27a087e Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 14:11:31 +0300 Subject: [PATCH 27/55] =?UTF-8?q?feat:=20middleware=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B0=D0=B9=D1=82=D0=B5=D1=80-=D0=BE=D0=B1=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/log.go | 60 ------------------------------- 1 file changed, 60 deletions(-) diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index 667be8a7..e92bd17a 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "net/http" "time" @@ -35,7 +34,6 @@ func MeowLogging() func(http.Handler) http.Handler { // запустить обработку next.ServeHTTP(&wr, r) }) - fmt.Println(wr.StatusCode()) // TODO // Возможно в одном ответе мы можем передавать просто две структуры! и в сообщении какие-то самые важные моменты @@ -65,13 +63,6 @@ func MeowLogging() func(http.Handler) http.Handler { } } -// wrapWriter Оборачивает оригинальный http.ResponseWriter оболочкой writeWrapper, которая хранит -// некоторые данные о манипуляциях с этим врайтером. Например, какой статус код был записан, -// сколько данных записано в байтах -func wrapWriter(w http.ResponseWriter) *writerWrapper { - return &writerWrapper{originalWriter: w, statusCode: 200} -} - // countTime засекает время исполнения функции аргумента func countTime(f func()) (d time.Duration) { defer func() { @@ -81,54 +72,3 @@ func countTime(f func()) (d time.Duration) { return // хрена се, эта функция работает примерно в тысячу раз быстрее чем прошлая с указателем } - -// writeWrapper это оболочка для интерфейса http.ResponseWriter, которая хранит некоторые параметры о -// манипуляциях с ней: сколько было записино, и какие данные под конец, так же отслеживает чтобы итоговый статус код был правильно записан -type writerWrapper struct { - originalWriter http.ResponseWriter - bytesWritten int - statusCode int -} // todo этот класс так или иначе используется в каждой мидлвари - -func (ww *writerWrapper) Header() http.Header { - return ww.originalWriter.Header() -} -func (ww *writerWrapper) Write(bb []byte) (int, error) { - n, err := ww.originalWriter.Write(bb) - ww.bytesWritten += n - return n, err -} - -func (ww *writerWrapper) WriteHeader(statusCode int) { - if ww.bytesWritten > 0 { - // я кое-что узнал в перерыве, что после использования Write() - // заголовки нельзя больше переписать даже при помощи WriteHeader() - // поэтому проверяем - log.Error(). - Str("location", "logger middleware"). - Msg("Попытка записи заголовков после использования функции Write(). Заголовки и статус уже не изменить") - - // TODO - // - // На данный момент, при вызове http.Error(...), - // он успевает как-то и записать в тело, и только - // потом потом делает WriteHeader(), что по моим - // наблюдениям попросту невозможно. Из-за этого - // Появляются лишние сообщения об эшибках. В любом - // случае этим не логгер должен заниматься - } - - if statusCode == 0 { - log.Error().Str("location", "logger middleware").Msg("Кто-то пытается записать в ответ статус код 0, это ошибка и приведет к падению сервера") - } - if statusCode != 200 { - ww.originalWriter.WriteHeader(statusCode) - } - ww.statusCode = statusCode - -} - -// проверить что writeWrapper отвечает интерфейсу http.ResponseWriter -var _ http.ResponseWriter = (*writerWrapper)(nil) - -// тут по-хорошему сделать бы инъекцию зависимости и предоставить нужный журнал, но нормального интерфейса у Олологгера у нас нет From bde024ca5f56ef522da35629ea8f5d1380f0ca69 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 14:11:56 +0300 Subject: [PATCH 28/55] =?UTF-8?q?fix:=20intercept=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B5=D0=BD=D1=83=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/responsewriter.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/intercept/responsewriter.go b/lib/intercept/responsewriter.go index 1b713130..f1f2ec48 100644 --- a/lib/intercept/responsewriter.go +++ b/lib/intercept/responsewriter.go @@ -24,9 +24,7 @@ func New(w http.ResponseWriter, data []byte) WriteInterceptor { } func (w *WriteInterceptor) WriteHeader(code int) { - w.statusCode = code - log.Info().Msgf("%v %v", w.statusCode, w) } func (w *WriteInterceptor) Header() http.Header { @@ -55,5 +53,8 @@ func (w WriteInterceptor) Buf() *bytes.Buffer { } func (w WriteInterceptor) StatusCode() int { + if w.statusCode == 0 { + return 200 + } return w.statusCode } From 5c0f0221fc08592fb02b3a7313eec56c4a254dd1 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Tue, 24 Oct 2023 14:14:59 +0300 Subject: [PATCH 29/55] =?UTF-8?q?docs:=20=D1=82=D1=83=D0=B4=D1=83=D1=88?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/server.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/server/server.go b/cmd/server/server.go index ee7c8cad..113aa8b0 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -96,6 +96,11 @@ func Run(cfg config, s api.Operator) { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Error().Msgf("^0^ не могу запустить сервер: %v \n", err) + //todo + // + // если не биндится, то хотя бы выходить с ошибкой + // + // можно дать несколько попыток забиндиться } <-serverCtx.Done() From 6aaa76fe5cc5dd213ec10a377cc988ec93f29afc Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:15:02 +0300 Subject: [PATCH 30/55] =?UTF-8?q?feat:=20intercept=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B1=D1=83=D1=84=D0=B5=D1=80?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=80=D1=83=D1=8E=D1=89=D1=83=D1=8E=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=B5=D1=80=D1=82=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/buffered.go | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 lib/intercept/buffered.go diff --git a/lib/intercept/buffered.go b/lib/intercept/buffered.go new file mode 100644 index 00000000..d6722582 --- /dev/null +++ b/lib/intercept/buffered.go @@ -0,0 +1,54 @@ +package intercept + +import ( + "io" + "net/http" +) + +// Buffered перехватывает все данные, которые должны быть +// записаны в респонс врайтер, и хранит в памяти, а записывает +// в момент вызова Flush(). Обязательно в конце вызывать Flush(). +// +// Применяется, когда мы хотим получить тело сообщения, и после +// провести с ним некие процедуры, например подписать, или архивировать. +// Но если во врайтер уже что-то записано, то мы не сможем поменять заголовки или +// или выход, поэтому мы перехватываем записанные данные, и пишем их в конце +// +// Заголовки при этом не перехватываются, а записываются в оригинальный +// врайтер сразу +// +// поведение defer w.Flush() не проверено +type Buffered struct { + http.ResponseWriter + buf io.ReadWriter + code int +} + +// WithBuffer создает буферную оболочку для w, где в оригинальный +// врайтер иничего не будет записано до вызова Flush(), +// было бы правильно называть этот вызов Close() +func WithBuffer(w http.ResponseWriter, buf io.ReadWriter) *Buffered { + return &Buffered{ + ResponseWriter: w, + buf: buf, + } +} + +func (w *Buffered) Write(data []byte) (int, error) { + return w.buf.Write(data) +} + +func (w *Buffered) WriteHeader(code int) { + w.code = code +} + +// Flush записывает перехваченные данные, такие как код и тело ответа +// в оригинальный врайтер +func (w Buffered) Flush() error { + if w.code != 0 { + w.WriteHeader(w.code) + + } + _, err := io.Copy(w.ResponseWriter, w.buf) + return err +} From 955d0a74b3ce8946b27cd481e03c4f678e011d8c Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:15:36 +0300 Subject: [PATCH 31/55] =?UTF-8?q?feat:=20intercept=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BE=D0=B1=D0=BE=D0=BB=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D1=83-=D1=81=D1=87=D0=B8=D1=82=D0=B0=D0=BB=D0=BA?= =?UTF-8?q?=D1=83=20BytesCounter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/bytecounter.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 lib/intercept/bytecounter.go diff --git a/lib/intercept/bytecounter.go b/lib/intercept/bytecounter.go new file mode 100644 index 00000000..0d29e8f6 --- /dev/null +++ b/lib/intercept/bytecounter.go @@ -0,0 +1,32 @@ +package intercept + +import "net/http" + +type BytesCounter struct { + http.ResponseWriter + n int + code int +} + +func (w *BytesCounter) Write(data []byte) (int, error) { + n, err := w.ResponseWriter.Write(data) + w.n += n + return n, err +} + +func (w *BytesCounter) WriteHeader(code int) { + w.code = code + w.ResponseWriter.WriteHeader(code) +} + +func (w BytesCounter) BytesWritten() int { + return w.n +} + +func (w BytesCounter) StatusCode() int { + return w.code +} + +func WithBytesCounter(w http.ResponseWriter) *BytesCounter { + return &BytesCounter{ResponseWriter: w, code: 200} +} From e81dc1ea924d8e829b8cbf49612e1295802ce28a Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:16:38 +0300 Subject: [PATCH 32/55] =?UTF-8?q?fix:=20middleware=20=D0=9F=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D1=8C=D0=BD=D0=BE=20=D0=B6=D1=83=D1=80=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B2?= =?UTF-8?q?=D1=80=D0=B5=D0=BC=D1=8F=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/log.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index e92bd17a..3e129fef 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -65,9 +65,9 @@ func MeowLogging() func(http.Handler) http.Handler { // countTime засекает время исполнения функции аргумента func countTime(f func()) (d time.Duration) { - defer func() { - d = time.Since(time.Now()) - }() + defer func(t time.Time) { + d = time.Since(t) + }(time.Now()) f() return // хрена се, эта функция работает примерно в тысячу раз быстрее чем прошлая с указателем From 918cc0b23052f465772b49393b02f1422b9b5881 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:17:19 +0300 Subject: [PATCH 33/55] =?UTF-8?q?fix:=20middleware=20=D0=9E=D1=81=D0=B2?= =?UTF-8?q?=D0=B5=D0=B6=D0=B8=D1=82=D1=8C=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D1=80=D1=83=D1=8E=D1=89=D1=83=D1=8E=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B4=D0=BB=D0=B2=D0=B0=D1=80=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/log.go | 47 +++++++------------------------ 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index 3e129fef..2ae311f9 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -5,58 +5,31 @@ import ( "time" "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/sign" "github.com/thefrol/kysh-kysh-meow/lib/intercept" ) func MeowLogging() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - d := time.Duration(0) - wr := intercept.New(w, make([]byte, 0, 1024)) - //defer wr.Close() // очень показательная ошибка! - // тут мы в дефер передали wr со всеми старыми значениями - // wr.StatusCode и всяких полей. - // а по логике моей структурки, я записываю этот StatusCode - // в ответ, приложение потом запишет туда 500 - // но в close() будет звучать все равно исходный 0, - // который был там записан во время вызова defer - // - // тут конечно очень большое поле для выбора как это фиксить - // можно передавать в дефер замыкание, можно вызывать клоуз - // по ссылки и тогда передавать по ссылке объект в дефер - // - // я выбрал вариант где Close() имеет ресивер по указателю, - // собсно в этом вся беда и была - defer wr.Close() - - d = countTime(func() { + faker := intercept.WithBytesCounter(w) + d := countTime(func() { // запустить обработку - next.ServeHTTP(&wr, r) + next.ServeHTTP(faker, r) }) - - // TODO - // Возможно в одном ответе мы можем передавать просто две структуры! и в сообщении какие-то самые важные моменты - // - // Вообще мне не очень нравится такой формат, как в задании - // хотелось бы видеть - пришел запрос номер такой-то - // запрос такой-то - // тут сообщения от хендера посередине - // ответ такой-то - // Чтобы сообщения были как бы обернуты миддлеваром, вот начало вот конец. - // Или в контексте реквеста передается как-то логгер, куда может писать хендлер и тогда его сообщения - // будут как-то отдельно форматироваться log.Info(). Str("method", r.Method). Str("uri", r.RequestURI). Bool("gzippedRequest", encoded(r, "gzip")). - Dur("duration", d). + Str("Sign", r.Header.Get(sign.SignHeaderName)). // todo sign.HeaderName + Dur("Duration", d). Msg("Request ->") log.Info(). - Int("statusCode", wr.StatusCode()). - Str("Content-Type", wr.Header().Get("Content-Type")). - Int("size", wr.Buf().Len()). + Int("statusCode", faker.StatusCode()). + Str("Content-Type", w.Header().Get("Content-Type")). + Str("Sign", w.Header().Get(sign.SignHeaderName)). + Int("Size", faker.BytesWritten()). // todo add gzipped response flag Msg("Response ->") }) From 1115128a35e2c43d0effde06d3e5084e81561dec Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:17:53 +0300 Subject: [PATCH 34/55] =?UTF-8?q?fix:=20middleware=20=D0=9F=D0=BE=D0=B4?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=20intercept.Buffered()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/signing.go | 49 ++++++++++----------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/internal/server/middleware/signing.go b/internal/server/middleware/signing.go index a79e3582..8e2d262b 100644 --- a/internal/server/middleware/signing.go +++ b/internal/server/middleware/signing.go @@ -56,44 +56,31 @@ func Signing(key string) func(http.Handler) http.Handler { } if err := sign.Check(data[:n], keyBytes, receivedSign); err != nil { - log.Info().Str("receivedSign", receivedSign).Msg("подпись не прошла проверку") api.HTTPErrorWithLogging(w, http.StatusNotFound, "Подпись не прошла проверку") return } - // теперь займемся запросом: - // обернем в перехватчик врайтер и подпишем ответ - fakew := SignInterceptor{ - WriteInterceptor: intercept.New(w, data), // todo если буфер слишком маленький - key: keyBytes, - // todo переиспользуем наш буфер, а моем наверное целиком весь буфер если его почистить - } - defer fakew.Close() - - next.ServeHTTP(&fakew, r) + // теперь займемся ответом: + buf := bytes.NewBuffer(data[:0]) + faker := intercept.WithBuffer(w, buf) - // todo в целом мы могли бы вместо Close тут разобраться с перехваченными данными - // Это было чуть более переиспользуемо и наверное понятно + next.ServeHTTP(faker, r) - }) - } -} + // теперь запишем все, что мы забуферизировали -type SignInterceptor struct { - intercept.WriteInterceptor - key []byte -} + s, err := sign.Bytes(buf.Bytes(), keyBytes) + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "не удается подписать ответ: %v", err) + return + } + w.Header().Set(sign.SignHeaderName, s) + log.Info().Str("sign", s).Msg("Запрос подписан") -func (w SignInterceptor) Close() { - s, err := sign.Bytes(w.Buf().Bytes(), w.key) - if err != nil { - log.Error().Msgf("cant sign response: %v", err) + // записываем из буфера в оригинальный райтер + if err := faker.Flush(); err != nil { + api.HTTPErrorWithLogging(w, http.StatusNotFound, "Не переписать из фейк врайтера : %v", err) + return + } + }) } - - // копируем все из временного хранилища по назначению - w.WriteInterceptor.Header().Set(sign.SignHeaderName, s) - log.Info().Str("sign", s).Msg("Запрос подписан") - - w.WriteInterceptor.Close() - } From 2b58de6bda2298a175f02b404e55d48415240282 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:19:44 +0300 Subject: [PATCH 35/55] =?UTF-8?q?docs:=20intercept=20=D0=94=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/docs.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/intercept/docs.go b/lib/intercept/docs.go index fedc2ae8..17905beb 100644 --- a/lib/intercept/docs.go +++ b/lib/intercept/docs.go @@ -1,4 +1,6 @@ // Содержит разные полезные утилиты для работы с хендлерами -// и мидлварями, например оборачивался для ResponseWriter, -// или для повторного использования (Request).body +// и мидлварями, например оборачивалки для ResponseWriter +// +// Buffered - защищает тело врайтера от записи и сохраняет данные в буфер +// BytesCounter - подсчитывает записанные байты и сохраняет значение кода ответа package intercept From 93dce69d7d3c6b53e7ca691f818852991c257dbf Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 12:20:14 +0300 Subject: [PATCH 36/55] =?UTF-8?q?fix:=20intercept=20=D0=A3=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D1=80=D1=8F=D0=B2=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BE=D0=B1=D0=BE=D1=80=D0=B0=D1=87=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/responsewriter.go | 60 --------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 lib/intercept/responsewriter.go diff --git a/lib/intercept/responsewriter.go b/lib/intercept/responsewriter.go deleted file mode 100644 index f1f2ec48..00000000 --- a/lib/intercept/responsewriter.go +++ /dev/null @@ -1,60 +0,0 @@ -package intercept - -import ( - "bytes" - "io" - "net/http" - - "github.com/rs/zerolog/log" -) - -// Необходимо закрыть -type WriteInterceptor struct { - statusCode int - origWriter http.ResponseWriter - buf *bytes.Buffer -} - -func New(w http.ResponseWriter, data []byte) WriteInterceptor { - // todo дай обработку на nil - return WriteInterceptor{ - origWriter: w, - buf: bytes.NewBuffer(data[:0]), // обнуляем массив, иначе пишет в конец, а читать будет с начала, и в выход выйдет мусор, что бы уже в буфере - } -} - -func (w *WriteInterceptor) WriteHeader(code int) { - w.statusCode = code -} - -func (w *WriteInterceptor) Header() http.Header { - return w.origWriter.Header() -} - -func (w WriteInterceptor) Write(data []byte) (int, error) { - return w.buf.Write(data) -} - -// Close говорит о том, что сообщение готовится к отправке, значит -// можно установить нужные хедеры и отправлять -func (w *WriteInterceptor) Close() { - if w.statusCode != 0 { // todo перенести логику в геттер - w.origWriter.WriteHeader(w.statusCode) - } - - _, err := io.Copy(w.origWriter, w.buf) - if err != nil { - log.Error().Msgf("copy a response to originalWriter: %v", err) - } -} - -func (w WriteInterceptor) Buf() *bytes.Buffer { - return w.buf -} - -func (w WriteInterceptor) StatusCode() int { - if w.statusCode == 0 { - return 200 - } - return w.statusCode -} From 9fca80eb5f2011fe369745becf0e38cf6b18274f Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 13:01:04 +0300 Subject: [PATCH 37/55] =?UTF-8?q?feat:=20intercept=20=D0=9E=D1=82=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=B4=20=D0=B4=D0=BB=D1=8F=20Buffered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/buffered.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/intercept/buffered.go b/lib/intercept/buffered.go index d6722582..840b8b91 100644 --- a/lib/intercept/buffered.go +++ b/lib/intercept/buffered.go @@ -52,3 +52,7 @@ func (w Buffered) Flush() error { _, err := io.Copy(w.ResponseWriter, w.buf) return err } + +func (w Buffered) StatusCode() int { + return w.code +} From 6b2f3cfa40db7688e859006e2ad0c55c6ba39e9d Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 13:01:25 +0300 Subject: [PATCH 38/55] =?UTF-8?q?docs:=20intercept=20=D0=A3=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=B0=D0=BA?= =?UTF-8?q?=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/docs.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/intercept/docs.go b/lib/intercept/docs.go index 17905beb..f4c94e01 100644 --- a/lib/intercept/docs.go +++ b/lib/intercept/docs.go @@ -3,4 +3,11 @@ // // Buffered - защищает тело врайтера от записи и сохраняет данные в буфер // BytesCounter - подсчитывает записанные байты и сохраняет значение кода ответа +// +// Важно понимать различия между ними. BytesCounter работает в проточном режиме +// и лишь сохраняет для дальнейшего использования данные, такие как +// код и тело ответа. После его передачи по цепочке мидлвари в следующую мидлварь +// ответ уже не получится изменить, в то время как Buffered позволяет +// полностью менять ответ до вызова Flush(). К тому же Flush вообще не обязательно +// вызывать package intercept From cb90843a674a049a12997e0971c923b2fe8e3aa7 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 14:18:38 +0300 Subject: [PATCH 39/55] =?UTF-8?q?fix:=20interceptor=20Buffered=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=86=D0=B5=20=D0=BF=D0=B8=D1=88=D0=B5=D1=82?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=B4=20=D0=B2=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=B2=D1=80=D0=B0=D0=B9=D1=82?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B1=D1=8B=D0=BB=20=D0=B7=D0=B0=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D1=8B=D0=B9=20=D0=B1=D0=B0=D0=B3,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BE=D0=B4=20=D0=BF=D0=B8=D1=81=D0=B0=D0=BB?= =?UTF-8?q?=D1=81=D1=8F=20=D0=B2=20=D0=B1=D1=83=D1=84=D0=B5=D1=80=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0,=20=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=B2=20=D0=B2=D1=8B=D1=85=D0=BE=D0=B4=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B2=D1=80=D0=B0=D0=B9=D1=82=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/intercept/buffered.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/intercept/buffered.go b/lib/intercept/buffered.go index 840b8b91..453f549c 100644 --- a/lib/intercept/buffered.go +++ b/lib/intercept/buffered.go @@ -46,7 +46,7 @@ func (w *Buffered) WriteHeader(code int) { // в оригинальный врайтер func (w Buffered) Flush() error { if w.code != 0 { - w.WriteHeader(w.code) + w.ResponseWriter.WriteHeader(w.code) } _, err := io.Copy(w.ResponseWriter, w.buf) From 1b144d71148ffc0fb8e43fa5f1dcda30bc503c75 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 17:40:41 +0300 Subject: [PATCH 40/55] =?UTF-8?q?fix:=20server=20=D0=A1=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B0=D1=82=D1=8C,=20=D1=87=D1=82=D0=BE=20=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89=D0=B5=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=BE=20=D0=B2=20=D0=91=D0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/server.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/server/server.go b/cmd/server/server.go index 113aa8b0..6a74b87d 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -144,6 +144,8 @@ func ConfigureStorage(cfg config) (api.Operator, context.CancelFunc) { if err := dbs.Check(context.TODO()); err != nil { log.Error().Msgf("Нет соединения с БД - %v", err) } + + log.Info().Msg("Создано хранилише в Базе данных") return dbs, func() { err := db.Close() if err != nil { From 52f12ea56f80d7ce545cf320ef77c1d8eec5cdb0 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 17:41:59 +0300 Subject: [PATCH 41/55] =?UTF-8?q?fix:=20middleware=20=D0=94=D0=9E=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/log.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/server/middleware/log.go b/internal/server/middleware/log.go index 2ae311f9..d9711a7d 100644 --- a/internal/server/middleware/log.go +++ b/internal/server/middleware/log.go @@ -13,10 +13,26 @@ func MeowLogging() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { faker := intercept.WithBytesCounter(w) + d := countTime(func() { // запустить обработку next.ServeHTTP(faker, r) }) + + defer func() { + msg := recover() + if msg == nil { + return + } + log.Error(). + Str("method", r.Method). + Str("uri", r.RequestURI). + Bool("gzippedRequest", encoded(r, "gzip")). + Str("Sign", r.Header.Get(sign.SignHeaderName)). // todo sign.HeaderName + Dur("Duration", d). + Msgf("PANIC -> %v", msg) + }() + log.Info(). Str("method", r.Method). Str("uri", r.RequestURI). From 48d73571bf4b87072237638bf4fe686b11cd101a Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 17:44:18 +0300 Subject: [PATCH 42/55] =?UTF-8?q?feat:=20middleware=20=D0=BA=D0=BE=D0=BC?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D1=81=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=BE=D0=BC=D0=BE?= =?UTF-8?q?=D1=89=D0=B8=20intercept.Buffered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/gzip.go | 59 +++++++++++++++++++++++++++--- internal/server/router/router.go | 5 +-- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/internal/server/middleware/gzip.go b/internal/server/middleware/gzip.go index 23137154..5fe2bf36 100644 --- a/internal/server/middleware/gzip.go +++ b/internal/server/middleware/gzip.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/server/api" + "github.com/thefrol/kysh-kysh-meow/lib/intercept" ) // GZIP это мидлварь для сервера, которая сжимает содержимое запроса @@ -26,7 +28,7 @@ import ( // Можно воспользоваться опцией GZIPDefault, содержащую все базовые условия для сжатия // // router.Use(GZIP(GZIPDefault)) -func GZIP(funcOpts ...gzipFuncOpt) func(http.Handler) http.Handler { +func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { // Тут описываемся сама мидлварь, получившая opts // в качестве настроек @@ -34,12 +36,57 @@ func GZIP(funcOpts ...gzipFuncOpt) func(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Eсли клиент поддерживает gzip, то подменяем врайтер, не забыв его закрыть, и отправляем // запрос дальше по цепочке - if acceptsEncoding(r, "gzip") { - cw := NewCompressedWriter(w, funcOpts...) - w = cw - defer cw.Close() + fmt.Println("гзип начат") + if !acceptsEncoding(r, "gzip") { + //не обжимаем + next.ServeHTTP(w, r) + return + } + + // буферизируем + buf := bytes.NewBuffer(make([]byte, 0, bufSize)) // todo что если у нас есть пул буферов? + faker := intercept.WithBuffer(w, buf) + next.ServeHTTP(faker, r) + + if buf.Len() < minLen || faker.StatusCode() >= 300 { + // записываем все в оригинальный врайтер, не сжимая + faker.Flush() + return + } + + // мы решили ужать ответ, + // для начала запишем новый заголовок, + // и запишем код ответа + + w.Header().Add("Content-Encoding", "gzip") + w.Header().Del("Content-Length") // очень важно, иначе будет ошибка при попытке закрыть врайтер компрессора + + if faker.StatusCode() != 0 { + w.WriteHeader(faker.StatusCode()) + } + + gz, err := gzip.NewWriterLevel(w, gzip.BestCompression) + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "GZIP writer init failed %v", err) + return + } + + n, err := io.Copy(gz, buf) + fmt.Println(n) + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "Compressing %v", err) // todo нужны хелперы вроду api.InternalError + return + } + + if buf.Len() > 0 { + log.Error(). + Msg("В буфере остались непрочитанные байты") // мой страх почему-то + } + + err = gz.Close() + if err != nil { + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "Не могу записать в зиппер: %v", err) } - next.ServeHTTP(w, r) }) } diff --git a/internal/server/router/router.go b/internal/server/router/router.go index 2c233520..cd1904c1 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/go-chi/chi/v5" - chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/thefrol/kysh-kysh-meow/internal/server/api" "github.com/thefrol/kysh-kysh-meow/internal/server/middleware" @@ -25,7 +24,7 @@ func MeowRouter(store api.Operator, key string) (router chi.Router) { router.Use(middleware.Signing(key)) } router.Use(middleware.UnGZIP) - router.Use(middleware.GZIP(middleware.GZIPDefault)) + router.Use(middleware.GZIP(50, 2048)) //todo нормальные константы вместо магических чисел // Создаем маршруты для обработки URL запросов router.Group(func(r chi.Router) { @@ -37,7 +36,7 @@ func MeowRouter(store api.Operator, key string) (router chi.Router) { // Создаем маршруты для обработки JSON запросов router.Group(func(r chi.Router) { - r.With(chimiddleware.AllowContentType("application/json")) + //r.With(chimiddleware.AllowContentType("application/json")) r.Post("/value", api.HandleJSONRequest(api.Retry3Times(store.Get))) r.Post("/value/", api.HandleJSONRequest(api.Retry3Times(store.Get))) From 7dcf6b5f55b0ba8da7fede1de931e946ed679ddd Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Wed, 25 Oct 2023 18:12:37 +0300 Subject: [PATCH 43/55] =?UTF-8?q?fix:=20middleware=20=D0=A3=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=81=D0=B6=D0=B8=D0=BC=D0=B0=D0=BB=D1=8C=D1=89=D0=B8=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/gzip.go | 233 ++------------------- internal/server/middleware/gzip.options.go | 98 --------- 2 files changed, 23 insertions(+), 308 deletions(-) delete mode 100644 internal/server/middleware/gzip.options.go diff --git a/internal/server/middleware/gzip.go b/internal/server/middleware/gzip.go index 5fe2bf36..d182c039 100644 --- a/internal/server/middleware/gzip.go +++ b/internal/server/middleware/gzip.go @@ -13,21 +13,19 @@ import ( "github.com/thefrol/kysh-kysh-meow/lib/intercept" ) +var acceptedContentTypes = []string{ + "text/plain", + "text/html", + "text/css", + "text/xml", + "application/json", + "application/javascript"} + +const CompressionLevel = gzip.BestCompression + // GZIP это мидлварь для сервера, которая сжимает содержимое запроса -// и оформляет все заголовки. Имеет множество настроек, которые передаются -// при создании, такие как: -// -// MinLenght(длинна) - минимальная длинна тела в байтах, чтобы сжать тело запроса -// ContentType(заголовок1, заголовок2, ... ) разрешенные к передаче заголовки, обычно text/plain,text/html, application/json -// StatusCodes(код1, код2, ...) разрешенные к сжиманию по кодам ответов, обычно 200 -// BestCompession, BestSpeed - уровень компрессии -// -// В общем виде, на сервере chi создание мидлвари выглядит следующим образом, -// router.Use(GZIP(BestCompresion,MinLenght(50),ContentTypes("text/plain"),StatusCodes(http.StatusOK))) -// -// Можно воспользоваться опцией GZIPDefault, содержащую все базовые условия для сжатия -// -// router.Use(GZIP(GZIPDefault)) +// и оформляет все заголовки. Сжимает, если тело сообщения больше чем +// minLen, и для сжатия создается буфер изначальной вместимости bufSize func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { // Тут описываемся сама мидлварь, получившая opts @@ -48,7 +46,7 @@ func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { faker := intercept.WithBuffer(w, buf) next.ServeHTTP(faker, r) - if buf.Len() < minLen || faker.StatusCode() >= 300 { + if buf.Len() < minLen || faker.StatusCode() >= 300 || !contentTypeZippable(w.Header().Get("Content-Type")) { // записываем все в оригинальный врайтер, не сжимая faker.Flush() return @@ -65,7 +63,7 @@ func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { w.WriteHeader(faker.StatusCode()) } - gz, err := gzip.NewWriterLevel(w, gzip.BestCompression) + gz, err := gzip.NewWriterLevel(w, CompressionLevel) if err != nil { api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "GZIP writer init failed %v", err) return @@ -92,200 +90,6 @@ func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { } } -// CompressedWriter это обертка над ResponseWriter, -// которая если нужно, будет использовать сжимать данные -// а если не нужно, просто отправит как есть -// -// Стоит обязательно создавать конструктором NewCompressedWriter, -// иначе будут пробелмы с статус кодом по умолчанию -// -// Вся концепция этого класса держится на том, в http.ResponseWrite -// устроен следующим образом: к тому моменту, как впервые используется -// Write, все заголовки и код ответа уже должны быть записаны, и -// впоследствии не могут быть изменены. А это мы можем должаться этого первого -// Write() и решить, будем ли мы сжимать данные или нет. -// -// Например, если мы отвечаем 404 или 400, то сжимать не надо -// И надо сжимать, если Content-Type: text или application/json -// -// Главная проблема, конечно с минимальной длинной, после которой начинается сжатие, -// это означает что первые байты мы должны записывать в какой-то буфер и если он переполнится, то -// активировать сжатие, и все писать потом уже в сжимальщик gzip.Writer -type CompressedWriter struct { - opts gzipOptions - - archiveWriter io.WriteCloser - originalWriter http.ResponseWriter - statusCode int - - fistBytesBuffer bytes.Buffer // сюда будут записаны первые minLenght байт - bytesWritten int - - ignore bool // true - не будем сжимать - checked bool // true- прошел все проверки по другим параментрам кроме минамальной длинны и собирается быть сжат - -} // todo а этот класс я бы вынес в отдельный пакет наверн - -func NewCompressedWriter(originalWriter http.ResponseWriter, funcOpts ...gzipFuncOpt) *CompressedWriter { - - // Создаем структуру настроек для GZIP, - // после применения к ней опцефункций, - // она будет передана в замыкание и будет уже работать с мидлварью - opts := gzipOptions{ - CompressionLevel: gzip.BestCompression, - } // todo наверное такие настройки самых базовых штук должны быть в конструкторе опций - opts.Apply(funcOpts...) - return &CompressedWriter{ - opts: opts, - originalWriter: originalWriter, - statusCode: 200, // сразу ставим StatusOK, потому что такой код у нас по умолчанию - } - - //todo я думаю нам нужны какие-то настройки по умолчанию, заголовки и статус коды нарзерешенные -} - -func (cw CompressedWriter) Header() http.Header { - return cw.originalWriter.Header() -} - -func (cw *CompressedWriter) Write(bb []byte) (int, error) { - - // если уже было выбрано, что компрессию игнорируем, то просто пишем в обернурый врайтер - if cw.ignore { - return cw.originalWriter.Write(bb) - } - - // если архиватор уже создан, то тупо пишем в него и не думаем - if cw.archiveWriter != nil { - return cw.archiveWriter.Write(bb) - } - //todo мы можем из Content-Lenght прочитать сколько у меня байт будет - - // тут мы проверяем, что этот ответ будет сжат, если проходит проверки, то устанавливаем checked=true - // стоит отменить, что checked и ignore не взаимозаменяемы. Во время первой записи у нас ни тот ни другой - // не установлены, и checked в основном используется, чтобы сэкономить пару тактов на проверку, чтобы - // не проверять кучу хедеров во время каждой записи, хотя может это и лишняя забота - if !cw.checked { - - // до этой отметки никакой записи в тело originalWriter, тот что мы обернули, - // не происходило, а ниже уже начинаются первые записи, значит надо подготовиться - // и записать все что потом уже не запишется, например код ответа - - // либо мы прошли проверки на архивацию checked=true, либо архивания отменилась ignore==true - if cw.opts.CheckStatusCode(cw.statusCode) && cw.opts.CheckContentTypes(cw.originalWriter.Header()) { - cw.checked = true - - } else { - // мы не будем использовать компрессию, устанавливаем флаг ignore и записываем байты в обернутый врайтер - cw.ignore = true - cw.confirmStatusCode() - return cw.originalWriter.Write(bb) - } - } - - // нам прошел буфер размером len, если с учетом буфера нам все же не хватает байт - // чтобы активировать архивацию, то записываем в буфер, и возвращаем управление - // - // TODOs - // - // из исходного кода chi.middleware мы знаем, что можно просто прочитать content-length - // и сразу узнаем, сколько у нас там байт, - // а коли контент-ленгтх не указан, то напрямую компрессируем - if cw.bytesWritten+len(bb) < cw.opts.MinLenght { - n, err := cw.fistBytesBuffer.Write(bb) - cw.bytesWritten += n - return n, err - } - - // Теперь мы прошли минимальный порог байтов, значит архивируем: - // создадим архиватор и сбросить все что уже накопилось - - gz, err := gzip.NewWriterLevel(cw.originalWriter, cw.opts.CompressionLevel) - if err != nil { - return 0, fmt.Errorf("не могу создать gzip.Writer: %v", err) - } - - // не забудем подсказать в заголовках, что мы архивируем содержание - cw.originalWriter.Header().Add("Content-Encoding", "gzip") - cw.originalWriter.Header().Del("Content-Length") - cw.confirmStatusCode() - - cw.archiveWriter = gz - - // если во временный буфер уже успели что-то записать то сбрасываем в архиватор - if cw.bytesWritten > 0 { - n, err := io.Copy(cw.archiveWriter, &cw.fistBytesBuffer) - if err != nil { - return int(n), err - } - - } - - // todo - // - // надо бы проверять, что он уже не сжат, - - nn, err := gz.Write(bb) - if err != nil { - log.Error().Str("Location", "compression middleware").Msg("error writing to first bytes buffer when writing, should not happen") - return nn, err - // todo тут ещё можно скипнуть с архива и попробовать например без архивации, если что-то идет не так. Например может можно вызвать recover? - } - cw.bytesWritten += nn - - return nn, nil - - // todo - // - // 1. пул архиваторов отдельным пакетом. Или пул-пакет) пул-пулак - // пул-пулак даже написать хочется - // короче создаются динамически, поддерживается число десять, - // может быть и больше, но тогда они будут утилизироваться, - // или больше нельзя) - // - // 2. архивация идет по порядку, надо это учитывать, что может быть заархивировано в каком-то порядке -} - -func (cw *CompressedWriter) WriteHeader(status int) { - // Запоминаем код ответа и используем его - // перед первым Write() в оригинальный - // responseWriter - cw.statusCode = status -} - -// confirmStatusCode подтвержает полученный статус код, и записывает его -// в оригинальный врайтер. В отличие от WriteHeader(), который просто запоминает код -// эта функция имеет внутреннее значение для мидлвари, она -// уже реально записывает статус код на выход. -// -// Тут мы проверяем, а не пришел ли нам статус код 0 -// по сути это тот же код 200, просто связанный с ошибками на других уровнях обработки. На это стоит обратить -// пристальное внимание, но такая ошибка не должна валить сервер -func (cw *CompressedWriter) confirmStatusCode() { - if cw.statusCode == 0 { - log.Error().Str("location", "server/middleware/gzip").Msg("Кто-то записал мне код статуса 0, вместо 200, это где-то после меня случилось. Я поменял статус код на 200, но нужно обязательно проверить, что-то работает не так") - cw.statusCode = 200 - } - if cw.statusCode != 200 { - cw.originalWriter.WriteHeader(cw.statusCode) - } -} - -func (cw *CompressedWriter) Close() { - // Если порог минимального количество байт так и не пройдет, то просто сбрасываем все в - // обернутый врайтер - if cw.archiveWriter == nil { - cw.confirmStatusCode() - io.Copy(cw.originalWriter, &cw.fistBytesBuffer) - } else { - // а если архиватор создан, то его надо закрыть - cw.archiveWriter.Close() - } - // это уже не часть интерфейса, но нужна для нашей мидвари, чтобы закрыть gzip.Writer -} - -var _ http.ResponseWriter = (*CompressedWriter)(nil) - // acceptsEncoding возвращает true, если на основании запроса r // можно сказать, клиент поддерживает кодирование в encoding // @@ -298,3 +102,12 @@ func acceptsEncoding(r *http.Request, encoding string) bool { } return false } + +func contentTypeZippable(s string) bool { + for _, ct := range acceptedContentTypes { + if strings.Contains(s, ct) { + return true + } + } + return false +} diff --git a/internal/server/middleware/gzip.options.go b/internal/server/middleware/gzip.options.go deleted file mode 100644 index 1ce27729..00000000 --- a/internal/server/middleware/gzip.options.go +++ /dev/null @@ -1,98 +0,0 @@ -package middleware - -import ( - "compress/gzip" - "net/http" - "strings" - - "github.com/thefrol/kysh-kysh-meow/lib/slices" -) - -type gzipOptions struct { - MinLenght int - contentTypes []string - CompressionLevel int - allowedStatusCodes []int -} - -// Apply применяеn опцефункцию к настройкам GZIP -func (o *gzipOptions) Apply(opts ...gzipFuncOpt) { - for _, f := range opts { - f(o) - } -} - -func (o gzipOptions) CheckContentTypes(h http.Header) bool { - for _, ct := range o.contentTypes { - for _, c := range h.Values("Content-Type") { - if strings.Contains(c, ct) { - return true - } - } - } - return false -} - -func (o gzipOptions) CheckStatusCode(code int) bool { - return slices.Contains[int](o.allowedStatusCodes, code) -} - -type gzipFuncOpt func(*gzipOptions) - -func ContentTypes(ct ...string) gzipFuncOpt { - return func(opts *gzipOptions) { - opts.contentTypes = append(opts.contentTypes, ct...) - } -} - -func MinLenght(len int) gzipFuncOpt { - return func(opts *gzipOptions) { - opts.MinLenght = len - } -} - -func CompressionLevel(level int) gzipFuncOpt { - return func(opt *gzipOptions) { - opt.CompressionLevel = level - } - -} - -// GZIPBestCompression устанавливаем компрессию на уровень gzip.BestSpeed -func GZIPBestSpeed(opt *gzipOptions) { - opt.CompressionLevel = gzip.BestSpeed -} - -// GZIPBestCompression устанавливаем компрессию на уровень gzip.BestCompression -func GZIPBestCompression(opt *gzipOptions) { - opt.CompressionLevel = gzip.BestCompression -} - -func StatusCodes(codes ...int) gzipFuncOpt { - return func(opt *gzipOptions) { - opt.allowedStatusCodes = append(opt.allowedStatusCodes, codes...) - } - -} - -// todo По-хорошему, у нас должны быть ещё какие-то удалятели значений, типа BlockStatusCode(200), типа чтобы можно было удалить из установленных автоматически или типа того - -// GZIPDefault создает типичные настройки для сжатия: -// -// Уровень сжатия: лучшая компрессия -// Заголовки: text/html, text/plain, text/xml, text/css, application/json, application/javascript -// Статус коды: 200 -// Минимальная длинна 50 байт -func GZIPDefault(opt *gzipOptions) { - opt.Apply( - GZIPBestCompression, - StatusCodes(http.StatusOK), - ContentTypes( - "text/plain", - "text/html", - "text/css", - "text/xml", - "application/json", - "application/javascript"), - MinLenght(50)) -} From 547185a22d6f53f038794ad877a628c6af2f3bee Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:23:19 +0300 Subject: [PATCH 44/55] =?UTF-8?q?fix:=20ungzip=20=D0=BF=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=8F=D1=82=D1=8C=20=D0=BA=D0=BE=D0=B4=20=D0=BE=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=B0...=20=D0=BF=D1=80=D0=B8=20=D0=B1=D0=B8?= =?UTF-8?q?=D1=82=D0=BE=D0=BC=20=D0=B3=D0=B7=D0=B8=D0=BF=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/middleware/ungzip.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/server/middleware/ungzip.go b/internal/server/middleware/ungzip.go index d23b8db8..a34b3189 100644 --- a/internal/server/middleware/ungzip.go +++ b/internal/server/middleware/ungzip.go @@ -22,9 +22,7 @@ func UnGZIP(next http.Handler) http.Handler { // в случае если перед нами gzip, заменяем исходное тело запроса на обертку с gzip gz, err := gzip.NewReader(r.Body) if err != nil { - log.Error().Str("location", "middleware/gzip").Strs("Content-Encoding", r.Header.Values("Content-Encoding")).Err(err).Msg("Cant unzip a request body") - http.Error(w, "cant unzip", http.StatusBadRequest) - //todo try recover and send data as is + api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Не могу декомпрессировать тело запроса %v", err) return } defer r.Body.Close() From 4c4c078828468497df85c04ef7cb32a3cda39a3f Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:25:06 +0300 Subject: [PATCH 45/55] =?UTF-8?q?fix:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/router/router.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/server/router/router.go b/internal/server/router/router.go index cd1904c1..bc65428c 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -9,6 +9,11 @@ import ( "github.com/thefrol/kysh-kysh-meow/internal/server/middleware" ) +const ( + CompressionTreshold = 50 + CompressionBufferLen = 2048 +) + // MeowRouter - основной роутер сервера, он отвечает за все мидлвари // и все маршруты, и даже чтобы на ответы типа 404 и 400 отправлять // стилизованные ответы. @@ -24,7 +29,7 @@ func MeowRouter(store api.Operator, key string) (router chi.Router) { router.Use(middleware.Signing(key)) } router.Use(middleware.UnGZIP) - router.Use(middleware.GZIP(50, 2048)) //todo нормальные константы вместо магических чисел + router.Use(middleware.GZIP(CompressionTreshold, CompressionBufferLen)) //todo нормальные константы вместо магических чисел // Создаем маршруты для обработки URL запросов router.Group(func(r chi.Router) { From 278ee1bd2447dcf04ac6a43211959b1cd405d65b Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:25:51 +0300 Subject: [PATCH 46/55] =?UTF-8?q?docs:=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=86=D0=B8=D1=8E=20+=20intercept=20+=20middleware?= =?UTF-8?q?=20+=20Readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +++++++++++++++- internal/server/middleware/gzip.go | 14 +++++++------- internal/server/middleware/ungzip.go | 11 +++++++---- lib/intercept/buffered.go | 5 +++++ lib/intercept/bytecounter.go | 3 +++ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 40cc3397..8028f158 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,21 @@ ---- -Это учебная работе в [Яндекс-практикуме](https://practicum.yandex.ru), в рамках которой будет реализован сервер `мяу`, и агент `тыгыдык`, а может наоборот... ++ Фантастическая скорость работы достигается статической типизацией, использованием стека ++ Защищает связь сервера и агента при помощи подписей `SHA256` ++ Сжимает запросы и ответы ++ Все пакеты и функции документированы ++ Модульно, предметно ориантировано ++ Более ста тестов + +## Стек + ++ Go: chi, http, easyjson, ++ gzip, encoding, crypto, ++ runtime, sync ++ zerolog + +Это учебная работе в [Яндекс-практикуме](https://practicum.yandex.ru), поэтому такие вещи как сжатие ответа и запроса, подписывание реализовано ручками при помощи стандартных библиотек. ## TODO diff --git a/internal/server/middleware/gzip.go b/internal/server/middleware/gzip.go index d182c039..36fc5d99 100644 --- a/internal/server/middleware/gzip.go +++ b/internal/server/middleware/gzip.go @@ -24,17 +24,13 @@ var acceptedContentTypes = []string{ const CompressionLevel = gzip.BestCompression // GZIP это мидлварь для сервера, которая сжимает содержимое запроса -// и оформляет все заголовки. Сжимает, если тело сообщения больше чем -// minLen, и для сжатия создается буфер изначальной вместимости bufSize +// если тело сообщения больше чем minLenж. Для сжатия первоначально +// создается буфер изначальной вместимости bufSize func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { - - // Тут описываемся сама мидлварь, получившая opts - // в качестве настроек return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Eсли клиент поддерживает gzip, то подменяем врайтер, не забыв его закрыть, и отправляем // запрос дальше по цепочке - fmt.Println("гзип начат") if !acceptsEncoding(r, "gzip") { //не обжимаем next.ServeHTTP(w, r) @@ -46,7 +42,11 @@ func GZIP(minLen int, bufSize int) func(http.Handler) http.Handler { faker := intercept.WithBuffer(w, buf) next.ServeHTTP(faker, r) - if buf.Len() < minLen || faker.StatusCode() >= 300 || !contentTypeZippable(w.Header().Get("Content-Type")) { + notZippable := buf.Len() < minLen || + faker.StatusCode() >= 300 || + !contentTypeZippable(w.Header().Get("Content-Type")) + + if notZippable { // записываем все в оригинальный врайтер, не сжимая faker.Flush() return diff --git a/internal/server/middleware/ungzip.go b/internal/server/middleware/ungzip.go index a34b3189..b5acac59 100644 --- a/internal/server/middleware/ungzip.go +++ b/internal/server/middleware/ungzip.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/server/api" ) // UnGZIP распаковывает запросы, закодированные при помощи GZIP, и пропускает все @@ -13,19 +13,22 @@ import ( // содержал подстроку gzip func UnGZIP(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // пропускаем, поскольку мы не обрабатываем такие сжималки if !encoded(r, "gzip") { next.ServeHTTP(w, r) return } - // в случае если перед нами gzip, заменяем исходное тело запроса на обертку с gzip + // в случае если перед нами закодироанное тело, + // передаем исходное тело декомпрессору, а + // а выход декомпрессора вкладываем в тело запроса gz, err := gzip.NewReader(r.Body) if err != nil { api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Не могу декомпрессировать тело запроса %v", err) return } + defer gz.Close() defer r.Body.Close() + r.Body = gz //todo: make a pool of zgips and return only buffer, not gzipeers next.ServeHTTP(w, r) @@ -40,7 +43,7 @@ func UnGZIP(next http.Handler) http.Handler { func encoded(r *http.Request, encoder string) bool { // По стандартным договоренностям, если запрос сжат или закодирован, то кодировщики // указываются в последовательности их применения, а значит нам нужно читать последний - // заголовок Content-Encoded, и если он gzip, то расшифровать + // заголовок Content-Encoding, и если он gzip, то расшифровать hh := r.Header.Values("Content-Encoding") // Если вообще нет таких заголовков, то возвращаем false diff --git a/lib/intercept/buffered.go b/lib/intercept/buffered.go index 453f549c..e26b04d2 100644 --- a/lib/intercept/buffered.go +++ b/lib/intercept/buffered.go @@ -34,10 +34,14 @@ func WithBuffer(w http.ResponseWriter, buf io.ReadWriter) *Buffered { } } +// Write воплощает интерфейс http.ResponseWriter, записываем +// данные в некий буфер func (w *Buffered) Write(data []byte) (int, error) { return w.buf.Write(data) } +// WriteHeader воплощает интерфейс http.ResponseWriter, сохраняя +// последний назначенный статус код в переменную func (w *Buffered) WriteHeader(code int) { w.code = code } @@ -53,6 +57,7 @@ func (w Buffered) Flush() error { return err } +// StatusCode возвращае значение последнего установленного кода ответа func (w Buffered) StatusCode() int { return w.code } diff --git a/lib/intercept/bytecounter.go b/lib/intercept/bytecounter.go index 0d29e8f6..d5f181da 100644 --- a/lib/intercept/bytecounter.go +++ b/lib/intercept/bytecounter.go @@ -8,6 +8,9 @@ type BytesCounter struct { code int } +// Write воплощает интерфейс http.ResponseWriter; +// При каждом вызове, увеличиваем счетчик записанных байт, +// при этом данные пишутся в оригинальный врайтер func (w *BytesCounter) Write(data []byte) (int, error) { n, err := w.ResponseWriter.Write(data) w.n += n From faa42517b64263e99f937922db0d81a28c92fe40 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:27:59 +0300 Subject: [PATCH 47/55] =?UTF-8?q?fix:=20compress=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BC=D1=83=D1=81=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/compress/compress.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/compress/compress.go b/internal/compress/compress.go index b49eeab6..d53d21b1 100644 --- a/internal/compress/compress.go +++ b/internal/compress/compress.go @@ -35,9 +35,3 @@ func Bytes(data []byte, level int) ([]byte, error) { return b.Bytes(), nil } - -var () - -// TODO -// -// все вот это вот я бы сделал отдельным пакетом compress From cd2bdf674aa8e2f82896a1048b89d8170a102de8 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:48:59 +0300 Subject: [PATCH 48/55] =?UTF-8?q?fix:=20api=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D1=8C=D1=88=D0=B5=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D0=B5=20=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=20location:?= =?UTF-8?q?=20handler=20=D1=8D=D1=82=D0=BE=20=D0=BD=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B4=D0=B0=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C?= =?UTF-8?q?=20=D1=8D=D1=82=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B1=D0=BE=D0=BB=D0=B5=D0=B5=20=D0=B3=D0=BB=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/server/api/error.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/server/api/error.go b/internal/server/api/error.go index e16a6f9c..0690c2a6 100644 --- a/internal/server/api/error.go +++ b/internal/server/api/error.go @@ -18,11 +18,8 @@ import ( // format, params - типичные параметры, как в функции Printf func HTTPErrorWithLogging(w http.ResponseWriter, statusCode int, format string, params ...interface{}) { s := fmt.Sprintf(format, params...) - log.Error().Str("location", "json update handler").Msg(s) + log.Error().Msg(s) http.Error(w, s, statusCode) - // TODO - // - // Возможно это пока единственный повод держать кастомный логгер, чтобы в нем была функция типа withHttpError(w) } // Retry3Times повторяет операцию op ровно три раза From f70cfb3013e8fba457a40bb52345910af0eb105f Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:50:12 +0300 Subject: [PATCH 49/55] =?UTF-8?q?fix:=20compress=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/compress/compress.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/compress/compress.go b/internal/compress/compress.go index d53d21b1..60cb308d 100644 --- a/internal/compress/compress.go +++ b/internal/compress/compress.go @@ -8,10 +8,12 @@ import ( "github.com/rs/zerolog/log" ) +const Bufferlen = 500 + // Bytes возвращает сжатый массив байт func Bytes(data []byte, level int) ([]byte, error) { - b := bytes.NewBuffer(make([]byte, 0, 500)) //todo нужна какая-то константа + b := bytes.NewBuffer(make([]byte, 0, Bufferlen)) gz, err := gzip.NewWriterLevel(b, level) if err != nil { return nil, fmt.Errorf("cant create compressor") @@ -35,3 +37,7 @@ func Bytes(data []byte, level int) ([]byte, error) { return b.Bytes(), nil } + +// todo +// +// Думаю, именно в этом пакете я бы хотел объявить пулы с gzip.Ecoder и gzip.decoder From e6d4e72ebb8642ddd126c80ea7fde4dc1e515be9 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:50:41 +0300 Subject: [PATCH 50/55] =?UTF-8?q?docs:=20=D0=BF=D0=BE=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/config.go | 4 ---- cmd/server/config_test.go | 2 +- cmd/server/server.go | 5 +++-- internal/server/api/batch_handlers.go | 3 +-- internal/server/api/error.go | 4 ++++ internal/server/middleware/gzip.go | 13 +++++++++++++ 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/cmd/server/config.go b/cmd/server/config.go index f8731a1a..6b8e31a1 100644 --- a/cmd/server/config.go +++ b/cmd/server/config.go @@ -47,10 +47,6 @@ func mustConfigure(defaults config) (cfg config) { panic(err) } - // todo - // - // вообще две эти функции сверху требуют проверку ошибок, и это в тестах тоже стоило бы отразить - // Тут обрабатываем особый случай. Если переменная окружения установлена, но в пустое значение // то мы перезаписываем установленный командной строкой флаг на пуское значение, хотя штатно // этого не было бы сделано diff --git a/cmd/server/config_test.go b/cmd/server/config_test.go index e4b8d40c..12213e69 100644 --- a/cmd/server/config_test.go +++ b/cmd/server/config_test.go @@ -94,7 +94,7 @@ func Test_configure(t *testing.T) { name: "командной строкой указать интервал записи", defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true"}, - commandLine: "serv -i 299", // todo может uint поставить??? + commandLine: "serv -i 299", wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 299, Restore: true, FileStoragePath: "/tmp/file"}, }, { diff --git a/cmd/server/server.go b/cmd/server/server.go index 6a74b87d..342f70a4 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -98,7 +98,8 @@ func Run(cfg config, s api.Operator) { log.Error().Msgf("^0^ не могу запустить сервер: %v \n", err) //todo // - // если не биндится, то хотя бы выходить с ошибкой + // если не биндится, то хотя бы выходить с ошибкой, + // в данный момент сервер не закроета сам // // можно дать несколько попыток забиндиться } @@ -197,7 +198,7 @@ func ConfigureStorage(cfg config) (api.Operator, context.CancelFunc) { log.Info().Msgf("Установлено сохранение с интервалом %v в %v в при записи", s.Interval, s.FileName) return storage.AsOperator(s), func() { - // оберстка сделана под группу ожидаения + // обертка сделана под группу ожидаения cancel() } diff --git a/internal/server/api/batch_handlers.go b/internal/server/api/batch_handlers.go index ca863b1d..08b7f03c 100644 --- a/internal/server/api/batch_handlers.go +++ b/internal/server/api/batch_handlers.go @@ -53,9 +53,8 @@ func HandleJSONBatch(handler func(context.Context, ...metrica.Metrica) (out []me HTTPErrorWithLogging(w, http.StatusNotFound, "В хранилище не найдена одна из метрик %v", in) return } - HTTPErrorWithLogging(w, http.StatusBadRequest, "Ошибка в работе хендлера метрике %+v: %v", in, err) // todo, а как бы сделать так, чтобы %v подсвечивался + HTTPErrorWithLogging(w, http.StatusBadRequest, "Ошибка в работе хендлера метрике %+v: %v", in, err) return - // todo тут точно надо будет поиграть с обертками } // Замаршаливаем результат работы хендлера diff --git a/internal/server/api/error.go b/internal/server/api/error.go index 0690c2a6..6a2a4097 100644 --- a/internal/server/api/error.go +++ b/internal/server/api/error.go @@ -22,6 +22,10 @@ func HTTPErrorWithLogging(w http.ResponseWriter, statusCode int, format string, http.Error(w, s, statusCode) } +// может ли так быть, что это часть Service? это какой-то сервис ошибок?? + +// todo добавить api.BadGateway(), api.BadGatewayf() + // Retry3Times повторяет операцию op ровно три раза // с промежутками 1,3,5 секунд. // diff --git a/internal/server/middleware/gzip.go b/internal/server/middleware/gzip.go index 36fc5d99..adb76d6b 100644 --- a/internal/server/middleware/gzip.go +++ b/internal/server/middleware/gzip.go @@ -103,6 +103,8 @@ func acceptsEncoding(r *http.Request, encoding string) bool { return false } +// contentTypeZippable проверяет подходит ли Content-Type +// для сжатия func contentTypeZippable(s string) bool { for _, ct := range acceptedContentTypes { if strings.Contains(s, ct) { @@ -111,3 +113,14 @@ func contentTypeZippable(s string) bool { } return false } + +// todo +// +// для части вспомогательных функций и констант, я бы +// воспользовался internal/compress, и надо бы придумать +// семантику вызова: +// compress.CheckContentType? compess.Recommended? +// compress.Gzip() compress.Unzip() ? +// archive.Compress archive. Decompress? +// archive.RequestEncoded? +// как много вообще этот пакет должен знать о http??? From 0308855af4d0ce981b1140f2978a68359c891f38 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Thu, 26 Oct 2023 09:50:41 +0300 Subject: [PATCH 51/55] =?UTF-8?q?docs:=20=D0=BF=D0=BE=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/report/docs.go | 1 - internal/report/send.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/report/docs.go b/internal/report/docs.go index 3d222b67..df59c23c 100644 --- a/internal/report/docs.go +++ b/internal/report/docs.go @@ -28,4 +28,3 @@ package report // // Теперь мне не нравится, что тут больно дофига всего замешано) // Тут и какие-то мидлвари, и опрос памяти и отправка -// Мидлварь, должна лежать наверное в агенте все же diff --git a/internal/report/send.go b/internal/report/send.go index 46fdd25a..a474be22 100644 --- a/internal/report/send.go +++ b/internal/report/send.go @@ -20,7 +20,7 @@ var ( ErrorRequestRejected = errors.New("запрос не принят, статус код не 200") ) -var defaultClient = resty.New() // todo .SetJSONMarshaler(easyjson.Marshal()) +var defaultClient = resty.New() // Send отправляет метрики из указанного хранилища store на сервер host. // При возникновении ошибок будет стараться отправить как можно больше метрик, From bc19daf501814a6e163be288ebe16365a3cd4e4a Mon Sep 17 00:00:00 2001 From: Dima Frolenko <85274858+thefrol@users.noreply.github.com> Date: Mon, 30 Oct 2023 04:29:10 +0300 Subject: [PATCH 52/55] =?UTF-8?q?14.2=20=D0=A0=D0=B5=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=BA=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B0=20(#37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: config добавить Secret для чувствительных данных * feat: Перенести конфиг сервера в пакет config * feat: создать пакет app для сервера * docs: сделать рекламирующий README.md * fix: config Сделать MustConfigure частью ServerConfig а ServerCongig теперь просто config.Server * feat: отменить панику при парсинге конфига сервера * fix: Создавать хранилище в методе config.Server * feat: config Избавиться от паник при создании хранилища * fix: капелька логгирования * переименовать файлы * fix: config поправить сообщение об ошибке * fix: config добавить пропущенное логирование * feat: config добавить тип ConnectionString скрывает пароль к БД * feat: server вынести код Run() в app * feat: Завершать хранилища по запросу в контекст * feat: вынести graceful в отдельный пакет * fix: graceful Выделить функции * fix: сервер УБрать лишние комменты * fix: config исправлен баг, когда создавалось два хранилища * fix: server исправить опечатку в комменте * fix: server уточнить название завершателя контекста * fix: agent выделить планировщик в отдельную функцию * feat: server Перенести конфигурацию в config * feat: config убрать панику при конфигурировании агента * fix: server небольшое оформления кода * fix: config поправить тесты после убирания паники --------- Co-authored-by: Dima Frolenko --- README.md | 14 +- cmd/agent/agent.go | 42 ++-- cmd/agent/agent_test.go | 12 +- cmd/server/config.go | 70 ------ cmd/server/server.go | 204 +++--------------- .../config.go => internal/config/agent.go | 21 +- .../config/agent_test.go | 68 +++--- internal/config/connstring.go | 37 ++++ internal/config/secret.go | 38 ++++ internal/config/server.go | 162 ++++++++++++++ .../config/server_test.go | 109 ++++++---- internal/config/types_test.go | 25 +++ .../server/app => lib/graceful}/README.md | 0 lib/graceful/serve.go | 63 ++++++ 14 files changed, 516 insertions(+), 349 deletions(-) delete mode 100644 cmd/server/config.go rename cmd/agent/config.go => internal/config/agent.go (85%) rename cmd/agent/config_test.go => internal/config/agent_test.go (64%) create mode 100644 internal/config/connstring.go create mode 100644 internal/config/secret.go create mode 100644 internal/config/server.go rename cmd/server/config_test.go => internal/config/server_test.go (62%) create mode 100644 internal/config/types_test.go rename {internal/server/app => lib/graceful}/README.md (100%) create mode 100644 lib/graceful/serve.go diff --git a/README.md b/README.md index 8028f158..217ad7c8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ ---- +Основная задача этого продукта: показать какой я классный сотрудник и как классно я подхожу на должность, на которую вы меня сейчас рассматриваете. Поэтому тут вас ждет: + + Фантастическая скорость работы достигается статической типизацией, использованием стека ++ Сладкая, как `торт наполеон`, и слоистая архитектура. Все зависимости идут снизу вверх. И нет циклических зависимостей. `DDD` во все поля + Защищает связь сервера и агента при помощи подписей `SHA256` + Сжимает запросы и ответы + Все пакеты и функции документированы @@ -18,7 +21,16 @@ + runtime, sync + zerolog -Это учебная работе в [Яндекс-практикуме](https://practicum.yandex.ru), поэтому такие вещи как сжатие ответа и запроса, подписывание реализовано ручками при помощи стандартных библиотек. +Это учебная работе в [Яндекс-практикуме](https://practicum.yandex.ru), поэтому такие вещи как сжатие ответа и запроса, подписывание реализовано ручками при помощи стандартных библиотек. + +## Вещи которыми я горжусь + +### `config.Secret`, `config.ConnectionString` + +Эта структура для чувствительных данных, которая защищена от попадания в консоль. Если попытаться распечатать конфиг, то мы увидим + +SecretValue:****** +DataBaseString: host=localhost user=user ... // хотя тут был пароль ## TODO diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 9ac5dd9e..8bb3a109 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -4,32 +4,52 @@ import ( "fmt" "os" "path" - "strings" "time" "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/compress" + "github.com/thefrol/kysh-kysh-meow/internal/config" "github.com/thefrol/kysh-kysh-meow/internal/report" "github.com/thefrol/kysh-kysh-meow/lib/scheduler" ) const updateRoute = "/updates" +var defaultConfig = config.Agent{ + Addr: "localhost:8080", + ReportInterval: 10, + PollingInterval: 2, +} + func main() { - log.Info().Msgf("Агент запущен строкой %v", strings.Join(os.Args, " ")) + // Парсим командную строку и переменные окружения + config := config.Agent{} + if err := config.Parse(defaultConfig); err != nil { + log.Error().Msgf("Ошибка парсинга конфига: %v", err) + os.Exit(2) + } + + // Настроим отправку + report.SetSigningKey(config.Key) + report.CompressLevel = compress.BestCompression + report.CompressMinLength = 100 + + // Запускаем работу + Serve(config) - config := mustConfigure(defaultConfig) +} +// Endpoint формирует точку, куда агент будет посылать все запросы на основе своей текущей конфигурации +func Endpoint(cfg config.Agent) string { + return fmt.Sprintf("%s%s", "http://", path.Join(cfg.Addr, updateRoute)) +} + +func Serve(config config.Agent) { // Метрики собираются во временное хранилище s, // где они хранятся в сыром виде и готовы превратиться // в массив metrica.Metrica var s report.Stats - // Настроим отправку - report.SetSigningKey(config.Key) - report.CompressLevel = compress.BestCompression - report.CompressMinLength = 100 - // запуск планировщика c := scheduler.New() //собираем данные раз в pollingInterval @@ -50,10 +70,4 @@ func main() { // Запускаем планировщик, и он занимает поток c.Serve(200 * time.Millisecond) - -} - -// Endpoint формирует точку, куда агент будет посылать все запросы на основе своей текущей конфигурации -func Endpoint(cfg config) string { - return fmt.Sprintf("%s%s", "http://", path.Join(cfg.Addr, updateRoute)) } diff --git a/cmd/agent/agent_test.go b/cmd/agent/agent_test.go index 6fa34f7e..7e1a6b1b 100644 --- a/cmd/agent/agent_test.go +++ b/cmd/agent/agent_test.go @@ -1,22 +1,26 @@ package main -import "testing" +import ( + "testing" + + "github.com/thefrol/kysh-kysh-meow/internal/config" +) func TestEndpoint(t *testing.T) { tests := []struct { name string - cfg config + cfg config.Agent want string }{ { name: "positive", - cfg: config{Addr: "localhost"}, + cfg: config.Agent{Addr: "localhost"}, want: "http://localhost/updates", }, { name: "positive 2", - cfg: config{Addr: ":8080"}, + cfg: config.Agent{Addr: ":8080"}, want: "http://:8080/updates", }, } diff --git a/cmd/server/config.go b/cmd/server/config.go deleted file mode 100644 index 6b8e31a1..00000000 --- a/cmd/server/config.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/caarlos0/env/v6" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -type config struct { - Addr string `env:"ADDRESS"` - StoreIntervalSeconds uint `env:"STORE_INTERVAL"` - FileStoragePath string `env:"FILE_STORAGE_PATH"` - Restore bool `env:"RESTORE"` - DatabaseDSN string `env:"DATABASE_DSN"` - Key string `env:"KEY"` -} - -var defaultConfig = config{ - Addr: ":8080", - StoreIntervalSeconds: 300, - FileStoragePath: "/tmp/metrics-db.json", - Restore: true, // в текущей конфигурации это значение командной строкой никак не поменять, нельзя указать -r 0, флан такое не принимает todo -} - -// mustConfigure парсит командную строку и переменные окружения, чтобы выдать структуру с конфигурацией сервера. -// В приоритете переменные окружения. Принимает на вход структуру defaults со значениями по умолчанию. -// -// Приоритет такой: -// - Если другого не указано, будет использоваться defaults -// - То, что указано в командной строке переписывает то, что указано в defaults -// - То, что указано в переменной окружения, переписывает то, что было указано ранее -func mustConfigure(defaults config) (cfg config) { - flag.StringVar(&cfg.Addr, "a", defaults.Addr, "[адрес:порт] устанавливает адрес сервера ") - flag.UintVar(&cfg.StoreIntervalSeconds, "i", defaults.StoreIntervalSeconds, "[время, сек] интервал сохранения показаний. При 0 запись делается почти синхронно") - flag.StringVar(&cfg.FileStoragePath, "f", defaults.FileStoragePath, "[строка] путь к файлу, откуда будут читаться при запуске и куда будут сохраняться метрики полученные сервером, если файл пустой, то сохранение будет отменено") - flag.BoolVar(&cfg.Restore, "r", defaultConfig.Restore, "[флаг] если установлен, загружает из файла ранее записанные метрики") - flag.StringVar(&cfg.DatabaseDSN, "d", defaults.DatabaseDSN, "[строка] подключения к базе данных") - flag.StringVar(&cfg.Key, "k", defaults.Key, "строка, секретный ключ подписи") - - flag.Parse() - err := env.Parse(&cfg) - if err != nil { - panic(err) - } - - // Тут обрабатываем особый случай. Если переменная окружения установлена, но в пустое значение - // то мы перезаписываем установленный командной строкой флаг на пуское значение, хотя штатно - // этого не было бы сделано - if v, ok := os.LookupEnv("FILE_STORAGE_PATH"); ok { - cfg.FileStoragePath = v - } - - return -} - -func init() { - // настраиваем дружелюбный цветастый вывод логгера - log.Logger = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stderr}) - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - - // добавляет смайлик кота в конец справки flags - flag.Usage = func() { - flag.PrintDefaults() - fmt.Println("^-^") - } -} diff --git a/cmd/server/server.go b/cmd/server/server.go index 342f70a4..d0dcddc9 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -4,48 +4,49 @@ package main import ( "context" - "database/sql" - "fmt" - "net/http" "os" - "os/signal" - "strings" - "syscall" "time" "github.com/rs/zerolog/log" - "github.com/thefrol/kysh-kysh-meow/internal/server/api" + "github.com/thefrol/kysh-kysh-meow/internal/config" "github.com/thefrol/kysh-kysh-meow/internal/server/router" - "github.com/thefrol/kysh-kysh-meow/internal/storage" + "github.com/thefrol/kysh-kysh-meow/lib/graceful" _ "github.com/jackc/pgx/v5/stdlib" ) +var defaultConfig = config.Server{ + Addr: ":8080", + StoreIntervalSeconds: 300, + FileStoragePath: "/tmp/metrics-db.json", + Restore: true, // в текущей конфигурации это значение командной строкой никак не поменять, нельзя указать -r 0, флан такое не принимает todo +} + func main() { - log.Info().Msgf("Сервер запущен строкой %v", strings.Join(os.Args, " ")) + // Парсим командную строку и переменные окружения + cfg := config.Server{} + if err := cfg.Parse(defaultConfig); err != nil { + log.Error().Msgf("Ошибка парсинга конфига: %v", err) + os.Exit(2) + } - cfg := mustConfigure(defaultConfig) + rootContext, cancelRootContext := context.WithCancel(context.Background()) // это пусть будет просто defer storage.Close // создаем хранилище - s, cancelStorage := ConfigureStorage(cfg) + s, err := cfg.MakeStorage(rootContext) + if err != nil { + log.Error().Msgf("Не удалось создать хранилише: %v", err) + return + } - // Создаем объект App, который в дальнейшем включит в себя все остальное тут - // app, err := app.New(context.TODO(), cfg.DatabaseDSN) - // if err != nil { - // log.Fatal().Msgf("Ошибка во время конфигурирования сервера %v", err) - // panic(err) - // } - // if err := app.CheckConnection(context.Background()); err == nil { - // log.Info().Msg("Связь с базой данных в порядке") - // } + // создаем роутер + router := router.MeowRouter(s, string(cfg.Key.ValueFunc()())) // Запускаем сервер с поддержкой нежного завершения, - // занимаем текущий поток до вызова сигнатов выключения - Run(cfg, s) + // занимаем текущий поток до вызова сигналов выключения + graceful.Serve(cfg.Addr, router) - // Завершаем последние дела - // попытаемся сохраниться в файл - cancelStorage() + cancelRootContext() // Даем ему время time.Sleep(time.Second) @@ -54,156 +55,3 @@ func main() { // Wait for server context to be stopped } - -// Run запускает сервер с поддержкой нежного завершения. Сервер можно будет выключить через -// SIGINT, SIGTERM, SIGQUIT -func Run(cfg config, s api.Operator) { - // Запускаем сервер с поддержкой нежного выключения - // вдохноввлено примерами роутера chi - server := http.Server{Addr: cfg.Addr, Handler: router.MeowRouter(s, cfg.Key)} - - // Server run context - serverCtx, serverStopCtx := context.WithCancel(context.Background()) - - // Listen for syscall signals for process to interrupt/quit - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - go func() { - <-sig - log.Debug().Msg("signal received") - // Shutdown signal with grace period of 30 seconds - shutdownCtx, cancel := context.WithTimeout(serverCtx, 30*time.Second) - defer cancel() - - go func() { - <-shutdownCtx.Done() - if shutdownCtx.Err() == context.DeadlineExceeded { - log.Fatal().Msg("graceful shutdown timed out.. forcing exit.") - return - } - }() - - // Trigger graceful shutdown - err := server.Shutdown(shutdownCtx) - if err != nil { - log.Fatal().Msg(err.Error()) - panic(err) - } - serverStopCtx() - log.Info().Msg("^-^ рутина остановки сервера завершилась") - }() - log.Info().Msgf("^.^ Мяу, сервер запускается по адресу %v!", cfg.Addr) - - if err := server.ListenAndServe(); err != http.ErrServerClosed { - log.Error().Msgf("^0^ не могу запустить сервер: %v \n", err) - //todo - // - // если не биндится, то хотя бы выходить с ошибкой, - // в данный момент сервер не закроета сам - // - // можно дать несколько попыток забиндиться - } - - <-serverCtx.Done() - - // МНе кажется в отдельную функцию надо выделить именно все, что относится к нежному завершению, + надо перевести комменты по коду на русский -} - -// ConfigureStorage подготавливает хранилище к работе в соответствии с текущими настройками, -// при необходимости загружает из файла значения метрик, запускает сохранение в файл, и -// возвращает интерфейс хранилища и функцию, подготавливающая ханилище к остановке -// -// На входе получает экземпляр хранилища m, и далее оборачивает его другим классов, -// наиболее соответсвующим задаче, исходя из cfg -func ConfigureStorage(cfg config) (api.Operator, context.CancelFunc) { - // 0. Если указана база данных, создаем хранилище с базой данных - // 1. Если путь не задан, то возвращаем хранилище в оперативке, без приблуд - // 2. Иначе оборачиваем файловым хранилищем, но не возвращаем пока - // 3. Если Restore=true, то читаем из файла. Если файла не существует, то игнорируем проблему - // 4. Оборачиваем файловое хранилище сихнронным или интервальным - - // TODO - // - // Думаю эта функция должна получать на вход контекст, а не возвращать CancelFunc - // - // И нужно убрать отсюда все паники - - // Если база данных - if cfg.DatabaseDSN != "" { - db, err := sql.Open("pgx", cfg.DatabaseDSN) - if err != nil { - e := fmt.Errorf("не могу создать соединение с БД: %w", err) - panic(e) - } - - dbs, err := storage.NewDatabase(db) - if err != nil { - log.Error().Msgf("Ошибка создания хранилища в базе данных - %v", err) - panic(err) - } - - if err := dbs.Check(context.TODO()); err != nil { - log.Error().Msgf("Нет соединения с БД - %v", err) - } - - log.Info().Msg("Создано хранилише в Базе данных") - return dbs, func() { - err := db.Close() - if err != nil { - log.Error().Msgf("Не могу закрыть базу данных: %v", err) - } - - // todo - // - // Конечно, я хочу делать это defer или как-то так, можно у нас будет некий app.Close() - } - } - - // Если не база данных, то начинаем с начала - создаем хранилище в памяти, и оборачиваем его всякими штучками если надо - m := storage.New() - - // Если путь до хранилища не пустой, то нам нужно инициаизировать обертки над хранилищем - if cfg.FileStoragePath == "" { - log.Info().Msg("Установлено хранилище в памяти. Сохранение на диск отключено") - return storage.AsOperator(m), func() { - log.Info().Msg("Хранилище сихронной записи получило сигнал о завершении, но файловая запись в текущей конфигурации сервера не используется. Ничего не записано") - } - } - - // Оборачиваем файловым хранилищем, в случае, есл и - fs := storage.NewFileStorage(&m, cfg.FileStoragePath) - if cfg.Restore { - err := fs.Restore() - // в случае, если файла не существует, игнорируем эту проблему - if err != nil && err != storage.ErrorRestoreFileNotExist { - panic(err) - } - log.Info().Msgf("Значения метрик загружены из %v", fs.FileName) - } - - if cfg.StoreIntervalSeconds == 0 { - // инициализируем сихнронную запись, - // при этом сохраняться в конце нам не понадобится - log.Info().Msgf("Установлено синхронное сохранение в %v в при записи", fs.FileName) - return storage.AsOperator(storage.NewSyncDump(&fs)), func() { - log.Info().Msg("Хранилище сихронной записи получило сигнал о завершении, но дополнительно сохранение не нужно") - } - } - - // Запускаем интервальную запись и создаем токен отмены, при необходимости сюда можно будет добавить и группу ожидания - s := storage.NewIntervalDump(&fs, time.Duration(cfg.StoreIntervalSeconds)*time.Second) - ctx, cancel := context.WithCancel(context.Background()) - go s.StartDumping(ctx) - - log.Info().Msgf("Установлено сохранение с интервалом %v в %v в при записи", s.Interval, s.FileName) - - return storage.AsOperator(s), func() { - // обертка сделана под группу ожидаения - cancel() - } - - // TODO - // - // Пока что судя по всему эта функция эвакуируем моё хранилище из стека, мне кажется, но не мапы - // Как вариант сразу создавать FileStorage, и просто не оборачивать его если надо -} diff --git a/cmd/agent/config.go b/internal/config/agent.go similarity index 85% rename from cmd/agent/config.go rename to internal/config/agent.go index 4130dc5b..a8379c66 100644 --- a/cmd/agent/config.go +++ b/internal/config/agent.go @@ -1,4 +1,4 @@ -package main +package config import ( "flag" @@ -11,22 +11,16 @@ import ( //"github.com/octago/sflags/gen/gflag" сделать свой репозиторий и залить его всем в ПР ) -type config struct { +type Agent struct { Addr string `env:"ADDRESS" flag:"~a" desc:"(строка) адрес сервера в формате host:port"` ReportInterval uint `env:"REPORT_INTERVAL" flag:"~r" desc:"(число, секунды) частота отправки данных на сервер"` PollingInterval uint `env:"POLLING_INTERVAL" flag:"~p" desc:"(число, секунды) частота отпроса метрик"` Key string `env:"KEY" flag:"~p" desc:"(строка) секретный ключ подписи"` } -var defaultConfig = config{ - Addr: "localhost:8080", - ReportInterval: 10, - PollingInterval: 2, -} - -// mustConfigure парсит настройки адреса сервера, и частоты опроса и отправки +// Parse парсит настройки адреса сервера, и частоты опроса и отправки // из командной строки и переменных окружения. В приоритете переменные окружения -func mustConfigure(defaults config) (cfg config) { +func (cfg *Agent) Parse(defaults Agent) error { // todo // сделать репозиторий sflags домашним, чтобы он мог устанавливаться от меня хотя бы // github.com/octago/sflags, сейчас там ошибка в go.mod @@ -39,14 +33,13 @@ func mustConfigure(defaults config) (cfg config) { flag.Parse() - err := env.Parse(&cfg) + err := env.Parse(cfg) if err != nil { - panic(err) + return err } log.Info().Msgf("Запущено с настройками %+v", cfg) - - return + return nil } func init() { diff --git a/cmd/agent/config_test.go b/internal/config/agent_test.go similarity index 64% rename from cmd/agent/config_test.go rename to internal/config/agent_test.go index 492b3bd2..f00e867c 100644 --- a/cmd/agent/config_test.go +++ b/internal/config/agent_test.go @@ -1,4 +1,4 @@ -package main +package config import ( "flag" @@ -10,93 +10,93 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_configure(t *testing.T) { +func Test_configureAgent(t *testing.T) { tests := []struct { name string - defaults config + defaults Agent env map[string]string commandLine string - wantCfg config - panic bool + wantCfg Agent + wantErr bool }{ { name: "без параметров строки", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: nil, commandLine: "agent", - wantCfg: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + wantCfg: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, }, { name: "Указать сервер через командную строку", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: nil, commandLine: "agent -a localhost:8092", - wantCfg: config{Addr: "localhost:8092", ReportInterval: 2, PollingInterval: 1}, + wantCfg: Agent{Addr: "localhost:8092", ReportInterval: 2, PollingInterval: 1}, }, { name: "Указать ключ через командную строку", - defaults: config{Key: "will be rewrited"}, + defaults: Agent{Key: "will be rewrited"}, env: nil, commandLine: "agent -k abcde", - wantCfg: config{Key: "abcde"}, + wantCfg: Agent{Key: "abcde"}, }, { name: "Указать ключ через переменные окружения", - defaults: config{Key: "will be rewrited"}, + defaults: Agent{Key: "will be rewrited"}, env: map[string]string{"KEY": "qwerty"}, commandLine: "agent -k abcde", - wantCfg: config{Key: "qwerty"}, + wantCfg: Agent{Key: "qwerty"}, }, { name: "Указать интервалы через командную строку", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: nil, commandLine: "agent -a localhost:8092 -r 10 -p 11", - wantCfg: config{Addr: "localhost:8092", ReportInterval: 10, PollingInterval: 11}, + wantCfg: Agent{Addr: "localhost:8092", ReportInterval: 10, PollingInterval: 11}, }, { name: "Указать адрес через переменную окружения", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: map[string]string{"ADDRESS": "localhost:8088"}, commandLine: "agent -a localhost:8092", - wantCfg: config{Addr: "localhost:8088", ReportInterval: 2, PollingInterval: 1}, + wantCfg: Agent{Addr: "localhost:8088", ReportInterval: 2, PollingInterval: 1}, }, { name: "Указать все через переменную окружения", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: map[string]string{"ADDRESS": "localhost:8088", "REPORT_INTERVAL": "3", "POLLING_INTERVAL": "4"}, commandLine: "agent -a localhost:8092", - wantCfg: config{Addr: "localhost:8088", ReportInterval: 3, PollingInterval: 4}, + wantCfg: Agent{Addr: "localhost:8088", ReportInterval: 3, PollingInterval: 4}, }, { name: "Отрицательное значение интервала вызывает панику в командной строке", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: map[string]string{"ADDRESS": "localhost:8088"}, commandLine: "agent -r -1", - panic: true, + wantErr: true, }, { name: "Отрицательное значение интервала вызывает панику в командной строке 2", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: map[string]string{"ADDRESS": "localhost:8088"}, commandLine: "agent -p -1", - panic: true, + wantErr: true, }, { name: "Отрицательное значение интервала вызывает панику в переменной окружения 3", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: map[string]string{"ADDRESS": "localhost:8088", "REPORT_INTERVAL": "-1"}, commandLine: "agent", - panic: true, + wantErr: true, }, { name: "Отрицательное значение интервала вызывает панику в переменной окружения 3", - defaults: config{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, + defaults: Agent{Addr: "localhost:8081", ReportInterval: 2, PollingInterval: 1}, env: map[string]string{"ADDRESS": "localhost:8088", "POLLING_INTERVAL": "-1"}, commandLine: "agent", - panic: true, + wantErr: true, }, } for _, tt := range tests { @@ -125,14 +125,20 @@ func Test_configure(t *testing.T) { defer func() { r := recover() if r != nil { - assert.True(t, tt.panic, "Panicked but should not") + assert.True(t, tt.wantErr, "Panicked but should not") return } - assert.False(t, tt.panic, "We not panicked but should") }() - //проведем конфигурацию - assert.True(t, reflect.DeepEqual(tt.wantCfg, mustConfigure(tt.defaults)), "Итоговая конфигурация не совпадает с ожидаемой") + cfg := Agent{} + + err := cfg.Parse(tt.defaults) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.True(t, reflect.DeepEqual(tt.wantCfg, cfg), "Итоговая конфигурация не совпадает с ожидаемой") }) } } diff --git a/internal/config/connstring.go b/internal/config/connstring.go new file mode 100644 index 00000000..751bec52 --- /dev/null +++ b/internal/config/connstring.go @@ -0,0 +1,37 @@ +package config + +import "strings" + +// ConnectionString создан для хранения строки соединения с БД. При попытке вывести такую структуру +// в консоль, часть строки с паролем покроется звездочкой. +// Пока что поддерживает только фомат "host= user= ..." +type ConnectionString struct { + s string +} + +func (c ConnectionString) String() string { + var result []string + for _, ss := range strings.Split(c.s, " ") { + if strings.HasPrefix(ss, "password") { + continue + } + result = append(result, ss) + } + return strings.Join(result, " ") +} + +func (c ConnectionString) Get() string { + return c.s +} + +// Set воплощает интерфейс Value для пакета flag +func (c *ConnectionString) Set(s string) error { + c.s = s + return nil +} + +// UnmarshalText воплощает интерфейс TextUnmarshaler +// пакета carlos0/env +func (c *ConnectionString) UnmarshalText(text []byte) error { + return c.Set(string(text)) +} diff --git a/internal/config/secret.go b/internal/config/secret.go new file mode 100644 index 00000000..b4331e1a --- /dev/null +++ b/internal/config/secret.go @@ -0,0 +1,38 @@ +package config + +// Secret сделан специально для хранения деликатных данных, таких как пароли и ключ +// если случайно попадёт в Print(), то максимум в консоли появятся звездочки. +// Выдает значение только через функцию. +// +// Вообще изначально был план вообще сделать Secret как бы переименованием func() string +// но тогда надо постоянно проверять на nil, иначе все падает. Это не оч надежно канеш +type Secret struct { + value []byte +} + +func (k Secret) ValueFunc() func() []byte { + return func() []byte { return k.value } +} + +func (k Secret) IsEmpty() bool { + return len(k.value) == 0 +} + +// String воплощает интерфейс fmt.Stringer, +// и возвращает звездочки, чтобы случайно в консоль не вывелось +func (k Secret) String() string { + return "********" +} + +// Set воплощает интерфейс Value для пакета flag +func (k *Secret) Set(s string) error { + k.UnmarshalText([]byte(s)) + return nil +} + +// UnmarshalText воплощает интерфейс TextUnmarshaler +// пакета carlos0/env +func (k *Secret) UnmarshalText(text []byte) error { + k.value = text + return nil +} diff --git a/internal/config/server.go b/internal/config/server.go new file mode 100644 index 00000000..bedcc6d8 --- /dev/null +++ b/internal/config/server.go @@ -0,0 +1,162 @@ +package config + +import ( + "context" + "database/sql" + "flag" + "fmt" + "os" + "time" + + "github.com/caarlos0/env/v6" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/thefrol/kysh-kysh-meow/internal/server/api" + "github.com/thefrol/kysh-kysh-meow/internal/storage" +) + +type Server struct { + Addr string `env:"ADDRESS"` + StoreIntervalSeconds uint `env:"STORE_INTERVAL"` + FileStoragePath string `env:"FILE_STORAGE_PATH"` + Restore bool `env:"RESTORE"` + DatabaseDSN ConnectionString `env:"DATABASE_DSN"` + Key Secret `env:"KEY"` +} + +// Parse парсит командную строку и переменные окружения, чтобы выдать структуру с конфигурацией сервера. +// В приоритете переменные окружения. Принимает на вход структуру defaults со значениями по умолчанию. +// +// Приоритет такой: +// - Если другого не указано, будет использоваться defaults +// - То, что указано в командной строке переписывает то, что указано в defaults +// - То, что указано в переменной окружения, переписывает то, что было указано ранее +func (cfg *Server) Parse(defaults Server) error { + // устанавливаем дополнительные значения по умолчанию + cfg.DatabaseDSN.s = defaults.DatabaseDSN.s + + // парсим командную строку + flag.StringVar(&cfg.Addr, "a", defaults.Addr, "[адрес:порт] устанавливает адрес сервера ") + flag.UintVar(&cfg.StoreIntervalSeconds, "i", defaults.StoreIntervalSeconds, "[время, сек] интервал сохранения показаний. При 0 запись делается почти синхронно") + flag.StringVar(&cfg.FileStoragePath, "f", defaults.FileStoragePath, "[строка] путь к файлу, откуда будут читаться при запуске и куда будут сохраняться метрики полученные сервером, если файл пустой, то сохранение будет отменено") + flag.BoolVar(&cfg.Restore, "r", defaults.Restore, "[флаг] если установлен, загружает из файла ранее записанные метрики") + flag.Var(&cfg.DatabaseDSN, "d", "[строка] подключения к базе данных") + flag.Var(&cfg.Key, "k", "строка, секретный ключ подписи") + + flag.Parse() + err := env.Parse(cfg) + if err != nil { + return err + } + + // Тут обрабатываем особый случай. Если переменная окружения установлена, но в пустое значение + // то мы перезаписываем установленный командной строкой флаг на пуское значение, хотя штатно + // этого не было бы сделано + if v, ok := os.LookupEnv("FILE_STORAGE_PATH"); ok { + cfg.FileStoragePath = v + } + + log.Info().Msgf("Запущено с настройками %+v", cfg) + return nil +} + +// ConfigureStorage подготавливает хранилище к работе в соответствии с текущими настройками, +// при необходимости загружает из файла значения метрик, запускает сохранение в файл, и +// возвращает интерфейс хранилища и функцию, подготавливающая ханилище к остановке +// +// На входе получает экземпляр хранилища m, и далее оборачивает его другим классов, +// наиболее соответсвующим задаче, исходя из cfg +func (cfg Server) MakeStorage(ctx context.Context) (api.Operator, error) { + // 0. Если указана база данных, создаем хранилище с базой данных + // 1. Если путь не задан, то возвращаем хранилище в оперативке, без приблуд + // 2. Иначе оборачиваем файловым хранилищем, но не возвращаем пока + // 3. Если Restore=true, то читаем из файла. Если файла не существует, то игнорируем проблему + // 4. Оборачиваем файловое хранилище сихнронным или интервальным + + // TODO + // + // Думаю эта функция должна получать на вход контекст, а не возвращать CancelFunc + // + // И нужно убрать отсюда все паники + + // Если база данных + if cfg.DatabaseDSN.Get() != "" { + db, err := sql.Open("pgx", cfg.DatabaseDSN.Get()) + if err != nil { + return nil, fmt.Errorf("не могу создать соединение с БД: %w", err) + } + + dbs, err := storage.NewDatabase(db) + if err != nil { + return nil, fmt.Errorf("ошибка создания хранилища в базе данных: %v", err) + } + + if err := dbs.Check(context.TODO()); err != nil { + log.Warn().Msgf("Нет соединения с БД - %v", err) + } + + go func() { + <-ctx.Done() + log.Info().Msg("Закрываю бд") + err := db.Close() + if err != nil { + log.Error().Msgf("Не могу закрыть базу данных: %v", err) + } + log.Info().Msg("Успешно закрыл БД") + + // конечно, это должно быть в store.close() + }() + + log.Info().Msg("Создано хранилише в Базе данных") + return dbs, nil + } + + // Если не база данных, то начинаем с начала - создаем хранилище в памяти, и оборачиваем его всякими штучками если надо + m := storage.New() + + // Если путь до хранилища не пустой, то нам нужно инициаизировать обертки над хранилищем + if cfg.FileStoragePath == "" { + log.Info().Msg("Установлено хранилище в памяти. Сохранение на диск отключено") + return storage.AsOperator(m), nil + } + + // Оборачиваем файловым хранилищем, в случае, есл и + fs := storage.NewFileStorage(&m, cfg.FileStoragePath) + if cfg.Restore { + err := fs.Restore() + // в случае, если файла не существует, игнорируем эту проблему + if err != nil && err != storage.ErrorRestoreFileNotExist { + panic(err) + } + log.Info().Msgf("Значения метрик загружены из %v", fs.FileName) + } + + if cfg.StoreIntervalSeconds == 0 { + // инициализируем сихнронную запись, + // при этом сохраняться в конце нам не понадобится + log.Info().Msgf("Установлено синхронное сохранение в %v в при записи", fs.FileName) + return storage.AsOperator(storage.NewSyncDump(&fs)), nil + } + + // Запускаем интервальную запись + s := storage.NewIntervalDump(&fs, time.Duration(cfg.StoreIntervalSeconds)*time.Second) + go s.StartDumping(ctx) + + log.Info().Msgf("Установлено сохранение с интервалом %v в %v в при записи", s.Interval, s.FileName) + + return storage.AsOperator(s), nil +} + +func init() { + // настраиваем дружелюбный цветастый вывод логгера + log.Logger = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stderr}) + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + + // добавляет смайлик кота в конец справки flags + flag.Usage = func() { + flag.PrintDefaults() + fmt.Println("^-^") + } +} + +// TODO может быть storage должно иметь что-то типа Close()? diff --git a/cmd/server/config_test.go b/internal/config/server_test.go similarity index 62% rename from cmd/server/config_test.go rename to internal/config/server_test.go index 12213e69..3f5f3980 100644 --- a/cmd/server/config_test.go +++ b/internal/config/server_test.go @@ -1,4 +1,4 @@ -package main +package config import ( "flag" @@ -8,116 +8,138 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func Test(t *testing.T) { + a := ConnectionString{s: "124"} + b := ConnectionString{s: "124"} + assert.True(t, reflect.DeepEqual(a, b)) +} + func Test_configure(t *testing.T) { tests := []struct { name string - defaults config + defaults Server env map[string]string commandLine string - wantCfg config - panic bool + wantCfg Server + wantErr bool }{ { name: "без параметров строки", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, env: nil, commandLine: "serv", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, }, { name: "restore установить флагом", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: nil, commandLine: "serv -r", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, }, { name: "restore отменить флагом", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, env: nil, commandLine: "serv -r=false", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, }, { name: "restore со значением через переменную окружения", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true"}, commandLine: "serv -r", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "/tmp/file"}, }, { name: "Указать ключ через командную строку", - defaults: config{Key: "will be rewrited"}, + defaults: Server{Key: newSecret("will be rewrited"), Restore: true}, env: nil, commandLine: "serv -k abcde", - wantCfg: config{Key: "abcde", Restore: true}, + wantCfg: Server{Key: newSecret("abcde"), Restore: true}, }, { name: "Указать ключ через переменные окружения", - defaults: config{Key: "will be rewrited"}, + defaults: Server{Key: newSecret("will be rewrited"), Restore: true}, env: map[string]string{"KEY": "qwerty"}, commandLine: "serv -k abcde", - wantCfg: config{Key: "qwerty", Restore: true}, + wantCfg: Server{Key: newSecret("qwerty"), Restore: true}, }, { name: "командной строкой указать файл куда писать", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true"}, commandLine: "serv -f 12342", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "12342"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "12342"}, }, { name: "переменной окружения указать файл куда писать", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true", "FILE_STORAGE_PATH": "123"}, commandLine: "serv -f 1234", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "123"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: "123"}, }, { name: "командной строкой указать пустую строчку для файла", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true"}, commandLine: `serv -f `, - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: ""}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: ""}, }, { name: "переменной окружения указать пустую строку для файла", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true", "FILE_STORAGE_PATH": ""}, commandLine: "serv -r", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: ""}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: true, FileStoragePath: ""}, }, { name: "командной строкой указать интервал записи", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true"}, commandLine: "serv -i 299", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 299, Restore: true, FileStoragePath: "/tmp/file"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 299, Restore: true, FileStoragePath: "/tmp/file"}, }, { name: "переменной окружения указать интервал записи", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true", "STORE_INTERVAL": "123"}, commandLine: "serv -i 22", - wantCfg: config{Addr: "localhost:8081", StoreIntervalSeconds: 123, Restore: true, FileStoragePath: "/tmp/file"}, + wantCfg: Server{Addr: "localhost:8081", StoreIntervalSeconds: 123, Restore: true, FileStoragePath: "/tmp/file"}, + }, + + { + name: "командной строкой указать строку подключения к бд", + defaults: Server{Restore: false, DatabaseDSN: newConnString("123")}, + env: map[string]string{}, + commandLine: "serv -d qwqwqw", + wantCfg: Server{DatabaseDSN: newConnString("qwqwqw"), Restore: false}, + }, + { + name: "переменной указать строку подключения к бд", + defaults: Server{Restore: false}, + env: map[string]string{"DATABASE_DSN": "1232"}, + commandLine: "serv -d qwqwqw", + wantCfg: Server{DatabaseDSN: newConnString("1232"), Restore: false}, }, { name: "отрицательный интервал записи в командной строке вызывает панику", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true"}, commandLine: "serv -i -22", - panic: true, + wantErr: true, }, { name: "отрицательный интервал записи в переменной окружения вызывает панику", - defaults: config{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, + defaults: Server{Addr: "localhost:8081", StoreIntervalSeconds: 300, Restore: false, FileStoragePath: "/tmp/file"}, env: map[string]string{"RESTORE": "true", "STORE_INTERVAL": "-2"}, commandLine: "serv", - panic: true, + wantErr: true, }, } for _, tt := range tests { @@ -145,19 +167,32 @@ func Test_configure(t *testing.T) { // отловим панику defer func() { - r := recover() - if r != nil { - assert.True(t, tt.panic, "Panicked but should not") + if r := recover(); r != nil { + assert.Truef(t, tt.wantErr, "Panicked but should not:%v", r) return } - assert.False(t, tt.panic, "We not panicked but should") }() //проведем конфигурацию - actual := mustConfigure(tt.defaults) + actual := Server{} + err := actual.Parse(tt.defaults) + if tt.wantErr { + assert.Error(t, err, "Должна быть ошибка, но ее нет") + return + } + require.NoError(t, err) assert.True(t, reflect.DeepEqual(tt.wantCfg, actual), "Итоговая конфигурация не совпадает с ожидаемой") }) } + +} + +func newSecret(s string) Secret { + return Secret{value: []byte(s)} +} + +func newConnString(s string) ConnectionString { + return ConnectionString{s: s} } diff --git a/internal/config/types_test.go b/internal/config/types_test.go new file mode 100644 index 00000000..b4bd7102 --- /dev/null +++ b/internal/config/types_test.go @@ -0,0 +1,25 @@ +package config_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/thefrol/kysh-kysh-meow/internal/config" +) + +func Test_HideSecretFromConsole(t *testing.T) { + original_key := "my_key" + key := config.Secret{} + key.Set(original_key) + output_to_console := fmt.Sprint(key) + assert.NotContains(t, output_to_console, original_key, "Вывод в консоль не должен содержать ключ исходный") +} + +func Test_HidePasswordFromConsole(t *testing.T) { + connstring := "host=localhost password=12342 dbname=123" + cs := config.ConnectionString{} + cs.Set(connstring) + output_to_console := fmt.Sprint(cs) + assert.NotContains(t, output_to_console, "password", "Вывод в консоль не должен содержать пароль") +} diff --git a/internal/server/app/README.md b/lib/graceful/README.md similarity index 100% rename from internal/server/app/README.md rename to lib/graceful/README.md diff --git a/lib/graceful/serve.go b/lib/graceful/serve.go new file mode 100644 index 00000000..76730224 --- /dev/null +++ b/lib/graceful/serve.go @@ -0,0 +1,63 @@ +package graceful + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/rs/zerolog/log" +) + +// Serve запускает сервер с поддержкой нежного завершения. Сервер можно будет выключить через +// SIGINT, SIGTERM, SIGQUIT +func Serve(addr string, router http.Handler) { + server := http.Server{Addr: addr, Handler: router} + + // запустим горутину, которая будет слушать сигналы от системы, и при получении + // начнет процедуру остановки сервера + go func() { + <-RequestStop() + log.Debug().Msg("server wants to shut down") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + ShutdownGracefullyContext(shutdownCtx, &server) + }() + + log.Info().Msgf("^.^ Мяу, сервер запускается по адресу %v!", addr) + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Error().Msgf("^0^ не могу запустить сервер: %v \n", err) + } + log.Error().Msg("Run() остановлен") + + // если ошибка при запуске сервера, то горутина не не получит сигнал, но в общем её вырубит система как бэ +} + +// RequestStop возвращает канал через который придёт сообщение, что операционная система запросила завершение работы +func RequestStop() chan os.Signal { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + return sig +} + +func ShutdownGracefullyContext(ctx context.Context, serv *http.Server) { + go func() { + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + log.Fatal().Msg("Время вышло, сервер будет завершен принудительно") + return + } + }() + + // Trigger graceful shutdown + err := serv.Shutdown(ctx) + if err != nil { + log.Fatal().Msg(err.Error()) + panic(err) + } + log.Info().Msg("^-^ рутина остановки сервера завершилась") +} From 9de77987dfd7713c7283b4a309f9df8395376f0e Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 30 Oct 2023 04:38:31 +0300 Subject: [PATCH 53/55] =?UTF-8?q?fix:=20config=20=D0=9D=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B2=20=D1=81=D1=82=D0=B8=D0=BB=D0=B5=20?= =?UTF-8?q?Go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/types_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/config/types_test.go b/internal/config/types_test.go index b4bd7102..83dcd844 100644 --- a/internal/config/types_test.go +++ b/internal/config/types_test.go @@ -9,17 +9,17 @@ import ( ) func Test_HideSecretFromConsole(t *testing.T) { - original_key := "my_key" + originalKey := "my_key" key := config.Secret{} - key.Set(original_key) - output_to_console := fmt.Sprint(key) - assert.NotContains(t, output_to_console, original_key, "Вывод в консоль не должен содержать ключ исходный") + key.Set(originalKey) + consoleOutput := fmt.Sprint(key) + assert.NotContains(t, consoleOutput, originalKey, "Вывод в консоль не должен содержать ключ исходный") } func Test_HidePasswordFromConsole(t *testing.T) { - connstring := "host=localhost password=12342 dbname=123" + connString := "host=localhost password=12342 dbname=123" cs := config.ConnectionString{} - cs.Set(connstring) - output_to_console := fmt.Sprint(cs) - assert.NotContains(t, output_to_console, "password", "Вывод в консоль не должен содержать пароль") + cs.Set(connString) + consoleOutput := fmt.Sprint(cs) + assert.NotContains(t, consoleOutput, "password", "Вывод в консоль не должен содержать пароль") } From 9ae6e0d3ec946a31886d8d099028e614f6ee0908 Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 30 Oct 2023 04:42:22 +0300 Subject: [PATCH 54/55] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=B5=D0=B8=D0=BC=D1=83=D1=89?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B2=D0=B0=20=D0=B2=20=D0=A0=D0=98=D0=94?= =?UTF-8?q?=D0=9C=D0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 217ad7c8..300370d1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ + Фантастическая скорость работы достигается статической типизацией, использованием стека + Сладкая, как `торт наполеон`, и слоистая архитектура. Все зависимости идут снизу вверх. И нет циклических зависимостей. `DDD` во все поля + Защищает связь сервера и агента при помощи подписей `SHA256` -+ Сжимает запросы и ответы ++ Сжимает запросы и ответы при помощи `gzip` ++ Превосходное оформление коммитов, ишью и пул-реквестов, прохождение код-ревью. Гитхаб использован как только возможно. + Все пакеты и функции документированы + Модульно, предметно ориантировано + Более ста тестов @@ -32,11 +33,6 @@ SecretValue:****** DataBaseString: host=localhost user=user ... // хотя тут был пароль -## TODO - -0. [ ] Создать контроллер-абстрацию, с главной [логикой приложения](./internal/server/api/README.MD) собсно. -1. [ ] Дождаться завершения горутин. Как? через sync.WaitGroup()? - ## Открытия ### `net/http` From c8df66e9396ef3eee86860fef9265c343257fa0a Mon Sep 17 00:00:00 2001 From: Dima Frolenko Date: Mon, 30 Oct 2023 04:49:50 +0300 Subject: [PATCH 55/55] =?UTF-8?q?docs:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=BA=D1=83=20=D0=B2=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 300370d1..771972e2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ + Сжимает запросы и ответы при помощи `gzip` + Превосходное оформление коммитов, ишью и пул-реквестов, прохождение код-ревью. Гитхаб использован как только возможно. + Все пакеты и функции документированы -+ Модульно, предметно ориантировано ++ Модульно, предметно ориентировано + Более ста тестов ## Стек