diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index e8dd78e3..d4468463 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -8,8 +8,8 @@ import ( "github.com/rs/zerolog/log" "github.com/thefrol/kysh-kysh-meow/internal/collector" - "github.com/thefrol/kysh-kysh-meow/internal/collector/compress" "github.com/thefrol/kysh-kysh-meow/internal/collector/report" + "github.com/thefrol/kysh-kysh-meow/internal/collector/report/compress" "github.com/thefrol/kysh-kysh-meow/internal/config" "github.com/thefrol/kysh-kysh-meow/lib/graceful" ) diff --git a/internal/collector/compress/compress.go b/internal/collector/report/compress/compress.go similarity index 100% rename from internal/collector/compress/compress.go rename to internal/collector/report/compress/compress.go diff --git a/internal/collector/compress/levels.go b/internal/collector/report/compress/levels.go similarity index 100% rename from internal/collector/compress/levels.go rename to internal/collector/report/compress/levels.go diff --git a/internal/collector/report/send.go b/internal/collector/report/send.go index f7e7d121..39cde0d5 100644 --- a/internal/collector/report/send.go +++ b/internal/collector/report/send.go @@ -8,8 +8,7 @@ import ( "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" - "github.com/thefrol/kysh-kysh-meow/internal/collector/compress" - "github.com/thefrol/kysh-kysh-meow/internal/collector/internal/pollcount" + "github.com/thefrol/kysh-kysh-meow/internal/collector/report/compress" "github.com/thefrol/kysh-kysh-meow/internal/metrica" "github.com/thefrol/kysh-kysh-meow/internal/sign" "github.com/thefrol/kysh-kysh-meow/lib/retry" @@ -20,7 +19,8 @@ var ( ErrorRequestRejected = errors.New("запрос не принят, статус код не 200") ) -var defaultClient = resty.New() +// это клиент, которым будет пользоваться наш пакет для отправки +var сlient = resty.New() // Send отправляет метрики из указанного хранилища store на сервер host. // При возникновении ошибок будет стараться отправить как можно больше метрик, @@ -37,7 +37,7 @@ func Send(metricas []metrica.Metrica, url string) error { 4. Попробовать отправить preparedRequest, если не получится, то ничего страшного 5. Если получилось, обнуляем pollCounter */ - preparedRequest := defaultClient.R() + preparedRequest := сlient.R() preparedRequest.Header.Set("Content-Type", "application/json") var b []byte // тут будет тело, которое в итоге запишем в сообщение @@ -113,9 +113,6 @@ func Send(metricas []metrica.Metrica, url string) error { return fmt.Errorf("%w: сервер вернул %v: %v", ErrorRequestRejected, resp.StatusCode(), string(resp.Body())) } - // Если сервер принял, то сбрасываем счетчик - pollcount.Drop() - return nil } diff --git a/internal/collector/report/send_test.go b/internal/collector/report/send_test.go index fc2965d1..1afe7b41 100644 --- a/internal/collector/report/send_test.go +++ b/internal/collector/report/send_test.go @@ -8,7 +8,6 @@ import ( "net/http" "net/http/httptest" "reflect" - "regexp" "strings" "testing" @@ -111,45 +110,13 @@ func (server *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // которая работает как замыкаение r.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewBuffer(bb)), nil - } + } // todo а вот тут могла бы и пригодиться подмена // добавляем запросы в массив запросов. Теперь каждый такой запрос помнит и тело своего запроса server.requests = append(server.requests, r) } -// routesUsed возвращает марштуры по которым проходили запросы -func (server testHandler) routesUsed() (routes []string) { - for _, r := range server.requests { - routes = append(routes, r.URL.Path) - } - return -} - -// containsRoute возвращает true, если в принятых сервером маршрутах находится такой. -// Задается регулярным вырежаением pattern -func (server testHandler) containsRoute(pattern string) (bool, error) { - for _, r := range server.requests { - found, err := regexp.MatchString(pattern, r.URL.Path) - if err != nil { - return false, err - } - if found { - return true, nil - } - } - return false, nil -} - // возвращает количество полученных запросов func (server testHandler) NumRequests() int { return len(server.requests) } - -// stringerWrap позволяет использовать интерфейс стрингер в полях структуры. Позволяя запихивать туда люую переменную, которую можно обратить в строку -type stringerWrap struct { - text string -} - -func (s stringerWrap) String() string { - return s.text -} diff --git a/internal/server/middleware/signing.go b/internal/server/middleware/signing.go index b8672752..dec3b952 100644 --- a/internal/server/middleware/signing.go +++ b/internal/server/middleware/signing.go @@ -11,82 +11,91 @@ import ( "github.com/thefrol/kysh-kysh-meow/lib/intercept" ) -const SignBufferSize = 1500 - -func Signing(key string) func(http.Handler) http.Handler { +// Это размер буфера, который будет использован +// для перехвата тела ответа. Чтобы лишний раз не +// гонять память, можно указать какой-то размер буфера +// который не надо будет аллоцировать. +// +// В идеале сюда должен полностью поместиться стандартный +// ответ сервера +const MinBufferSize = 15 + +// CheckSignature это мидлварь, которая проверяет подписи полученных +// запросов, используя пакет sign и ключ шифрования key. Подпись +// мы получаем из заголовка sign.SigningHeaderName +func CheckSignature(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, "Нет подписи") - //w.WriteHeader(http.StatusBadRequest) - //w.Write([]byte("no sign")) - //log.Info().Msg("Нет подписи") next.ServeHTTP(w, r) return } - if r.GetBody == nil { - - // todo - // - // Влад рекомендкует вот такую темку - // - // body, err := io.ReadAll(r.Body) - // buf := bytes.NewBuffer(body) - // r.Body = io.NopCloser(bytes.NewBuffer(body)) - - 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() + // подменим тело запроса + // то есть прочитаем все из тела запроса + // потом запишем это все обратно + body, err := io.ReadAll(r.Body) if err != nil { - api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "Cant get request boby, signing check failed") + api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "Не удалось прочитать тело запроса") return } - defer body.Close() - - data := make([]byte, SignBufferSize) - // bug если буфер меньше чем сообщение, - // то типа не прочитается и подпись не сможет валидироваться - // но большое буфер тоже не охота делать - // TODO!!! - n, err := body.Read(data) + + // закрываем тело исходного запроса + err = r.Body.Close() if err != nil { - api.HTTPErrorWithLogging(w, http.StatusInternalServerError, "cant read body") + log.Error().Msg("Не могу закрыть тело сообщения в подписывающей мидвари") return } - if err := sign.Check(data[:n], keyBytes, receivedSign); err != nil { - api.HTTPErrorWithLogging(w, http.StatusNotFound, "Подпись не прошла проверку") + // и подменяем тело исходного запроса + r.Body = io.NopCloser(bytes.NewBuffer(body)) + + // проверим подпись запроса + if err := sign.Check(body, keyBytes, receivedSign); err != nil { + api.HTTPErrorWithLogging(w, http.StatusBadRequest, "Подпись не прошла проверку") return } - // теперь займемся ответом: - buf := bytes.NewBuffer(data[:0]) + next.ServeHTTP(w, r) + }) + } +} + +// SignResponse подписывает ответы сервера ключом, +// получившуюся подпись будет положена в заголовок +// sign.SignHeaderName +func SignResponse(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) { + + // Перехватим все, что последующие обработчики запишут + // в ответ. И это все будет лежать в buf + buf := bytes.NewBuffer(make([]byte, 0, MinBufferSize)) faker := intercept.WithBuffer(w, buf) + // запускаем дальше по цепочке обработчики + // результат их работы будет записан в faker next.ServeHTTP(faker, r) - // теперь запишем все, что мы забуферизировали - + // в buf хранится буферизированный ответ, + // теперь мы посчитаем подпись для него 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("Запрос подписан") diff --git a/internal/server/middleware/signing_test.go b/internal/server/middleware/signing_test.go new file mode 100644 index 00000000..9084e14c --- /dev/null +++ b/internal/server/middleware/signing_test.go @@ -0,0 +1,103 @@ +package middleware + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/thefrol/kysh-kysh-meow/internal/sign" +) + +// Handler простая ручка для тестового сервера запросов +// перед ней будет стоять мидлварь, которая при ошибке +// подписи тупо не пропустит до сюда +func Handler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Response")) +} + +// тест проверяет работу подписывающей мидлвари. +// Она должна подписать и прочитать запрос +func TestSigning(t *testing.T) { + // Ключ шифрования + var ( + key = "123" + ) + + // Запрос + var ( + body = "data_for_signing" + requestSign = "CyHw9JM8fq5pvtsVbGVPYq/+qXahhUCyl18O85/DFt8=" + ) + + // Ответ + var ( + statusCode = http.StatusOK + responseSign = "XdNsy/vA+t7X+zIELkzdoHbvXylCa3F2vxBlf0y3kQo=" + ) + + //создадим наш сервер + route := "/path" + + s := chi.NewRouter() + s.Use(CheckSignature(key), SignResponse(key)) + s.Get(route, Handler) + + // создадим запрос + r := httptest.NewRequest(http.MethodGet, route, bytes.NewBuffer([]byte(body))) + r.Header.Set(sign.SignHeaderName, requestSign) + + // запускаем запрос + w := httptest.NewRecorder() + s.ServeHTTP(w, r) + + // обрабатываем ответ + res := w.Result() + + assert.Equal(t, statusCode, res.StatusCode) + assert.Equal(t, responseSign, res.Header.Get(sign.SignHeaderName)) +} + +// тест проверяет работу подписывающей мидлвари. +// Приходящий запрос имеем неправильную подпись +func Test_Signing_BadSign(t *testing.T) { + // Ключ шифрования + var ( + key = "123" + ) + + // Запрос + // + // тут подпись запроса не валидная + var ( + body = "data_for_signing" + requestSign = "bad_sign_CyHw9JM8fq5pvtsVbGVPYq/+qXahhUCyl18O85/DFt8=" + ) + + // Ответ + var ( + statusCode = http.StatusBadRequest + ) + + //создадим наш сервер + route := "/path" + + s := chi.NewRouter() + s.Use(CheckSignature(key)) + s.Get(route, Handler) + + // создадим запрос + r := httptest.NewRequest(http.MethodGet, route, bytes.NewBuffer([]byte(body))) + r.Header.Set(sign.SignHeaderName, requestSign) + + // запускаем запрос + w := httptest.NewRecorder() + s.ServeHTTP(w, r) + + // обрабатываем ответ + res := w.Result() + + assert.Equal(t, statusCode, res.StatusCode) +} diff --git a/internal/server/router/router.go b/internal/server/router/router.go index bc65428c..ec7533f5 100644 --- a/internal/server/router/router.go +++ b/internal/server/router/router.go @@ -26,10 +26,12 @@ func MeowRouter(store api.Operator, key string) (router chi.Router) { // настраиваем мидлвари, логгер, распаковщик и запаковщик router.Use(middleware.MeowLogging()) if key != "" { - router.Use(middleware.Signing(key)) + router.Use( + middleware.CheckSignature(key), + middleware.SignResponse(key)) } router.Use(middleware.UnGZIP) - router.Use(middleware.GZIP(CompressionTreshold, CompressionBufferLen)) //todo нормальные константы вместо магических чисел + router.Use(middleware.GZIP(CompressionTreshold, CompressionBufferLen)) // Создаем маршруты для обработки URL запросов router.Group(func(r chi.Router) { @@ -84,9 +86,3 @@ func MeowRouter(store api.Operator, key string) (router chi.Router) { return router } - -// Кажется в голове начинает зреть понимание бизнес логики. -// Типа вне зависисмости это gRPC ли, или это HTTP, или даже если -// это в микрофон кто-то сказал - мы берем метрику и возвращаем. -// Или изменяем метрику, и записываем в хранилище. По сути это -// и есть логика нашей программы diff --git a/internal/sign/headers.go b/internal/sign/headers.go index 729ec1b3..2693b697 100644 --- a/internal/sign/headers.go +++ b/internal/sign/headers.go @@ -1,3 +1,6 @@ package sign const SignHeaderName = "HashSHA256" + +// todo вообще не очень хорошо, что у нас зависимость от этого поакета +// в сервере и агенте