package app import ( "bytes" "context" "crypto/subtle" "encoding/json" "errors" "evening_detective_server/internal/services/team_service" "fmt" "io" "net/http" "strconv" "time" ) func TeamRegistrationHandler(token string, register func(context.Context, int, string, team_service.Registration) (int, error)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") fail := func(code int, message string) { w.WriteHeader(code) _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) } if token == "" { fail(http.StatusServiceUnavailable, "Интеграция регистрации не настроена") return } if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+token)) != 1 { fail(http.StatusUnauthorized, "Неверный токен интеграции") return } gameID, err := strconv.ParseInt(r.PathValue("id"), 10, 32) deliveryID := r.Header.Get("X-Delivery-Id") if err != nil || gameID <= 0 || deliveryID == "" || len(deliveryID) > 255 { fail(http.StatusBadRequest, "Требуются ID игры и заголовок X-Delivery-Id (до 255 байт)") return } decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)) var payload json.RawMessage if err := decoder.Decode(&payload); err != nil { fail(http.StatusBadRequest, fmt.Sprintf("Некорректный JSON регистрации: %v", err)) return } var extra any if err := decoder.Decode(&extra); err != io.EOF { if err == nil { fail(http.StatusBadRequest, "Некорректный JSON регистрации: после объекта обнаружены лишние данные") } else { fail(http.StatusBadRequest, fmt.Sprintf("Некорректный JSON регистрации: после объекта обнаружены лишние данные: %v", err)) } return } registration, rpcID, isRPC, err := decodeRegistrationPayload(payload) if err != nil { fail(http.StatusBadRequest, fmt.Sprintf("Некорректный JSON регистрации: %v", err)) return } ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second) defer cancel() id, err := register(ctx, int(gameID), deliveryID, registration) switch { case errors.Is(err, team_service.ErrInvalidRegistration): fail(http.StatusBadRequest, err.Error()) case errors.Is(err, team_service.ErrRegistrationClosed): fail(http.StatusConflict, err.Error()) case err != nil: fail(http.StatusInternalServerError, "Не удалось зарегистрировать команду") default: if isRPC { _ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "result": map[string]int{"id": id}, "id": rpcID}) } else { _ = json.NewEncoder(w).Encode(map[string]int{"id": id}) } } }) } func decodeRegistrationPayload(payload json.RawMessage) (team_service.Registration, json.RawMessage, bool, error) { var fields map[string]json.RawMessage if err := json.Unmarshal(payload, &fields); err != nil { return team_service.Registration{}, nil, false, err } if rawRPC, ok := fields["jsonrpc"]; ok { var version string if err := json.Unmarshal(rawRPC, &version); err != nil || version != "2.0" { return team_service.Registration{}, nil, true, errors.New(`поле "jsonrpc" должно быть равно "2.0"`) } params, ok := fields["params"] if !ok { return team_service.Registration{}, nil, true, errors.New(`в JSON-RPC запросе отсутствует поле "params"`) } var registration team_service.Registration if err := decodeStrict(params, ®istration); err != nil { return team_service.Registration{}, nil, true, fmt.Errorf("поле params: %w", err) } return registration, fields["id"], true, nil } var registration team_service.Registration if err := decodeStrict(payload, ®istration); err != nil { return team_service.Registration{}, nil, false, err } return registration, nil, false, nil } func decodeStrict(data []byte, target any) error { decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() return decoder.Decode(target) }