Files
Mester Gábor 2dd6519168 Initial commit
2026-03-19 07:12:03 +01:00

219 lines
5.2 KiB
Go

package api
import (
"encoding/base64"
"fmt"
"net/url"
"sort"
"strings"
"unicode"
"greq/internal/storage"
)
// ImportCURL parses a curl command string into a RequestFile.
// Handles -X, -H/--header, -d/--data/--data-raw, -u/--user, -b/--cookie.
// Unknown placeholder variables ({{…}}) are left as-is.
func ImportCURL(raw string) (storage.RequestFile, error) {
tokens := tokenizeCURL(raw)
if len(tokens) == 0 {
return storage.RequestFile{}, fmt.Errorf("empty input")
}
if !strings.EqualFold(tokens[0], "curl") {
return storage.RequestFile{}, fmt.Errorf("does not start with 'curl'")
}
rf := storage.RequestFile{
Method: "GET",
Headers: make(map[string]string),
}
i := 1
for i < len(tokens) {
tok := tokens[i]
switch tok {
case "-X", "--request":
i++
if i < len(tokens) {
rf.Method = strings.ToUpper(tokens[i])
}
case "-H", "--header":
i++
if i < len(tokens) {
if k, v, ok := splitHeader(tokens[i]); ok {
rf.Headers[k] = v
}
}
case "-d", "--data", "--data-raw", "--data-ascii", "--data-urlencode":
i++
if i < len(tokens) {
if rf.Body != "" {
rf.Body += "&"
}
rf.Body += tokens[i]
if rf.Method == "GET" {
rf.Method = "POST"
}
}
case "--data-binary":
i++
if i < len(tokens) && !strings.HasPrefix(tokens[i], "@") {
rf.Body = tokens[i]
if rf.Method == "GET" {
rf.Method = "POST"
}
}
case "-u", "--user":
i++
if i < len(tokens) {
enc := base64.StdEncoding.EncodeToString([]byte(tokens[i]))
rf.Headers["Authorization"] = "Basic " + enc
}
case "-b", "--cookie":
i++
if i < len(tokens) {
rf.Headers["Cookie"] = tokens[i]
}
case "-A", "--user-agent":
i++
if i < len(tokens) {
rf.Headers["User-Agent"] = tokens[i]
}
// Flags to silently ignore (no value)
case "--compressed", "--silent", "-s", "--insecure", "-k",
"-L", "--location", "-v", "--verbose", "-i", "--include",
"--no-keepalive", "--http2", "--http1.1", "--http1.0":
// Short flags with values that we don't handle — skip value
default:
if !strings.HasPrefix(tok, "-") {
if rf.URL == "" {
rf.URL = tok
}
} else if len(tok) == 2 {
// Unknown short flag: skip its value
i++
}
// Unknown long flags: skip
}
i++
}
if rf.URL == "" {
return rf, fmt.Errorf("no URL found in curl command")
}
if len(rf.Headers) == 0 {
rf.Headers = nil
}
rf.Name = inferName(rf.Method, rf.URL)
return rf, nil
}
// ExportCURL builds a shell-safe curl command from a RequestFile.
// Variables are resolved before embedding in the command string.
func ExportCURL(rf storage.RequestFile, envs map[string]string) string {
var parts []string
parts = append(parts, "curl")
parts = append(parts, shellQuote(ResolveVariables(rf.URL, envs)))
if rf.Method != "GET" || rf.Body != "" {
parts = append(parts, "-X", rf.Method)
}
// Deterministic header order
keys := make([]string, 0, len(rf.Headers))
for k := range rf.Headers {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := ResolveVariables(rf.Headers[k], envs)
parts = append(parts, "-H", shellQuote(k+": "+v))
}
if rf.Body != "" {
parts = append(parts, "--data-raw", shellQuote(ResolveVariables(rf.Body, envs)))
}
return strings.Join(parts, " \\\n ")
}
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------
func tokenizeCURL(s string) []string {
// Join backslash-newline continuations
s = strings.ReplaceAll(s, "\\\n", " ")
s = strings.ReplaceAll(s, "\\\r\n", " ")
s = strings.TrimSpace(s)
var tokens []string
var cur strings.Builder
inSingle := false
inDouble := false
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c == '\'' && !inDouble:
inSingle = !inSingle
case c == '"' && !inSingle:
inDouble = !inDouble
case c == '\\' && !inSingle:
i++
if i < len(s) {
cur.WriteByte(s[i])
}
case (c == ' ' || c == '\t' || c == '\n') && !inSingle && !inDouble:
if cur.Len() > 0 {
tokens = append(tokens, cur.String())
cur.Reset()
}
default:
cur.WriteByte(c)
}
}
if cur.Len() > 0 {
tokens = append(tokens, cur.String())
}
return tokens
}
func splitHeader(s string) (key, val string, ok bool) {
idx := strings.Index(s, ": ")
if idx == -1 {
idx = strings.Index(s, ":")
if idx == -1 {
return "", "", false
}
}
return strings.TrimSpace(s[:idx]), strings.TrimSpace(s[idx+1:]), true
}
func inferName(method, rawURL string) string {
u, err := url.Parse(rawURL)
if err != nil || u.Path == "" || u.Path == "/" {
return method + " Request"
}
parts := strings.Split(strings.Trim(u.Path, "/"), "/")
last := parts[len(parts)-1]
if last == "" || last == "{" {
return method + " Request"
}
// Title-case the last path segment
words := strings.FieldsFunc(strings.ReplaceAll(last, "_", " "), func(r rune) bool {
return r == '-' || unicode.IsSpace(r)
})
for i, w := range words {
if len(w) > 0 {
words[i] = strings.ToUpper(w[:1]) + w[1:]
}
}
return strings.Join(words, " ")
}
// shellQuote wraps s in single quotes, escaping any embedded single quotes.
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}