v5.0.71: rawQuery (no %2F) + Accept: */* (DDoS-Guard POST fix)

This commit is contained in:
“Naeel”
2026-07-01 15:52:50 +04:00
parent 3f9b02c7f7
commit f2d2b02e45
4 changed files with 156 additions and 13 deletions
+142
View File
@@ -148,3 +148,145 @@ if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
Создано задач: 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
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "terra.k8c.ru/nubes-test/nubes"
version = "5.0.70"
version = "5.0.71"
}
}
}
+12 -11
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"os"
"regexp"
@@ -833,27 +834,21 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa
return nil, nil, err
}
// ?endpoint= формат — как генератор (Python) и curl
// ?endpoint= формат — без url.Values.Encode (сохраняет / как /)
endpointPath := path
extraQuery := ""
if idx := strings.Index(path, "?"); idx >= 0 {
endpointPath = path[:idx]
extraQuery = path[idx+1:]
}
q := req.URL.Query()
q.Set("endpoint", endpointPath)
rawQuery := "endpoint=" + endpointPath
if extraQuery != "" {
// Добавляем параметры из path в URL (не в endpoint)
for _, param := range strings.Split(extraQuery, "&") {
kv := strings.SplitN(param, "=", 2)
if len(kv) == 2 {
q.Set(kv[0], kv[1])
}
}
rawQuery += "&" + extraQuery
}
req.URL.RawQuery = q.Encode()
req.URL.RawQuery = rawQuery
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "*/*")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
@@ -861,6 +856,12 @@ func (c *UniversalClient) doRequest(ctx context.Context, method, path string, pa
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
// DEBUG
if os.Getenv("NUBES_DEBUG_HTTP") == "1" {
dump, _ := httputil.DumpRequestOut(req, body != nil)
fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, path, dump)
}
resp, err := c.HttpClient.Do(req)
if err != nil {
lastErr = err
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
var (
version string = "5.0.70"
version string = "5.0.71"
)
func main() {