package http import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // journalMounts — перечень адресного пространства для проверок журнала. // Обработчиков он здесь не вешает: журналу нужны только границы, а не то, что // стоит за ними. func journalMounts() []Mount { return []Mount{ {Path: AppRoot}, {Path: HealthPath, Exact: true}, {Path: MetricsPath, Exact: true}, } } // journalRouteOf — маршрут строки журнала для названного пути. func journalRouteOf(path string) string { return JournalRoute(httptest.NewRequest(http.MethodGet, path, http.NoBody), journalMounts()) } // Путь, которым распоряжается спрашивающий, в журнал дословно не идёт — и // принадлежность сервису тут ничего не меняет: путь под корнем приложения // выбирает тот же аноним, что и путь вне корней. func TestJournalRouteHidesRequestedPath(t *testing.T) { cases := []string{ "/", "/records/abc123def456ghi", "/" + strings.Repeat("a", 1024), "/api/files/files/rec0000000000000/0f7b8dd3-d1cc-424c.mp3", "/_/", // Под корнем приложения — то же самое: образца с таким хвостом нет. "/app", "/app/", "/app/" + strings.Repeat("b", 1024), "/app/audiorecords/abc123def456ghi/file/" + strings.Repeat("c", 512), "/app/audiorecords/abc/def/ghi", } for _, path := range cases { assert.Equal(t, webappRoute, journalRouteOf(path), path) } } // Дословно пишется закрытый перечень: точные адреса наблюдения и образцы // адресов приложения. Идентификатор записи в образец не входит — он приходит // строкой обработчика полем `record_id`. func TestJournalRouteKeepsClosedList(t *testing.T) { cases := map[string]string{ "/app/audiorecords": AppRouteRecords, "/app/audiorecords/abc123def456ghi": AppRouteRecord, "/app/audiorecords/abc123def456ghi/text": AppRouteRecordText, "/app/audiorecords/abc123def456ghi/file": AppRouteRecordFile, "/app/me": AppRouteMe, "/app/config": AppRouteConfig, HealthPath: HealthPath, MetricsPath: MetricsPath, } for path, want := range cases { assert.Equal(t, want, journalRouteOf(path), path) } } // Значение маршрута берётся из перечня образцов и ничего сверх него не // возвращает: путь, приведённый маршрутизатором к другому виду, дословно уехать // не может. func TestJournalRouteAnswersOnlyFromClosedList(t *testing.T) { allowed := map[string]bool{webappRoute: true, HealthPath: true, MetricsPath: true} for _, pattern := range AppRoutePatterns { allowed[pattern] = true } cases := []string{ "/app/me/", "/app/audiorecords/../me", "/app//me", "/app/audiorecords/{id}", "/app/me/" + strings.Repeat("d", 256), } for _, path := range cases { route := journalRouteOf(path) assert.True(t, allowed[route], "маршрут %q не принадлежит закрытому перечню (путь %q)", route, path) } } // Имя, под которым копия легла в каталог данных, в журнал не идёт: строка // журнала иначе стала бы бессрочным ключом к чужой записи. Сегодня оно не // попадает туда по построению — адрес копии назван идентификатором самой записи, // — и проверка сторожит именно это. func TestJournalCarriesNoStoredFileName(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) created := httptest.NewRecorder() env.serve(created, createMultipartRequest(t, "разговор.mp3", []byte("данные"))) require.Equal(t, http.StatusCreated, created.Code) recordID := intakeItemOf(t, created).ID require.Equal(t, http.StatusOK, env.getOwn("/app/audiorecords/"+recordID+"/file?"+CopyParam+"="+CopyOriginal).Code) names := storedFileNames(t, env) require.Len(t, names, 1) journal := env.journal.String() assert.NotContains(t, journal, names[0], "имя файла в каталоге данных уехало в журнал") assert.Contains(t, journal, recordID, "идентификатор записи остаётся: по нему прослеживается путь") } // Путь, отданный приложению, журнал заменяет исходом и длиной: строка о нём не // растёт вместе с длиной пути. func TestJournalWebappOutcomeInsteadOfPath(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) long := "/" + strings.Repeat("щ", 500) require.Equal(t, http.StatusOK, env.get(long).Code) journal := env.journal.String() assert.NotContains(t, journal, strings.Repeat("щ", 500), "путь уехал в журнал дословно") assert.Contains(t, journal, "webapp.outcome="+OutcomeMarkup) assert.Contains(t, journal, "http.path_length=") } // Неузнанный запрос под корнем приложения журнал тоже не пишет дословно, а // строка о нём не растёт вместе с длиной пути. Прежде путь под корнем считался // принадлежащим сервису и уезжал в строку целиком — аноним писал в журнал // владельца свой текст произвольной длины. func TestJournalHidesAnonymousPathUnderAppRoot(t *testing.T) { env := setupTestEnv(t, readableMetaViewer()) const filler = 4096 long := "/app/" + strings.Repeat("щ", filler) require.Equal(t, http.StatusUnauthorized, env.get(long).Code) journal := env.journal.String() assert.NotContains(t, journal, strings.Repeat("щ", filler), "путь уехал в журнал дословно") assert.Contains(t, journal, "http.route="+webappRoute) assert.Contains(t, journal, "http.path_length=") assert.Less(t, len(journal), filler, "строка журнала растёт вместе с длиной запрошенного пути") }