feat(ansible): support form-urlencoded uri bodies

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 02:17:38 +00:00
parent f9d8b3bc51
commit 75bafd10c8
3 changed files with 112 additions and 4 deletions

View file

@ -1679,8 +1679,13 @@ func moduleURIWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
}
if bodyText != "" {
curlOpts = append(curlOpts, "-d", sprintf("%q", bodyText))
if bodyFormat == "json" && !hasHeaderIgnoreCase(headersMap(args), "Content-Type") {
curlOpts = append(curlOpts, "-H", "\"Content-Type: application/json\"")
if !hasHeaderIgnoreCase(headersMap(args), "Content-Type") {
switch bodyFormat {
case "json":
curlOpts = append(curlOpts, "-H", "\"Content-Type: application/json\"")
case "form-urlencoded", "form_urlencoded", "form":
curlOpts = append(curlOpts, "-H", "\"Content-Type: application/x-www-form-urlencoded\"")
}
}
}
}

View file

@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"io/fs"
"net/url"
"os"
"path"
"path/filepath"
@ -1298,8 +1299,13 @@ func (e *Executor) moduleURI(ctx context.Context, client sshExecutorClient, args
}
if bodyText != "" {
curlOpts = append(curlOpts, "-d", sprintf("%q", bodyText))
if bodyFormat == "json" && !hasHeaderIgnoreCase(headersMap(args), "Content-Type") {
curlOpts = append(curlOpts, "-H", "\"Content-Type: application/json\"")
if !hasHeaderIgnoreCase(headersMap(args), "Content-Type") {
switch bodyFormat {
case "json":
curlOpts = append(curlOpts, "-H", "\"Content-Type: application/json\"")
case "form-urlencoded", "form_urlencoded", "form":
curlOpts = append(curlOpts, "-H", "\"Content-Type: application/x-www-form-urlencoded\"")
}
}
}
}
@ -1361,11 +1367,86 @@ func renderURIBody(body any, bodyFormat string) (string, error) {
}
return string(data), nil
}
case "form-urlencoded", "form_urlencoded", "form":
return renderURIBodyFormEncoded(body), nil
default:
return sprintf("%v", body), nil
}
}
func renderURIBodyFormEncoded(body any) string {
values := url.Values{}
switch v := body.(type) {
case map[string]any:
keys := make([]string, 0, len(v))
for key := range v {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
appendFormValue(values, key, v[key])
}
case map[any]any:
keys := make([]string, 0, len(v))
for key := range v {
if s, ok := key.(string); ok {
keys = append(keys, s)
}
}
sort.Strings(keys)
for _, key := range keys {
appendFormValue(values, key, v[key])
}
case map[string]string:
keys := make([]string, 0, len(v))
for key := range v {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
values.Add(key, v[key])
}
case []any:
for _, item := range v {
if pair, ok := item.(map[string]any); ok {
key := getStringArg(pair, "key", "")
if key == "" {
key = getStringArg(pair, "name", "")
}
if key != "" {
appendFormValue(values, key, pair["value"])
}
}
}
case string:
return v
default:
return sprintf("%v", body)
}
return values.Encode()
}
func appendFormValue(values url.Values, key string, value any) {
switch v := value.(type) {
case nil:
values.Add(key, "")
case string:
values.Add(key, v)
case []string:
for _, item := range v {
values.Add(key, item)
}
case []any:
for _, item := range v {
values.Add(key, sprintf("%v", item))
}
default:
values.Add(key, sprintf("%v", v))
}
}
func headersMap(args map[string]any) map[string]any {
headers, _ := args["headers"].(map[string]any)
return headers

View file

@ -1251,6 +1251,28 @@ func TestModulesAdv_ModuleURI_Good_PostWithBodyAndHeaders(t *testing.T) {
assert.True(t, mock.containsSubstring("Authorization"))
}
func TestModulesAdv_ModuleURI_Good_FormURLEncodedBody(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl.*form\.example\.com`, "created\n201", "", 0)
result, err := moduleURIWithClient(e, mock, map[string]any{
"url": "https://form.example.com/submit",
"method": "POST",
"body_format": "form-urlencoded",
"body": map[string]any{
"name": "Alice Example",
"scope": []any{"read", "write"},
},
"status_code": 201,
})
require.NoError(t, err)
assert.False(t, result.Failed)
assert.Equal(t, 201, result.RC)
assert.True(t, mock.containsSubstring(`-d "name=Alice+Example&scope=read&scope=write"`))
assert.True(t, mock.containsSubstring("Content-Type: application/x-www-form-urlencoded"))
}
func TestModulesAdv_ModuleURI_Good_WrongStatusCode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "Not Found\n404", "", 0)