0%

使用 golang 实现解析 cURL

本文对应源码:https://github.com/killlowkey/parse-curl

cURL 是什么

维基百科:“cURL是一个命令行工具,用于使用URL语法获取或发送数据”,简而言之,可以使用 cURL 发送 http/https 请求。

使用 cURL

在进行实现之前,我们需要对 cURL 语法有一个大致的了解,当然有使用 cURL 的读者可以跳过该部分。下面来看一个简单的 cURL 例子,该命令向 https://www.baidu.com 发送一个 GET 请求,获取该响应数据。

1
curl https://www.baidu.com

cURL 还提供一系列的参数,比如通过 -X、–request 参数来指定请求方法,-H 参数指定请求头,-d 参数指定请求参数,–data-row 指定 body 等等。之后我们得到一个复杂的 cURL 例子,如下所示,该命令携带一个json数据向 http://localhost:6000/log 发送一个 POST 请求。

1
2
3
4
5
6
curl --location --request POST 'http://localhost:6000/log' \
--header 'Content-Type: application/json' \
--data-raw '{
"serviceName": "Gateway-Service",
"content": "test"
}'

小试牛刀

现在进入正题,有了前面的知识铺垫,相信会事半功倍。编写代码之前,先来讲述一下实现思路,cURL 是一个类似与 key/value 格式数据,也就是说通过 -H 指定请求头参数,后面跟着请求头数据,所以这里采用状态机方式实现会更好。

一个完整的请求由请求行、请求头和body进行组成,下面我们定义了一个 Request struct 来表示请求,解析之后通过该 struct 进行表示。

1
2
3
4
5
6
7
8
type Header map[string]string

type Request struct {
Method string `json:"method"`
Url string `json:"url"`
Header Header `json:"header"`
Body string `json:"body"`
}

解析之前需要对传入的 curl 参数进行一个判断,如果该参数不是一个 curl 命令,那么无需继续解析了。判断方法也很简单,如果该参数是 curl 开头(注意 curl 后还有个空格),那么基本可以认定该参数是一个 curl 命令。

1
2
3
if strings.Index(curl, "curl ") != 0 {
return nil, false
}

因为 curl 是一个 shell 命令,接下来使用 go-shellwords 库对 curl 参数进行解析,得到一个 string 数组。

1
2
3
4
args, err := shellwords.Parse(curl)
if err != nil {
return nil, false
}

之后调用 rewrite 方法对 args 数组进行二次清洗,该步骤的目的是为了去除数组中的\n 元素。因为 curl 还有一个特殊的参数,使用-X 参数来指定请求方法,比如说 -XPUT,该参数指定了一个 PUT 请求,所以这里需要将 -XPUT 这种参数进行拆分,得到 -X 与 PUT 两个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func Parse(curl string) (*Request, bool) { 
....
args = rewrite(args)
request := &Request{Method: "GET", Header: Header{}}
state := ""
....
}

func rewrite(args []string) []string {
res := make([]string, 0)

for _, arg := range args {
arg = strings.TrimSpace(arg)

if arg == "\n" {
continue
}

if strings.Contains(arg, "\n") {
arg = strings.ReplaceAll(arg, "\n", "")
}

// split request method
if strings.Index(arg, "-X") == 0 {
res = append(res, arg[0:2])
res = append(res, arg[2:])
} else {
res = append(res, arg)
}
}

return res
}

实现状态机

状态机是实现解析 cURL 核心部分,前面我们对 cURL 参数进行分割与清洗。

实现状态机之前我们需要对 cURL 一些参数进行了解,比如说通过 -A 与 –user–agent 参数可以指定 user-agent。此时就可以给状态机赋值为 user-agent 然后就跳出该 switch,等到遍历到下一个参数的时候,就进入 switch 中的最后一个 case,在该case 里面,会对状态机的状态进行检测,提取出相应的值,最后清除状态的状态。

解析 -H 与 –header 参数时,需要进行拆分操作。比如说 -H 'Accept-Encoding: gzip, deflate, sdch',此时就需要获取请求头与值得到 Accept-Encodinggzip, deflate, sdch,最后添加到 request struct 的 header 中。请求的类型为 application/x-www-form-urlencoded 时,需要使用 & 对请求参数进行拼接,倘若是 application/json,那么直接赋值即可。解析 -u 与 –user 参数时,需要对 arg 参数进行一个 base64 编码操作,然后向请求添加一个 Authorization 头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
func Parse(curl string) (*Request, bool) {  
....
for _, arg := range args {
switch true {
case isUrl(arg):
request.Url = arg
break

case arg == "-A" || arg == "--user-agent":
state = "user-agent"
break

case arg == "-H" || arg == "--header":
state = "header"
break

case arg == "-d" || arg == "--data" || arg == "--data-ascii" || arg == "--data-raw":
state = "data"
break

case arg == "-u" || arg == "--user":
state = "user"
break

case arg == "-I" || arg == "--head":
request.Method = "HEAD"
break

case arg == "-X" || arg == "--request":
state = "method"
break

case arg == "-b" || arg == "--cookie":
state = "cookie"
break

case len(arg) > 0:
switch state {
case "header":
fields := parseField(arg)
request.Header[fields[0]] = strings.TrimSpace(fields[1])
state = ""
break

case "user-agent":
request.Header["User-Agent"] = arg
state = ""
break

case "data":
if request.Method == "GET" || request.Method == "HEAD" {
request.Method = "POST"
}

if !hasContentType(*request) {
request.Header["Content-Type"] = "application/x-www-form-urlencoded"
}

if len(request.Body) == 0 {
request.Body = arg
} else {
request.Body = request.Body + "&" + arg
}

state = ""
break

case "user":
request.Header["Authorization"] = "Basic " +
base64.StdEncoding.EncodeToString([]byte(arg))
state = ""
break

case "method":
request.Method = arg
state = ""
break

case "cookie":
request.Header["Cookie"] = arg
state = ""
break

default:
break
}
}

}
....
}

略有小成

创建一个 example,使用 Parse 方法传入一个 curl 命令,得到一个 json 对象。

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
parseCurl "parse-curl"
)

func main() {
request, _ := parseCurl.Parse("curl 'http://google.com/' \\\n -H 'Accept-Encoding: gzip, deflate, sdch' \\\n -H 'Accept-Language: en-US,en;q=0.8,da;q=0.6' \\\n -H 'Upgrade-Insecure-Requests: 1' \\\n -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36' \\\n -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8' \\\n -H 'Connection: keep-alive' \\\n --compressed\n")
fmt.Println(request.ToJson(true))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"method": "GET",
"url": "http://google.com/",
"header": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "en-US,en;q=0.8,da;q=0.6",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.110 Safari/537.36"
},
"body": ""
}

参考