Вот, отправь Соннету: --- **Контекст**: Пишем Terraform Provider для Nubes Cloud (ColdFusion API). Провайдер на Go, версия 5.0.66, опубликован в registry `terra.k8c.ru/nubes-test/nubes`. **Суть проблемы**: `terraform apply` для создания S3-бакета падает с ошибкой. Прошли путь от 403 до EOF. **Хронология**: 1. `HTTP 403` — DDoS-Guard блокирует Go `crypto/tls` (JA3 fingerprint). curl с того же сервера работает. 2. Поменяли URL с `index.cfm/instances` на `index.cfm?endpoint=/instances` — 403 остался. 3. Добавили `utls.HelloFirefox_120` — **403 ушёл**, но стал `EOF` (сервер закрывает соединение без ответа). 4. Убрали utls, оставили `InsecureSkipVerify: true` — `EOF`. 5. Вернули REST-путь (`index.cfm/instances`) + `InsecureSkipVerify: true` — `EOF`. **Что работает**: curl с токеном напрямую с того же сервера: ```bash curl -H "Authorization: Bearer $TOKEN" -H "User-Agent: Mozilla/5.0" \ "https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances&page=1&size=100" # → JSON, 200 OK ``` **Код**: `doRequest()` в client.go (строка 831). Использует `http.DefaultTransport.Clone()` с `TLSClientConfig{InsecureSkipVerify: true, MinVersion: TLS1.2}`. **Вопросы**: 1. Почему Go HTTP клиент (с идентичными заголовками) получает EOF там, где curl получает JSON? 2. Может ли ColdFusion-сервер требовать определённый порядок заголовков или HTTP-метод (POST вместо GET)? 3. Как заставить Go `http.Client` отправлять точно такие же запросы как curl? Может, через `net/http/httputil` дампить запрос? **Файлы**: - client.go - provider.go Список задач обновлен Searched for files matching `**/client.go`, 2 matches Read memory [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/repo/tf_provider_inventory.md) Searched for text `doRequest|DoRequest` (`**/universal_rebuild/internal/core/client.go`), 20 results Searched for text `doRequest|DoRequest` (`**/internal/core/client.go`), 20 results Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#800-800), lines 800 to 900 Read [](file:///home/naeel/tf_provider/internal/core/client.go#177-177), lines 177 to 260 Searched for text `HttpClient|NewClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport` (`**/universal_rebuild/internal/core/client.go`), 5 results Searched for text `HttpClient|NewClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport` (`**/internal/core/client.go`), 7 results Searched for text `HttpClient|NewUniversalClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport|http.Client{` (`**/universal_rebuild/internal/provider/**`), 11 results Searched for text `HttpClient|NewUniversalClient|InsecureSkipVerify|TLSClientConfig|DefaultTransport|http.Client{` (`**/internal/provider/**`), 20 results Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#105-105), lines 105 to 165 Теперь посмотрю на S3-ресурс и полный URL, чтобы понять что именно запрашивается: Searched for text `s3|bucket|S3|Bucket` (`**/universal_rebuild/internal/core/client.go`), 8 results Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#1-1), lines 1 to 50 Теперь у меня достаточно контекста. Сформирую план. Created memory file [](file:///home/naeel/.vscode-server/data/User/workspaceStorage/358cf615179442f06eab7245b983b753/GitHub.copilot-chat/memory-tool/memories/MjE3NzcyMzEtOGRkYy00ODA4LWI0OGItOGEyZDliNmU5YjFl/plan.md) ## Анализ и план ### Корневая причина EOF **Главный виновник — HTTP/2.** `http.DefaultTransport.Clone()` копирует ссылку на `TLSNextProto` map, в которой зарегистрирован HTTP/2 (`h2`) handler. При TLS-хендшейке Go предлагает `h2` в ALPN-расширении. ColdFusion + DDoS-Guard не поддерживают HTTP/2 — сервер принимает TLS-соединение, но сразу закрывает его без HTTP-ответа → **EOF**. Именно поэтому curl работает: он по умолчанию использует HTTP/1.1 и не предлагает `h2` в ALPN. `InsecureSkipVerify` здесь роли не играет — он влияет только на проверку сертификата, не на TLS-хендшейк. --- ### Ответы на вопросы **1. Почему Go получает EOF, а curl — нет?** Go через ALPN предлагает `["h2", "http/1.1"]`. Сервер видит `h2` и закрывает соединение (ColdFusion не понимает HTTP/2). curl предлагает только `["http/1.1"]` — сервер отвечает нормально. **2. Может ли ColdFusion требовать особый порядок заголовков?** Маловероятно причина. Порядок заголовков важен для WAAP/DDoS-Guard fingerprinting, но если 403 ушёл, то DDoS-Guard вас пропускает, а EOF — это именно HTTP/2 на уровне приложения. **3. Как заставить Go отправлять то же, что curl?** Двумя действиями ниже. --- ### Plan: Fix EOF (2 изменения) **Файлы к изменению:** - provider.go — строка ~124 - client.go — строка ~836 (`doRequest`) **Шаг 1 — Отключить HTTP/2** (приоритет #1, наиболее вероятный фикс) В provider.go после `transport := http.DefaultTransport.(*http.Transport).Clone()` добавить две строки: ```go transport := http.DefaultTransport.(*http.Transport).Clone() transport.TLSHandshakeTimeout = 60 * time.Second // Отключаем HTTP/2: ColdFusion + DDoS-Guard не поддерживают h2 → EOF transport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) transport.ForceAttemptHTTP2 = false // запрещаем upgrade ``` > `ForceAttemptHTTP2 = false` одного недостаточно — нужен именно пустой `TLSNextProto`, иначе `h2` всё равно остаётся в ALPN. **Шаг 2 — Убрать Content-Type на GET** (приоритет #2, возможная причина WAF-блокировки) В `doRequest()` в client.go заголовок `Content-Type` выставлять только если payload != nil: ```go if payload != nil { req.Header.Set("Content-Type", "application/json") } ``` **Шаг 3 — Debug dump** (если 1+2 не помогают) Добавить через env-флаг `NUBES_DEBUG_HTTP=1`: ```go if os.Getenv("NUBES_DEBUG_HTTP") == "1" { dump, _ := httputil.DumpRequestOut(req, true) fmt.Fprintf(os.Stderr, ">>> REQUEST:\n%s\n", dump) } ``` Потом сравнить вывод с `curl -v --http1.1 -H "Authorization: Bearer ..." ...`. --- **Верификация:** 1. `terraform apply` для S3-бакета — нет EOF 2. `curl -v --http2 ` — проверить что с `--http2` curl тоже получает EOF (подтвердит гипотезу) 3. `terraform plan` после успешного apply --- Ждать «делай» для реализации? Создано задач: 3 Вопрос Соннету: --- **Контекст**: Terraform Provider на Go. API: ColdFusion за DDoS-Guard. **Что сделали**: - `TLSNextProto = make(map[string]...)` — GET работает (200 JSON) - `?endpoint=` формат URL — как у генератора - `User-Agent: Mozilla/5.0` **Проблема**: GET → 200. POST → 403. curl (GET и POST) → оба 200. **Код POST**: ```go func (c *UniversalClient) doRequest(ctx context.Context, method, path string, payload interface{}) ([]byte, http.Header, error) { // payload — json-сериализованная структура req, _ := http.NewRequestWithContext(ctx, method, c.ApiEndpoint, body) // ?endpoint=/instances q := req.URL.Query() q.Set("endpoint", path) req.URL.RawQuery = q.Encode() req.Header.Set("User-Agent", "Mozilla/5.0") if body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set("Authorization", "Bearer "+token) } ``` **Работающий curl**: ```bash curl -X POST \ -H "User-Agent: Mozilla/5.0" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"serviceId":13}' \ "https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances" ``` **Вопрос**: Что в Go `http.Client` (c `TLSNextProto = make(...)`) может вызывать 403 на POST, при том что GET работает, и curl POST тоже работает? Может ли `http.NewRequestWithContext` добавлять заголовки/байты, которые триггерят DDoS-Guard на POST но не на GET? Список задач обновлен Прочитаю актуальный код `doRequest` и инициализацию transport, чтобы ответить точно, а не по памяти: Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/core/client.go#804-804), lines 804 to 880 Read [](file:///home/naeel/tf_provider/universal_rebuild/internal/provider/provider.go#122-122), lines 122 to 155 ## Диагноз: 3 кандидата, #1 самый вероятный Код виден полностью. Вот что реально отличает Go POST от curl POST. --- ### #1 (главный подозреваемый): `url.Values.Encode()` кодирует `/` как `%2F` ```go q.Set("endpoint", endpointPath) // endpointPath = "/instances" req.URL.RawQuery = q.Encode() // → endpoint=%2Finstances ``` curl отправляет: `?endpoint=/instances` Go отправляет: `?endpoint=%2Finstances` **Почему GET проходит, а POST нет?** DDoS-Guard WAF для POST-запросов почти всегда имеет **allowlist эндпоинтов** — проверяет, что `endpoint` соответствует разрешённым путям. `/instances` совпадает, `%2Finstances` — нет → 403. GET-запросы проверяются слабее (read-only, меньший риск). --- ### #2: Нет заголовка `Accept` curl всегда добавляет `Accept: */*` автоматически. Ваш Go-код его не ставит. DDoS-Guard fingerprinting видит: POST с телом + нет `Accept` → нет `Accept` = нет браузера = потенциальный бот. --- ### #3: `Accept-Encoding: gzip` добавляет transport Т.к. `DisableCompression` не выставлен, transport добавляет `Accept-Encoding: gzip` ко всем запросам. Для GET это тоже есть → GET проходит → вероятно, не причина. Но на всякий случай стоит проверить. --- ### Как проверить немедленно `DumpRequestOut` уже в коде: ```bash NUBES_DEBUG_HTTP=1 terraform apply 2>&1 | grep -A 30 ">>> REQ POST" ``` И сравнить с: ```bash curl -v -X POST -H "User-Agent: Mozilla/5.0" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"serviceId":13}' \ "https://deck-api-test.ngcloud.ru/api/v1/index.cfm?endpoint=/instances" 2>&1 | grep -E "^>|^<" ``` --- ### Что менять **Fix #1 — не использовать `q.Encode()` для endpoint:** ```go // Вместо q.Set("endpoint",...) + q.Encode() rawQuery := "endpoint=" + endpointPath // слэш не кодируется if extraQuery != "" { rawQuery += "&" + extraQuery } req.URL.RawQuery = rawQuery ``` **Fix #2 — добавить `Accept: */*`** (как curl): ```go req.Header.Set("Accept", "*/*") ``` **Fix #3 (опционально) — отключить автоматическое gzip в transport:** ```go transport.DisableCompression = true ``` --- ### Итог | Причина | Почему GET ок | Почему POST 403 | |---|---|---| | `%2Finstances` в URL | WAF для GET мягче | WAF для POST проверяет allowlist путей | | Нет `Accept` | То же | Browser fingerprint не проходит | | `Connection: close` | - | (здесь `req.Close = true` нет, уже убрано) | Начинать с Fix #1 — вероятность ~70% что это именно оно. Создано задач: 4