v5.0.71: rawQuery (no %2F) + Accept: */* (DDoS-Guard POST fix)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "terra.k8c.ru/nubes-test/nubes"
|
||||
version = "5.0.70"
|
||||
version = "5.0.71"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
version string = "5.0.70"
|
||||
version string = "5.0.71"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
Reference in New Issue
Block a user