package middleware import ( "bytes" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) // sentinel имитирует действующий machine token: если он окажется в журнале — // значит секрет утёк в /var/log/hy2xs/hy2xs-admin.log, который отдаётся // оператору через ExportLog и попадает в diagnostics-бандл. const sentinel = "SUPER_SECRET_SENTINEL" func captureLog(t *testing.T, request *http.Request) string { t.Helper() previousOut := logrus.StandardLogger().Out previousFormatter := logrus.StandardLogger().Formatter t.Cleanup(func() { logrus.SetOutput(previousOut) logrus.SetFormatter(previousFormatter) }) buffer := &bytes.Buffer{} logrus.SetOutput(buffer) logrus.SetFormatter(&logrus.JSONFormatter{}) gin.SetMode(gin.TestMode) engine := gin.New() engine.Use(LogHandler()) engine.POST("/internal/hysteria/auth", func(c *gin.Context) { c.Status(http.StatusOK) }) engine.GET("/api/peers", func(c *gin.Context) { c.Status(http.StatusOK) }) engine.ServeHTTP(httptest.NewRecorder(), request) return buffer.String() } // Регрессия: логировался c.Request.RequestURI, то есть путь ВМЕСТЕ с query. // Hysteria обращается к машинному endpoint'у при каждом подключении пира, так // что токен оседал в журнале в штатном режиме работы. func TestLogHandlerNeverLogsQueryValues(t *testing.T) { request := httptest.NewRequest( http.MethodPost, "/internal/hysteria/auth?access_token="+sentinel, strings.NewReader("{}"), ) logged := captureLog(t, request) if strings.Contains(logged, sentinel) { t.Fatalf("machine token попал в журнал: %s", logged) } if !strings.Contains(logged, `"reqPath":"/internal/hysteria/auth"`) { t.Fatalf("в журнале нет пути запроса: %s", logged) } if strings.Contains(logged, "reqUri") { t.Fatalf("поле reqUri вернулось в журнал: %s", logged) } } // Имена параметров для диагностики сохраняются: важно видеть, что параметр был // передан, а не что в нём лежало. func TestLogHandlerKeepsQueryKeyNames(t *testing.T) { request := httptest.NewRequest( http.MethodPost, "/internal/hysteria/auth?access_token="+sentinel, strings.NewReader("{}"), ) logged := captureLog(t, request) if !strings.Contains(logged, `"reqQueryKeys":"access_token"`) { t.Fatalf("имя query-параметра не попало в журнал: %s", logged) } } func TestLogHandlerOmitsQueryKeysWhenThereAreNone(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/api/peers", nil) logged := captureLog(t, request) if strings.Contains(logged, "reqQueryKeys") { t.Fatalf("пустой список параметров не должен появляться в журнале: %s", logged) } } // Значение секретоподобного параметра не должно попасть в журнал ни на каком // маршруте, а не только на машинном. func TestLogHandlerRedactsQueryValuesOnOperatorRoutes(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/api/peers?includeSecrets=true&name="+sentinel, nil) logged := captureLog(t, request) if strings.Contains(logged, sentinel) { t.Fatalf("значение query-параметра попало в журнал: %s", logged) } if !strings.Contains(logged, `"reqPath":"/api/peers"`) { t.Fatalf("в журнале нет пути запроса: %s", logged) } }