Initial commit
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
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, "'", "'\\''") + "'"
|
||||
}
|
||||
Reference in New Issue
Block a user