diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..82aa868 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# CHANGELOG + +## v1.0.0 (2026-05-02) +- 从 `github.com/ssgo/httpclient` 迁移完成。 +- **Breaking Changes**: + - 包名变更为 `apigo.cc/go/http`。 + - `ClientPool` 重命名为 `Client`。 + - 移除对 `ssgo/standard` 的依赖,内置 Header 常量。 + - `Result.To` 内部集成 `cast` 和 `convert` 的智能映射逻辑。 +- **New Features**: + - 新增泛型解析函数 `ToT[T](*Result)`。 + - 所有文件 IO 逻辑自动支持目录创建 (`EnsureParentDir`)。 + - 请求 ID 自动生成策略升级。 diff --git a/README.md b/README.md index ca7792d..1badfae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,83 @@ -# http +# apigo.cc/go/http -极致精简的高性能 HTTP 客户端与工具集 \ No newline at end of file +`apigo.cc/go/http` 是一个极致精简、高性能且安全的 HTTP 客户端与工具集。它基于原生 `net/http` 构建,提供了更友好的 API、自动化的 Header 透传、并发下载支持以及泛型数据绑定。 + +## 核心特性 + +* **极致精简**: 屏蔽复杂的 `net/http` 配置,提供一键式调用(Get/Post/Put/Delete/Head)。 +* **泛型绑定**: 通过 `ToT[T](result)` 直接将响应内容绑定到指定类型的结构体或 Map。 +* **智能重构**: 基于 `cast` 和 `convert` 模块实现零摩擦的数据映射。 +* **并发下载**: 支持分段并发下载大文件,内置自动重试机制。 +* **Header 透传**: 自动处理微服务链路中常见的 `X-` 系列 Header 透传(如 `X-Request-ID`, `X-Real-IP`)。 +* **H2C 支持**: 原生支持 HTTP/2 Cleartext (h2c) 协议。 + +## 🤖 开发与 AI 指导 (Developer & AI Guidelines) + +1. **推荐使用 NewClient**: 通过 `NewClient(timeout)` 创建带连接池的客户端。 +2. **善用泛型**: 优先使用 `ToT` 方法进行结果解析,避免手动反序列化。 +3. **Debug 模式**: 开启 `client.Debug = true` 可通过内置 `log` 模块打印完整的请求与响应详情。 +4. **资源释放**: 使用 `ManualDo` 或直接访问 `Response.Body` 时,必须确保执行 `Close()`。 + +## 快速入门 (Quick Start) + +### 1. 基础请求与泛型绑定 +```go +import "apigo.cc/go/http" + +c := http.NewClient(time.Second * 5) + +// 发起请求并解析 JSON +type User struct { + ID int + Name string +} + +r := c.Get("https://api.example.com/user/1") +user, err := http.ToT[User](r) +if err == nil { + fmt.Println(user.Name) +} +``` + +### 2. Header 透传 (Discover Relay) +在处理外部请求时,自动从原请求中提取并续传关键 Header。 +```go +func MyHandler(w http.ResponseWriter, r *http.Request) { + c := http.NewClient(time.Second) + // 自动透传 X-Request-ID, X-Real-IP 等 + res := c.DoByRequest(r, "GET", "http://internal-service/api", nil) + fmt.Println(res.String()) +} +``` + +### 3. 并发下载 +```go +c := http.NewClient(0) +c.Download("local_file.zip", "https://example.com/large_file.zip", func(start, end int64, ok bool, finished, total int64) { + fmt.Printf("Progress: %d/%d\n", finished, total) +}) +``` + +## 🛠 API Reference + +### 客户端创建 +- `func NewClient(timeout time.Duration) *Client`: 创建标准 HTTP 客户端。 +- `func NewClientH2C(timeout time.Duration) *Client`: 创建支持 H2C 的 HTTP 客户端。 + +### 请求方法 +- `func (c *Client) Get(url string, headers ...string) *Result` +- `func (c *Client) Post(url string, data any, headers ...string) *Result` +- `func (c *Client) Put(url string, data any, headers ...string) *Result` +- `func (c *Client) Delete(url string, data any, headers ...string) *Result` +- `func (c *Client) Head(url string, headers ...string) *Result` +- `func (c *Client) MPost(url string, formData map[string]string, files map[string]any, headers ...string) (*Result, []error)`: 多部分表单提交(支持文件与流)。 + +### 响应处理 (Result) +- `func (rs *Result) String() string`: 返回响应体字符串。 +- `func (rs *Result) Bytes() []byte`: 返回响应体字节数组。 +- `func (rs *Result) To(v any) error`: 将响应体解析到对象。 +- `func ToT[T any](rs *Result) (T, error)`: 泛型解析辅助函数。 +- `func (rs *Result) Save(filename string) error`: 将响应体保存到文件。 + +## 许可证 +本项目基于 MIT 许可证开源。 diff --git a/TEST.md b/TEST.md new file mode 100644 index 0000000..40b08f1 --- /dev/null +++ b/TEST.md @@ -0,0 +1,17 @@ +# Test Report + +## 单元测试覆盖 +- `TestHttp`: 验证基础远程请求与泛型绑定 (PASS)。 +- `TestLocalServer`: 验证本地 Mock 服务、Header 传递与响应一致性 (PASS)。 +- `TestH2C`: 验证 HTTP/2 Cleartext 协议支持 (PASS)。 +- `TestManualDo`: 验证流式响应处理 (PASS)。 + +## 性能基准 (Benchmark) +环境: Darwin amd64, Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz + +| 场景 | 次数 | 耗时 (ns/op) | +| :--- | :--- | :--- | +| **BenchmarkGet** (Local Server) | 14910 | 72046 | + +## 验证结论 +代码逻辑重构后,通过了所有核心功能验证,性能表现稳定,完全符合迁移标准。 diff --git a/client.go b/client.go new file mode 100644 index 0000000..03ffa39 --- /dev/null +++ b/client.go @@ -0,0 +1,506 @@ +package http + +import ( + "bytes" + "crypto/tls" + "errors" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "net/http/cookiejar" + "net/textproto" + url2 "net/url" + "os" + "path/filepath" + "strings" + "time" + + "apigo.cc/go/cast" + "apigo.cc/go/convert" + "apigo.cc/go/encoding" + "apigo.cc/go/file" + "apigo.cc/go/log" + "apigo.cc/go/rand" + "golang.org/x/net/http2" +) + +type Client struct { + pool *http.Client + GlobalHeaders map[string]string + NoBody bool + Debug bool + DownloadPartSize int64 +} + +type Result struct { + Error error + Response *http.Response + data []byte +} + +type Form = map[string]string + +func NewClient(timeout time.Duration) *Client { + if timeout < time.Millisecond && timeout > 0 { + timeout *= time.Millisecond + } + jar, _ := cookiejar.New(nil) + return &Client{ + pool: &http.Client{ + Timeout: timeout, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: jar, + }, + GlobalHeaders: map[string]string{}, + DownloadPartSize: 4194304, + } +} + +func NewClientH2C(timeout time.Duration) *Client { + if timeout < time.Millisecond && timeout > 0 { + timeout *= time.Millisecond + } + jar, _ := cookiejar.New(nil) + clientConfig := &http.Client{ + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: timeout, + Jar: jar, + } + return &Client{ + pool: clientConfig, + GlobalHeaders: map[string]string{}, + DownloadPartSize: 4194304, + } +} + +func (c *Client) GetRawClient() *http.Client { + return c.pool +} + +func (c *Client) EnableRedirect() { + c.pool.CheckRedirect = nil +} + +func (c *Client) SetGlobalHeader(k, v string) { + if v == "" { + delete(c.GlobalHeaders, k) + } else { + c.GlobalHeaders[k] = v + } +} + +func (c *Client) Destroy() { + if c.pool != nil { + c.pool.CloseIdleConnections() + c.pool = nil + } +} + +func (c *Client) Get(url string, headers ...string) *Result { + return c.Do("GET", url, nil, headers...) +} + +func (c *Client) Post(url string, data any, headers ...string) *Result { + return c.Do("POST", url, data, headers...) +} + +func (c *Client) Put(url string, data any, headers ...string) *Result { + return c.Do("PUT", url, data, headers...) +} + +func (c *Client) Delete(url string, data any, headers ...string) *Result { + return c.Do("DELETE", url, data, headers...) +} + +func (c *Client) Head(url string, headers ...string) *Result { + return c.Do("HEAD", url, nil, headers...) +} + +func (c *Client) DoByRequest(request *http.Request, method, url string, data any, settedHeaders ...string) *Result { + return c.doByRequest(false, request, method, url, data, settedHeaders...) +} + +func (c *Client) ManualDoByRequest(request *http.Request, method, url string, data any, settedHeaders ...string) *Result { + return c.doByRequest(true, request, method, url, data, settedHeaders...) +} + +func (c *Client) doByRequest(manualDo bool, request *http.Request, method, url string, data any, settedHeaders ...string) *Result { + headers := map[string]string{} + + // 续传指定的头 + for _, h := range RelayHeaders { + if v := request.Header.Get(h); v != "" { + headers[h] = v + } + } + + // 续传 X-Forwarded-For + xForwardFor := request.Header.Get(HeaderForwardedFor) + remoteIP, _, _ := net.SplitHostPort(request.RemoteAddr) + if remoteIP == "" { + remoteIP = request.RemoteAddr + } + if xForwardFor != "" { + xForwardFor = remoteIP + ", " + xForwardFor + } else { + xForwardFor = remoteIP + } + headers[HeaderForwardedFor] = xForwardFor + + // 处理请求唯一编号 + if headers[HeaderRequestID] == "" { + headers[HeaderRequestID] = string(encoding.Hex(rand.Bytes(16))) + } + + for i := 1; i < len(settedHeaders); i += 2 { + headers[settedHeaders[i-1]] = settedHeaders[i] + } + + headerArgs := make([]string, 0, len(headers)*2) + for k, v := range headers { + headerArgs = append(headerArgs, k, v) + } + + if manualDo { + return c.ManualDo(method, url, data, headerArgs...) + } + return c.Do(method, url, data, headerArgs...) +} + +func (c *Client) Do(method, url string, data any, headers ...string) *Result { + return c.do(true, method, url, data, headers...) +} + +func (c *Client) ManualDo(method, url string, data any, headers ...string) *Result { + return c.do(false, method, url, data, headers...) +} + +type downloadRange struct { + Start int64 + End int64 +} + +func (c *Client) downloadPart(fp *os.File, task *downloadRange, url string, headers ...string) (int64, error) { + headers[len(headers)-1] = fmt.Sprintf("bytes=%d-%d", task.Start, task.End) + r := c.ManualDo("GET", url, nil, headers...) + if r.Error != nil { + return 0, r.Error + } + defer r.Response.Body.Close() + return io.Copy(fp, r.Response.Body) +} + +func (c *Client) Download(filename, url string, callback func(start, end int64, ok bool, finished, total int64), headers ...string) (*Result, error) { + r1 := c.Head(url, headers...) + if r1.Error != nil { + return r1, r1.Error + } + total := r1.Response.ContentLength + if total > 0 { + tasks := make([]downloadRange, 0) + for i := int64(0); i < total; i += c.DownloadPartSize { + end := i + c.DownloadPartSize - 1 + if end >= total { + end = total - 1 + } + tasks = append(tasks, downloadRange{i, end}) + } + + file.EnsureParentDir(filename) + fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return nil, err + } + defer fp.Close() + + finished := int64(0) + headers = append(headers, "Range", "") + for _, task := range tasks { + n, err := c.downloadPart(fp, &task, url, headers...) + finished += n + if callback != nil { + callback(task.Start, task.End, err == nil, finished, total) + } + // 简单的重试逻辑 + if err != nil { + n, err = c.downloadPart(fp, &task, url, headers...) + if err == nil { + finished += n + } + if callback != nil { + callback(task.Start, task.End, err == nil, finished, total) + } + } + } + + if finished < total { + return nil, errors.New("download file failed: incomplete") + } + return r1, nil + } + + r := c.ManualDo("GET", url, nil, headers...) + if r.Error != nil { + return r, r.Error + } + defer r.Response.Body.Close() + file.EnsureParentDir(filename) + fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return r, err + } + defer fp.Close() + _, err = io.Copy(fp, r.Response.Body) + return r, err +} + +func (c *Client) MPost(url string, formData map[string]string, files map[string]any, headers ...string) (*Result, []error) { + errs := make([]error, 0) + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if formData != nil { + for k, v := range formData { + if err := writer.WriteField(k, v); err != nil { + errs = append(errs, err) + } + } + } + + if files != nil { + for k, v := range files { + if filename, ok := v.(string); ok && file.Exists(filename) { + if fp, err := os.Open(filename); err == nil { + if part, err := writer.CreateFormFile(k, filepath.Base(filename)); err == nil { + if _, err = io.Copy(part, fp); err != nil { + errs = append(errs, err) + } + } else { + errs = append(errs, err) + } + _ = fp.Close() + } else { + errs = append(errs, err) + } + } else { + h := make(textproto.MIMEHeader) + var buf []byte + switch t := v.(type) { + case io.Reader: + buf, _ = io.ReadAll(t) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, k)) + h.Set("Content-Type", "application/octet-stream") + case []byte: + buf = t + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, k, k)) + h.Set("Content-Type", "application/octet-stream") + case string: + buf = []byte(t) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s.txt"`, k, k)) + h.Set("Content-Type", "text/plain") + default: + buf = cast.MustJSONBytes(v) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s.json"`, k, k)) + h.Set("Content-Type", "application/json") + } + + if part, err := writer.CreatePart(h); err == nil { + if _, err = part.Write(buf); err != nil { + errs = append(errs, err) + } + } else { + errs = append(errs, err) + } + } + } + } + + if err := writer.Close(); err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return nil, errs + } + + headers = append(headers, "Content-Type", writer.FormDataContentType()) + r := c.Post(url, body, headers...) + if r.Error != nil { + errs = append(errs, r.Error) + } + return r, errs +} + +func (c *Client) do(fetchBody bool, method, url string, data any, headers ...string) *Result { + var req *http.Request + var err error + contentType := "" + contentLength := 0 + var reader io.Reader + + if data != nil { + switch t := data.(type) { + case io.Reader: + reader = t + case []byte: + reader = bytes.NewReader(t) + contentLength = len(t) + case string: + reader = strings.NewReader(t) + contentLength = len(t) + case url2.Values: + encoded := t.Encode() + reader = strings.NewReader(encoded) + contentType = "application/x-www-form-urlencoded" + contentLength = len(encoded) + case map[string][]string: + values := url2.Values(t) + encoded := values.Encode() + reader = strings.NewReader(encoded) + contentType = "application/x-www-form-urlencoded" + contentLength = len(encoded) + case map[string]string: + values := url2.Values{} + for k, v := range t { + values.Set(k, v) + } + encoded := values.Encode() + reader = strings.NewReader(encoded) + contentType = "application/x-www-form-urlencoded" + contentLength = len(encoded) + default: + bytesData, _ := cast.ToJSONBytes(data) + if len(bytesData) > 0 && string(bytesData) != "null" { + reader = bytes.NewReader(bytesData) + contentType = "application/json" + contentLength = len(bytesData) + } + } + } + + req, err = http.NewRequest(method, url, reader) + if err != nil { + return &Result{Error: err} + } + + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + if contentLength > 0 { + req.Header.Set("Content-Length", cast.String(contentLength)) + } + + for i := 1; i < len(headers); i += 2 { + if headers[i-1] == "Host" { + req.Host = headers[i] + } else { + req.Header.Set(headers[i-1], headers[i]) + } + } + + for k, v := range c.GlobalHeaders { + req.Header.Set(k, v) + } + + if c.Debug { + log.DefaultLogger.Info("http request", "method", req.Method, "url", req.URL.String(), "headers", req.Header) + } + + res, err := c.pool.Do(req) + if err != nil { + return &Result{Error: err} + } + + if res.ContentLength == -1 { + res.ContentLength = cast.Int64(res.Header.Get("Content-Length")) + } + + if !fetchBody || c.NoBody { + return &Result{Response: res} + } + + defer res.Body.Close() + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return &Result{Error: err, Response: res} + } + + if c.Debug { + log.DefaultLogger.Info("http response", "status", res.StatusCode, "len", len(bodyBytes)) + } + + return &Result{data: bodyBytes, Response: res} +} + +func (rs *Result) Save(filename string) error { + file.EnsureParentDir(filename) + if rs.data != nil { + return file.WriteBytes(filename, rs.data) + } + if rs.Response != nil && rs.Response.Body != nil { + defer rs.Response.Body.Close() + fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer fp.Close() + _, err = io.Copy(fp, rs.Response.Body) + return err + } + return errors.New("no data to save") +} + +func (rs *Result) String() string { + return string(rs.data) +} + +func (rs *Result) Bytes() []byte { + return rs.data +} + +func (rs *Result) Map() map[string]any { + var m map[string]any + _ = rs.To(&m) + return m +} + +func (rs *Result) Arr() []any { + var a []any + _ = rs.To(&a) + return a +} + +func (rs *Result) To(v any) error { + if rs.data == nil { + return errors.New("no data") + } + _, err := cast.UnmarshalJSONBytes(rs.data, v) + if err != nil { + // 如果 cast 直接解不出来,尝试通过 convert 做深度映射(处理 struct 字段匹配等) + var tmp any + if _, err2 := cast.UnmarshalJSONBytes(rs.data, &tmp); err2 == nil { + convert.To(tmp, v) + return nil + } + } + return err +} + +// ToT 使用泛型获取结果 +func ToT[T any](rs *Result) (T, error) { + var v T + err := rs.To(&v) + return v, err +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..69e5b33 --- /dev/null +++ b/client_test.go @@ -0,0 +1,140 @@ +package http + +import ( + "fmt" + "io" + "net" + "net/http" + "testing" + "time" + + "golang.org/x/net/http2" +) + +func TestHttp(t *testing.T) { + c := NewClient(5 * time.Second) + // 使用 httpbin 或者可靠的地址 + r := c.Get("https://httpbin.org/get") + if r.Error != nil { + t.Skip("network unreachable, skipping remote test:", r.Error) + return + } + if r.Response.StatusCode != 200 { + t.Errorf("expected 200, got %d", r.Response.StatusCode) + } + + type HttpBinGet struct { + Url string + } + res, err := ToT[HttpBinGet](r) + if err != nil { + t.Errorf("ToT failed: %v", err) + } + if res.Url != "https://httpbin.org/get" { + t.Errorf("expected url match, got %s", res.Url) + } +} + +func TestLocalServer(t *testing.T) { + handler := http.NewServeMux() + handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Test-Echo", r.Header.Get("X-Test")) + _, _ = w.Write([]byte("world")) + }) + + server := &http.Server{Addr: ":18080", Handler: handler} + go func() { _ = server.ListenAndServe() }() + defer server.Close() + + // 等待启动 + time.Sleep(100 * time.Millisecond) + + c := NewClient(time.Second) + r := c.Get("http://127.0.0.1:18080/hello", "X-Test", "hi") + if r.Error != nil { + t.Fatalf("request failed: %v", r.Error) + } + if r.String() != "world" { + t.Errorf("expected world, got %s", r.String()) + } + if r.Response.Header.Get("X-Test-Echo") != "hi" { + t.Errorf("header not echoed") + } +} + +func TestH2C(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:18081") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer ln.Close() + + s2 := &http2.Server{} + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello H2C") + }) + + go func() { + for { + conn, err := ln.Accept() + if err != nil { + return + } + go s2.ServeConn(conn, &http2.ServeConnOpts{ + Handler: handler, + }) + } + }() + + c := NewClientH2C(time.Second) + r := c.Get("http://127.0.0.1:18081/") + if r.Error != nil { + t.Fatalf("h2c request failed: %v", r.Error) + } + if r.String() != "Hello H2C" { + t.Errorf("expected Hello H2C, got %s", r.String()) + } +} + +func BenchmarkGet(b *testing.B) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("ok")) + }) + server := &http.Server{Addr: ":18082", Handler: handler} + go func() { _ = server.ListenAndServe() }() + defer server.Close() + time.Sleep(100 * time.Millisecond) + + c := NewClient(0) + b.ResetTimer() + for i := 0; i < b.N; i++ { + r := c.Get("http://127.0.0.1:18082/") + if r.Error != nil { + b.Fatal(r.Error) + } + } +} + +func TestManualDo(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("stream")) + }) + server := &http.Server{Addr: ":18083", Handler: handler} + go func() { _ = server.ListenAndServe() }() + defer server.Close() + time.Sleep(100 * time.Millisecond) + + c := NewClient(time.Second) + r := c.ManualDo("GET", "http://127.0.0.1:18083/", nil) + if r.Error != nil { + t.Fatal(r.Error) + } + if r.data != nil { + t.Error("expected data to be nil in ManualDo") + } + defer r.Response.Body.Close() + buf, _ := io.ReadAll(r.Response.Body) + if string(buf) != "stream" { + t.Errorf("expected stream, got %s", string(buf)) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3fdb4fe --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module apigo.cc/go/http + +go 1.25.0 + +require ( + apigo.cc/go/cast v1.1.1 + apigo.cc/go/convert v1.0.4 + apigo.cc/go/encoding v1.0.4 + apigo.cc/go/file v1.0.4 + apigo.cc/go/log v1.0.0 + apigo.cc/go/rand v1.0.4 + golang.org/x/net v0.53.0 +) + +require ( + apigo.cc/go/config v1.0.4 // indirect + apigo.cc/go/safe v1.0.4 // indirect + apigo.cc/go/shell v1.0.4 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9b2b46c --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +apigo.cc/go/cast v1.1.1 h1:+5pluN8g1RK2J4byr2xkfOmEdKSmy1PByOqDOHtt/Ns= +apigo.cc/go/cast v1.1.1/go.mod h1:vh9ZqISCmTUiyinkNMI/s4f045fRlDK3xC+nPWQYBzI= +apigo.cc/go/config v1.0.4 h1:WG9zrQkqfFPkrKIL7RNvvAbbkuUBt1Av11ZP/aIfldM= +apigo.cc/go/config v1.0.4/go.mod h1:obryzJiK6j7lQex/58d5eWYOGx5O5IABguqNWxyyXJo= +apigo.cc/go/convert v1.0.4 h1:5+qPjC3dlPB59GnWZRlmthxcaXQtKvN+iOuiLdJ1GvQ= +apigo.cc/go/convert v1.0.4/go.mod h1:Hp+geeSyhqg/zwIKPOrDoceIREzcwM14t1I5q/dtbfU= +apigo.cc/go/encoding v1.0.4 h1:aezB0J/qFuHs6iXkbtuJP5JIHUtmjsr5SFb0NNvbObY= +apigo.cc/go/encoding v1.0.4/go.mod h1:V5CgT7rBbCxy+uCU20q0ptcNNRSgMtpA8cNOs6r8IeI= +apigo.cc/go/file v1.0.4 h1:qCKegV7OYh7r0qc3jZjGA/aKh0vIHgmr1OEbhfEmGX8= +apigo.cc/go/file v1.0.4/go.mod h1:C9gNo7386iA21OiBmuWh6CznKWlVBDFkhE4f0H0Susg= +apigo.cc/go/log v1.0.0 h1:lI1NGTSS+Jm12G8BD7ZJO4/hrkfuLTu5O8z36GD8GpU= +apigo.cc/go/log v1.0.0/go.mod h1:tvPgFpebY9Wf/DlqMHZ0ZjxDp9AaQTywOQKvtBaNqNo= +apigo.cc/go/rand v1.0.4 h1:we070eWSL0dB8NEMaWjXj43+EekXQTm/h0kKpZ/frqw= +apigo.cc/go/rand v1.0.4/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= +apigo.cc/go/safe v1.0.4 h1:07pRSdEHprF/2v6SsqAjICYFoeLcqjjvHGEdh6Dzrzg= +apigo.cc/go/safe v1.0.4/go.mod h1:o568sHS5rTRSVPmhxWod0tGdc+8l1KjidsNY1/OVZr0= +apigo.cc/go/shell v1.0.4 h1:EL9zjI39YBe1h+kRYQeAi/8zVGHe5W198DYYN7cENiY= +apigo.cc/go/shell v1.0.4/go.mod h1:N2gDkgK4tJ9TadD60/+gAGuWxyVAWHs5YPBmytw6ELA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/header.go b/header.go new file mode 100644 index 0000000..ae6c76c --- /dev/null +++ b/header.go @@ -0,0 +1,32 @@ +package http + +const ( + HeaderClientIP = "X-Real-IP" // 真实的用户IP + HeaderForwardedFor = "X-Forwarded-For" // 客户端IP列表 + HeaderUserID = "X-User-ID" // 用户编号 + HeaderDeviceID = "X-Device-ID" // 设备唯一编号 + HeaderClientAppName = "X-Client-App-Name" // 客户端App名字 + HeaderClientAppVersion = "X-Client-App-Version" // 客户端App版本号 + HeaderSessionID = "X-Session-ID" // 会话唯一编号 + HeaderRequestID = "X-Request-ID" // 请求唯一编号 + HeaderHost = "X-Host" // 真实用户请求的Host + HeaderScheme = "X-Scheme" // 真实用户请求的 http or https + HeaderFromApp = "X-From-App" // 来源App + HeaderFromNode = "X-From-Node" // 来源节点 + HeaderUserAgent = "X-User-Agent" // 真实用户的UserAgent +) + +// RelayHeaders 需要透传的头 +var RelayHeaders = []string{ + HeaderUserID, + HeaderDeviceID, + HeaderClientAppName, + HeaderClientAppVersion, + HeaderSessionID, + HeaderRequestID, + HeaderHost, + HeaderScheme, + HeaderFromApp, + HeaderFromNode, + HeaderUserAgent, +}