Compare commits

...

2 Commits
v1.3.4 ... main

Author SHA1 Message Date
AI Engineer
12c9672f40 feat: align JS exports to uppercase methods, map headers, and add multipart safety (by AI) 2026-06-10 11:53:11 +08:00
AI Engineer
e0a3aa2bfb 对齐 Tag v1.5.0 (By AI) 2026-06-03 20:10:57 +08:00
5 changed files with 112 additions and 60 deletions

View File

@ -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` 以增强脚本间隔离。

View File

@ -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

24
go.mod
View File

@ -3,22 +3,22 @@ module apigo.cc/go/http
go 1.25.0
require (
apigo.cc/go/cast v1.3.3
apigo.cc/go/encoding v1.3.1
apigo.cc/go/file v1.3.2
apigo.cc/go/jsmod v1.0.0
apigo.cc/go/log v1.3.4
apigo.cc/go/rand v1.3.1
apigo.cc/go/cast v1.5.0
apigo.cc/go/encoding v1.5.0
apigo.cc/go/file v1.5.0
apigo.cc/go/jsmod v1.5.0
apigo.cc/go/log v1.5.0
apigo.cc/go/rand v1.5.0
golang.org/x/net v0.54.0
)
require (
apigo.cc/go/config v1.3.1 // indirect
apigo.cc/go/id v1.3.1 // indirect
apigo.cc/go/safe v1.3.1 // indirect
apigo.cc/go/shell v1.3.1 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.44.0 // indirect
apigo.cc/go/config v1.5.0 // indirect
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.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
)

46
go.sum
View File

@ -1,35 +1,33 @@
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
apigo.cc/go/jsmod v1.0.0 h1:lVQMq0tCno4kbHlQ3j5wzsm+v24J+bznIoHxpton0pE=
apigo.cc/go/jsmod v1.0.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
apigo.cc/go/config v1.5.0 h1:Yuz9QEb11XXG4XkhDi/ueT2M1T3Q9PElE5tiakvjehs=
apigo.cc/go/config v1.5.0/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw=
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0=
apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8=
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/log v1.5.0 h1:kQuLLtbt33mEuc/xJVcy8NODXkso/QKSZWNclKrSpsI=
apigo.cc/go/log v1.5.0/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM=
apigo.cc/go/shell v1.5.0 h1:WLDMMqUU0INeaBDmQsTPr0h/NfB2RknAtiJ5NL467+Q=
apigo.cc/go/shell v1.5.0/go.mod h1:rYHA77d5hEsQHcJrbAWf1pHy0sxayeJ0gU55LA/JWQk=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
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=

View File

@ -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)
}