Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Почистить мидвари #53

Merged
merged 10 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
11 changes: 4 additions & 7 deletions internal/collector/report/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -20,7 +19,8 @@ var (
ErrorRequestRejected = errors.New("запрос не принят, статус код не 200")
)

var defaultClient = resty.New()
// это клиент, которым будет пользоваться наш пакет для отправки
var сlient = resty.New()

// Send отправляет метрики из указанного хранилища store на сервер host.
// При возникновении ошибок будет стараться отправить как можно больше метрик,
Expand All @@ -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 // тут будет тело, которое в итоге запишем в сообщение
Expand Down Expand Up @@ -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
}

Expand Down
35 changes: 1 addition & 34 deletions internal/collector/report/send_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strings"
"testing"

Expand Down Expand Up @@ -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
}
103 changes: 56 additions & 47 deletions internal/server/middleware/signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("Запрос подписан")

Expand Down
103 changes: 103 additions & 0 deletions internal/server/middleware/signing_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 4 additions & 8 deletions internal/server/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -84,9 +86,3 @@ func MeowRouter(store api.Operator, key string) (router chi.Router) {

return router
}

// Кажется в голове начинает зреть понимание бизнес логики.
// Типа вне зависисмости это gRPC ли, или это HTTP, или даже если
// это в микрофон кто-то сказал - мы берем метрику и возвращаем.
// Или изменяем метрику, и записываем в хранилище. По сути это
// и есть логика нашей программы
3 changes: 3 additions & 0 deletions internal/sign/headers.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package sign

const SignHeaderName = "HashSHA256"

// todo вообще не очень хорошо, что у нас зависимость от этого поакета
// в сервере и агенте
Loading