From f9d1a858d13a8d0e1e34175b245cc9f5d0076c5e Mon Sep 17 00:00:00 2001 From: smail Date: Fri, 18 Sep 2026 22:21:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=89=8B=E6=9C=BA=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E7=A0=81=E7=89=88=E6=9C=AC=E6=8E=A5=E5=8F=A3=E5=B9=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=A0=B8=E5=AF=B9=E5=89=8D=E7=AB=AF=E8=B7=AF?= =?UTF-8?q?=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/listen/http_server/api_v1_compat.go | 14 ++++ .../listen/http_server/api_v1_compat_test.go | 74 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/backend/listen/http_server/api_v1_compat.go b/backend/listen/http_server/api_v1_compat.go index 726f6a8c..08f7f8bd 100644 --- a/backend/listen/http_server/api_v1_compat.go +++ b/backend/listen/http_server/api_v1_compat.go @@ -32,6 +32,7 @@ func registerV1Routes(mux *http.ServeMux) { registerV1MethodRoute(mux, "/api/v1/security/recovery/verify", "/api/security/recovery/verify", http.MethodPost, 16<<10, controllers.VerifyPasswordRecovery) registerV1MethodRoute(mux, "/api/v1/security/recovery/complete", "/api/security/recovery/complete", http.MethodPost, 16<<10, controllers.CompletePasswordRecovery) registerV1CredentialsQuestionRoute(mux) + registerV1TOTPRoute(mux) registerV1MethodRoute(mux, "/api/v1/security/credentials/recovery-codes", "/api/security/credentials/recovery-codes", http.MethodGet, 0, controllers.GetRecoveryCodeStatus) registerV1MethodRoute(mux, "/api/v1/security/credentials/recovery-codes/generate", "/api/security/credentials/recovery-codes/generate", http.MethodPost, 16<<10, controllers.GenerateRecoveryCodes) registerV1MethodRoute(mux, "/api/v1/security/credentials/recovery-codes/revoke", "/api/security/credentials/recovery-codes/revoke", http.MethodPost, 4<<10, controllers.RevokeRecoveryCodes) @@ -133,6 +134,19 @@ func registerV1CredentialsQuestionRoute(mux *http.ServeMux) { }) } +func registerV1TOTPRoute(mux *http.ServeMux) { + handler := limitBody(4<<10, contextIterceptor(rewriteV1Path("/api/security/totp", controllers.TOTPSettings))) + mux.HandleFunc("/api/v1/security/totp", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet, http.MethodPost: + handler.ServeHTTP(w, r) + default: + w.Header().Set("Allow", "GET, POST") + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } + }) +} + func registerV1PluginRoutes(mux *http.ServeMux) { getUserSettings := contextIterceptor(controllers.GetTelegramUserSettings) updateUserSettings := limitBody(16<<10, contextIterceptor(controllers.UpdateTelegramUserSettings)) diff --git a/backend/listen/http_server/api_v1_compat_test.go b/backend/listen/http_server/api_v1_compat_test.go index 57596b40..7d6480a7 100644 --- a/backend/listen/http_server/api_v1_compat_test.go +++ b/backend/listen/http_server/api_v1_compat_test.go @@ -1,8 +1,12 @@ package http_server import ( + "io/fs" "net/http" "net/http/httptest" + "os" + "path/filepath" + "regexp" "testing" "git.pcux.cn/xuans/Smail/config" @@ -201,6 +205,7 @@ func TestFrontendV1RoutesAreRegistered(t *testing.T) { "/api/v1/security/login/resend", "/api/v1/security/login/status", "/api/v1/security/login/verify", + "/api/v1/security/totp", "/api/v1/security/recovery/begin", "/api/v1/security/recovery/complete", "/api/v1/security/recovery/verify", @@ -235,3 +240,72 @@ func TestFrontendV1RoutesAreRegistered(t *testing.T) { } } } + +func TestFrontendSourceV1RoutesAreRegistered(t *testing.T) { + mux := http.NewServeMux() + setupMux := http.NewServeMux() + registerSetupAPIRoutes(setupMux, withSetupSecurity(t, "0123456789abcdefghijklmnop"), func(http.ResponseWriter, *http.Request) {}) + if err := router(mux); err != nil { + t.Fatal(err) + } + pattern := regexp.MustCompile("[\x22\x27\x60](/api/v1/[a-zA-Z0-9_/-]+)[\x22\x27\x60]") + count := 0 + err := filepath.WalkDir("../../../frontend/src", func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() || (filepath.Ext(path) != ".js" && filepath.Ext(path) != ".vue" && filepath.Ext(path) != ".mjs") { + return nil + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + for _, match := range pattern.FindAllSubmatch(content, -1) { + endpoint := string(match[1]) + _, route := mux.Handler(httptest.NewRequest(http.MethodGet, endpoint, nil)) + if endpoint == "/api/v1/setup" { + _, route = setupMux.Handler(httptest.NewRequest(http.MethodGet, endpoint, nil)) + } + if route == "" || route == "/" { + t.Errorf("%s: endpoint %s is not registered", path, endpoint) + } + count++ + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if count == 0 { + t.Fatal("no frontend API calls inspected") + } +} + +func TestV1TOTPRouteMethods(t *testing.T) { + oldInit := config.IsInit + t.Cleanup(func() { config.SetInitState(oldInit) }) + config.SetInitState(false) + mux := http.NewServeMux() + registerV1TOTPRoute(mux) + for _, method := range []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPut} { + recorder := httptest.NewRecorder() + mux.ServeHTTP(recorder, httptest.NewRequest(method, "/api/v1/security/totp", nil)) + want := http.StatusOK + if method == http.MethodDelete || method == http.MethodPut { + want = http.StatusMethodNotAllowed + if recorder.Header().Get("Allow") != "GET, POST" { + t.Fatal("incorrect allowed methods") + } + } + if recorder.Code != want { + t.Errorf("%s returned %d, want %d", method, recorder.Code, want) + } + if recorder.Header().Get("Deprecation") != "" { + t.Fatal("versioned TOTP route marked as legacy") + } + } + if publicSecurityPath("/api/v1/security/totp") { + t.Fatal("TOTP settings must require a session") + } +}