package api import ( "crypto/subtle" "encoding/json" "errors" "net/http" "strings" "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/summercms/modules/wire" ) // maxReportBody caps the 404 report body: the URL and the referer fit // with room to spare. const maxReportBody = 8 << 10 // notFoundReport is the 404 report body. type notFoundReport struct { URL string `json:"url"` Referer string `json:"referer"` } // NotFound serves POST /_seo/api/v1/notfound (D-20, P-22): the frontend // server reports a 404 with "Authorization: Bearer ", the configured // golem15.seo.notfound_token, compared in constant time. It answers 204, // 401 for a missing or wrong token, 404 when no token is configured (the // endpoint is off), and 422 for a body without a path or http(s) URL of at // most 2048 bytes. func NotFound(service func() *classes.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { svc := service() if svc == nil { wire.WriteJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "SEO is not available"}) return } want := svc.NotFoundToken() if want == "" { wire.WriteJSON(w, http.StatusNotFound, map[string]string{"error": "Not Found"}) return } got, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ") if !ok || subtle.ConstantTimeCompare([]byte(strings.TrimSpace(got)), []byte(want)) != 1 { wire.WriteJSON(w, http.StatusUnauthorized, map[string]string{"error": "Unauthorized"}) return } var in notFoundReport dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxReportBody)) if err := dec.Decode(&in); err != nil { wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "The request body is invalid."}) return } if err := svc.ReportNotFound(r.Context(), in.URL, in.Referer); err != nil { if errors.Is(err, classes.ErrNotFoundURL) { wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) return } log.Error("golem15.seo: 404 report", "error", err) wire.WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "Internal Server Error"}) return } w.WriteHeader(http.StatusNoContent) } }