diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9c3d5..28cd6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## v1.5.1 (2026-06-08) +- **JS 对齐 & 安全加固**: + - HTTP 方法名统一更正为全大写(`GET`, `POST`, `PUT`, `DELETE`),强化协议语义。 + - **Headers 优化**: JS 侧 Headers 参数从变长字符串改为更符合 JS 习惯的 `map[string]string` 对象。 + - **深度安全扫描**: 在 `POST/PUT` 请求中对 `Multipart` 涉及的文件路径进行前置扫描,强制通过 `file.VerifyPathForSafeMode` 校验,严防低代码环境下的敏感文件泄露。 + - **结果集对齐**: `Result.Save` 方法现在同样受安全沙箱控制。 + ## v1.3.4 (2026-05-30) - **API 变更**: 将 `timeout(ms)` 拆分为 `new(ms)` 和 `newH2C(ms)` 以支持 HTTP/2 Cleartext。 - **安全性**: 移除 `setGlobalHeader` / `getGlobalHeader` 以增强脚本间隔离。 diff --git a/client.go b/client.go index c9a6813..008cb06 100644 --- a/client.go +++ b/client.go @@ -188,7 +188,7 @@ func (client *Client) doByRequest(manualDo bool, request *http.Request, method, } } if !foundID { - headers = append(headers, HeaderRequestID, string(encoding.Hex(rand.Bytes(16)))) + headers = append(headers, HeaderRequestID, encoding.Hex(rand.Bytes(16))) } // 续传 X-Forwarded-For diff --git a/go.mod b/go.mod index e8ae2bd..cc677c2 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( apigo.cc/go/id v1.5.0 // indirect apigo.cc/go/safe v1.5.0 // indirect apigo.cc/go/shell v1.5.0 // indirect - golang.org/x/crypto v0.51.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 37436d7..bafd3f8 100644 --- a/go.sum +++ b/go.sum @@ -24,12 +24,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/js_export.go b/js_export.go index 4df01e7..b2333e2 100644 --- a/js_export.go +++ b/js_export.go @@ -1,73 +1,112 @@ package http import ( + "context" "time" + "apigo.cc/go/file" "apigo.cc/go/jsmod" ) func init() { jsmod.Register("http", map[string]any{ // Static requests with default timeout (30s) - "get": func(url string, headers ...string) *jsResult { - return wrapResult(Get(url, headers...)) + "GET": func(ctx context.Context, url string, headers map[string]string) *jsResult { + return wrapResult(ctx, Get(url, flattenHeaders(headers)...)) }, - "post": func(url string, data any, headers ...string) *jsResult { - return wrapResult(Post(url, data, headers...)) + "POST": func(ctx context.Context, url string, data any, headers map[string]string) (*jsResult, error) { + if err := verifyMultipart(ctx, data); err != nil { + return nil, err + } + return wrapResult(ctx, Post(url, data, flattenHeaders(headers)...)), nil }, - "put": func(url string, data any, headers ...string) *jsResult { - return wrapResult(Put(url, data, headers...)) + "PUT": func(ctx context.Context, url string, data any, headers map[string]string) (*jsResult, error) { + if err := verifyMultipart(ctx, data); err != nil { + return nil, err + } + return wrapResult(ctx, Put(url, data, flattenHeaders(headers)...)), nil }, - "delete": func(url string, data any, headers ...string) *jsResult { - return wrapResult(Delete(url, data, headers...)) + "DELETE": func(ctx context.Context, url string, data any, headers map[string]string) *jsResult { + return wrapResult(ctx, Delete(url, data, flattenHeaders(headers)...)) }, // Client creation - "new": func(ms int) *jsClient { + "New": func(ms int) *jsClient { return &jsClient{c: NewClient(time.Duration(ms) * time.Millisecond)} }, - "newH2C": func(ms int) *jsClient { + "NewH2C": func(ms int) *jsClient { return &jsClient{c: NewClientH2C(time.Duration(ms) * time.Millisecond)} }, // Data markers - "form": func(data map[string]string) Form { + "Form": func(data map[string]string) Form { return Form(data) }, - "multipart": func(data map[string]any) Multipart { + "Multipart": func(data map[string]any) Multipart { return Multipart(data) }, }) } +func flattenHeaders(m map[string]string) []string { + if len(m) == 0 { + return nil + } + res := make([]string, 0, len(m)*2) + for k, v := range m { + res = append(res, k, v) + } + return res +} + +func verifyMultipart(ctx context.Context, data any) error { + if m, ok := data.(Multipart); ok { + for _, v := range m { + if s, ok := v.(string); ok && file.Exists(s) { + if _, err := file.VerifyPathForSafeMode(ctx, s); err != nil { + return err + } + } + } + } + return nil +} + // jsClient wraps the internal Client to provide a JS-friendly interface type jsClient struct { c *Client } -func (jc *jsClient) Get(url string, headers ...string) *jsResult { - return wrapResult(jc.c.Get(url, headers...)) +func (jc *jsClient) GET(ctx context.Context, url string, headers map[string]string) *jsResult { + return wrapResult(ctx, jc.c.Get(url, flattenHeaders(headers)...)) } -func (jc *jsClient) Post(url string, data any, headers ...string) *jsResult { - return wrapResult(jc.c.Post(url, data, headers...)) +func (jc *jsClient) POST(ctx context.Context, url string, data any, headers map[string]string) (*jsResult, error) { + if err := verifyMultipart(ctx, data); err != nil { + return nil, err + } + return wrapResult(ctx, jc.c.Post(url, data, flattenHeaders(headers)...)), nil } -func (jc *jsClient) Put(url string, data any, headers ...string) *jsResult { - return wrapResult(jc.c.Put(url, data, headers...)) +func (jc *jsClient) PUT(ctx context.Context, url string, data any, headers map[string]string) (*jsResult, error) { + if err := verifyMultipart(ctx, data); err != nil { + return nil, err + } + return wrapResult(ctx, jc.c.Put(url, data, flattenHeaders(headers)...)), nil } -func (jc *jsClient) Delete(url string, data any, headers ...string) *jsResult { - return wrapResult(jc.c.Delete(url, data, headers...)) +func (jc *jsClient) DELETE(ctx context.Context, url string, data any, headers map[string]string) *jsResult { + return wrapResult(ctx, jc.c.Delete(url, data, flattenHeaders(headers)...)) } // jsResult wraps *Result to hide sensitive methods like Save() type jsResult struct { - r *Result + ctx context.Context + r *Result } -func wrapResult(r *Result) *jsResult { - return &jsResult{r: r} +func wrapResult(ctx context.Context, r *Result) *jsResult { + return &jsResult{ctx: ctx, r: r} } func (jr *jsResult) String() string { @@ -99,3 +138,11 @@ func (jr *jsResult) Error() string { } return "" } + +func (jr *jsResult) Save(filename string) error { + p, err := file.VerifyPathForSafeMode(jr.ctx, filename) + if err != nil { + return err + } + return jr.r.Save(p) +}