feat(httpc): go-hua/httpurl 迁入并更名 httpc(HTTP 客户端)
- 原包名 httpurl 看不出是客户端, 更名 httpc; GET/POST/PUT/DELETE/代理/ Digest 认证, 函数签名不变, 附测试、例程与 README
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# httpc
|
||||
|
||||
轻量 HTTP 客户端:GET/POST/PUT/DELETE 快捷方法(自定义 Header、可选代理)、
|
||||
Digest 认证请求。
|
||||
|
||||
> 迁移自 go-hua/httpurl 并更名:原包名 `httpurl` 看不出是客户端,更名 `httpc`
|
||||
> (http client)。函数签名不变。
|
||||
|
||||
## 用法
|
||||
|
||||
```go
|
||||
import "git.zeroonesoft.cn/golib/zogo/httpc"
|
||||
|
||||
body, err := httpc.Get("https://api.example.com/v1/info", nil)
|
||||
body, err := httpc.Post("https://api.example.com/v1/add", []byte(`{"id":1}`),
|
||||
map[string]string{"Content-Type": "application/json"})
|
||||
body, err := httpc.GetProxy(url, nil, "http://127.0.0.1:8080") // 走代理
|
||||
body, err := httpc.Put(url, payload, nil)
|
||||
body, err := httpc.Delete(url, nil, nil)
|
||||
|
||||
// Digest 认证
|
||||
resp, err := httpc.DigestAuthRequest(&httpc.RequestArg{
|
||||
Url: url,
|
||||
Method: "GET",
|
||||
Username: "admin",
|
||||
Password: "pass",
|
||||
})
|
||||
```
|
||||
|
||||
完整可运行例程:[examples/httpc/main.go](../examples/httpc/main.go)
|
||||
|
||||
## 返回与错误
|
||||
|
||||
- 2xx 返回响应体字节集;非 2xx 返回包含状态码的错误。
|
||||
- `Get/Post/Put/Delete` 的 `header` 参数传 `nil` 表示不附加自定义头。
|
||||
|
||||
## 注意
|
||||
|
||||
- `DigestAuthRequest` 只处理 RFC 2617 Digest 流程(qop=auth);Basic/Bearer
|
||||
请直接在 header 里带 Authorization。
|
||||
- 需要 JSON 自动绑定/泛型封装时配合服务端包 `zogo/httpx` 之外的客户端封装自行扩展。
|
||||
@@ -0,0 +1,126 @@
|
||||
package httpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RequestArg Digest 认证请求参数
|
||||
type RequestArg struct {
|
||||
Host string //http://localhost
|
||||
Uri string
|
||||
Method string
|
||||
Header map[string]string //请求头
|
||||
PostBody []byte
|
||||
Username string //用户名
|
||||
Password string //密码
|
||||
}
|
||||
|
||||
// DigestAuthRequest digestAuth 请求
|
||||
func DigestAuthRequest(arg *RequestArg) ([]byte, error) {
|
||||
url := arg.Host + arg.Uri
|
||||
req, err := http.NewRequest(arg.Method, url, nil)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
resStr, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resStr, nil
|
||||
}
|
||||
parts := digestParts(resp)
|
||||
parts["uri"] = arg.Uri
|
||||
parts["method"] = arg.Method
|
||||
parts["username"] = arg.Username
|
||||
parts["password"] = arg.Password
|
||||
parts["algorithm"] = "MD5"
|
||||
req, err = http.NewRequest(arg.Method, url, bytes.NewBuffer(arg.PostBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", getDigestAuthorization(parts))
|
||||
for k, v := range arg.Header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
all, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// 第一次请求的响应header 获取第二次请求所需要的信息 Www-Authenticate
|
||||
func digestParts(resp *http.Response) map[string]string {
|
||||
result := map[string]string{}
|
||||
if len(resp.Header["Www-Authenticate"]) > 0 {
|
||||
wantedHeaders := []string{"nonce", "realm", "qop", "opaque"}
|
||||
responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
|
||||
for _, r := range responseHeaders {
|
||||
for _, w := range wantedHeaders {
|
||||
if strings.Contains(r, w) {
|
||||
result[w] = strings.Split(r, `"`)[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 第二次请求的请求header Authorization 值
|
||||
func getDigestAuthorization(digestParts map[string]string) string {
|
||||
d := digestParts
|
||||
//ha1=md5(username:realm:password)
|
||||
ha1 := getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
|
||||
//ha2=md5(method:uri)
|
||||
ha2 := getMD5(d["method"] + ":" + d["uri"])
|
||||
nonceCount := "00000001"
|
||||
cnonce := getCnonce()
|
||||
|
||||
//response=md5(ha1:nonce:nc:cnonce:qop:ha2)
|
||||
response := getMD5(fmt.Sprintf("%s:%s:%s:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2))
|
||||
|
||||
//qop和nc的值不能加引号
|
||||
authorization := fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", opaque="%s", algorithm=MD5", qop=%s, nc=%s, cnonce="%s"`,
|
||||
d["username"], d["realm"], d["nonce"], d["uri"], response, d["opaque"], d["qop"], nonceCount, cnonce)
|
||||
|
||||
return authorization
|
||||
}
|
||||
|
||||
// 字符串MD5加密
|
||||
func getMD5(text string) string {
|
||||
hash := md5.New()
|
||||
hash.Write([]byte(text))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// 获取 cnonce
|
||||
// 客户端提供的不透明带引号的字符串值,客户端和服务器都使用它来避免选定的明文攻击、提供相互身份验证以及提供一些消息完整性保护
|
||||
func getCnonce() string {
|
||||
b := make([]byte, 8)
|
||||
io.ReadFull(rand.Reader, b)
|
||||
return fmt.Sprintf("%x", b)[:16]
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
// Package httpc 封装常用 HTTP 客户端请求(Get/Post/Put/Delete/代理/Digest 认证)。
|
||||
package httpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 复用的 http.Transport 实例
|
||||
var transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
// 复用的 http.Client 实例
|
||||
var client = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: time.Second * 15,
|
||||
}
|
||||
|
||||
// Post 发送 POST 请求并返回响应体
|
||||
// header 为空或未指定 Content-Type 时默认使用 application/json
|
||||
func Post(url string, param []byte, header map[string]string) ([]byte, error) {
|
||||
payload := bytes.NewReader(param)
|
||||
// 创建新的 HTTP POST 请求
|
||||
req, err := http.NewRequest("POST", url, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加请求头
|
||||
for k, v := range header {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
if header == nil || header["Content-Type"] == "" {
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
}
|
||||
// 发送请求
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 确保响应体在函数结束时关闭
|
||||
defer res.Body.Close()
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查响应状态码
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Get 发送 GET 请求并返回响应体
|
||||
func Get(httpUrl string, header map[string]string) ([]byte, error) {
|
||||
// 创建新的 HTTP GET 请求
|
||||
req, err := http.NewRequest("GET", httpUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加请求头
|
||||
for k, v := range header {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
// 发送请求
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 确保响应体在函数结束时关闭
|
||||
defer res.Body.Close()
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查响应状态码
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// GetProxy 通过指定代理发送 GET 请求并返回响应体
|
||||
// proxyAddr 如 "http://127.0.0.1:7890",为空则直连
|
||||
func GetProxy(httpUrl string, header map[string]string, proxyAddr string) ([]byte, error) {
|
||||
|
||||
req, err := http.NewRequest("GET", httpUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range header {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
// req.Header.Add("Content-Type", "application/json")
|
||||
/*req.Header.Add("Authorization", "BNbmgAAGI155F6MJ3N2Tk9ruL_6XQpx-uxkkg:tGCY3xCsgybHd5IjcDMi9yZXBvcy93aF9mbG93RGF0YVNvdXJjZTEiLCJleHBpcmVzIjoxNTM2NzU4NjQ3LCJjb250ZW5VudFR5cGUiOiIiLCJoZWFkZXJzIjoiIiwibWV0aG9kIjoiR0VUIn0=")*/
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
if proxyAddr != "" {
|
||||
proxyUrl, err := url.Parse(proxyAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr.Proxy = http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
client.Timeout = time.Second * 15
|
||||
defer client.CloseIdleConnections()
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
//fmt.Println(res)
|
||||
//fmt.Println(string(body))
|
||||
if res.StatusCode != 200 {
|
||||
return body, fmt.Errorf("status code: %d", res.StatusCode)
|
||||
}
|
||||
return body, nil
|
||||
|
||||
}
|
||||
|
||||
// Put 发送 PUT 请求并返回响应体
|
||||
func Put(url string, param []byte, header map[string]string) ([]byte, error) {
|
||||
payload := bytes.NewReader(param)
|
||||
// 创建新的 HTTP GET 请求
|
||||
req, err := http.NewRequest("PUT", url, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加请求头
|
||||
for k, v := range header {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
// 发送请求
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 确保响应体在函数结束时关闭
|
||||
defer res.Body.Close()
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查响应状态码
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Delete 发送 DELETE 请求并返回响应体
|
||||
func Delete(url string, param []byte, header map[string]string) ([]byte, error) {
|
||||
payload := bytes.NewReader(param)
|
||||
// 创建新的 HTTP DELETE 请求
|
||||
req, err := http.NewRequest("DELETE", url, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 添加请求头
|
||||
for k, v := range header {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
// 发送请求
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 确保响应体在函数结束时关闭
|
||||
defer res.Body.Close()
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查响应状态码
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("status code: %d", res.StatusCode)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package httpc
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Method = %s, want GET", r.Method)
|
||||
}
|
||||
if r.URL.Query().Get("name") != "zogo" {
|
||||
t.Errorf("query name = %q", r.URL.Query().Get("name"))
|
||||
}
|
||||
w.Write([]byte(`{"ok":true}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
body, err := Get(ts.URL+"/get?name=zogo", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if string(body) != `{"ok":true}` {
|
||||
t.Errorf("body = %q", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetWithHeader(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("X-Token") != "abc" {
|
||||
t.Errorf("X-Token = %q", r.Header.Get("X-Token"))
|
||||
}
|
||||
w.Write([]byte("ok"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
if _, err := Get(ts.URL, map[string]string{"X-Token": "abc"}); err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPost(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("Method = %s, want POST", r.Method)
|
||||
}
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Content-Type = %q, want application/json", r.Header.Get("Content-Type"))
|
||||
}
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
if string(b) != `{"id":1}` {
|
||||
t.Errorf("body = %q", string(b))
|
||||
}
|
||||
w.Write([]byte("posted"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
body, err := Post(ts.URL, []byte(`{"id":1}`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Post: %v", err)
|
||||
}
|
||||
if string(body) != "posted" {
|
||||
t.Errorf("body = %q", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostCustomContentType(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.Header.Get("Content-Type"), "text/plain") {
|
||||
t.Errorf("Content-Type = %q, want text/plain", r.Header.Get("Content-Type"))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
if _, err := Post(ts.URL, []byte("text"), map[string]string{"Content-Type": "text/plain"}); err != nil {
|
||||
t.Fatalf("Post: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("Method = %s, want PUT", r.Method)
|
||||
}
|
||||
w.Write([]byte("putted"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
body, err := Put(ts.URL, []byte(`{}`), nil)
|
||||
if err != nil || string(body) != "putted" {
|
||||
t.Errorf("Put = %q, %v", string(body), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Errorf("Method = %s, want DELETE", r.Method)
|
||||
}
|
||||
w.Write([]byte("deleted"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
body, err := Delete(ts.URL, []byte(`{}`), nil)
|
||||
if err != nil || string(body) != "deleted" {
|
||||
t.Errorf("Delete = %q, %v", string(body), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProxyEmptyProxy(t *testing.T) {
|
||||
// 代理地址为空时应直连
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("direct"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
body, err := GetProxy(ts.URL, nil, "")
|
||||
if err != nil || string(body) != "direct" {
|
||||
t.Errorf("GetProxy 空代理 = %q, %v", string(body), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorStatusCode(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
_, err := Get(ts.URL, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "500") {
|
||||
t.Errorf("非 2xx 响应应返回错误,got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user