Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b2a7ab163 | ||
|
|
a5339d98d5 | ||
|
|
b2364194b1 | ||
|
|
c25a442b38 | ||
|
|
c0e0d7c280 | ||
|
|
6b6e191c71 | ||
|
|
4ef4578f1d |
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,6 +1,15 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## v1.0.3 (2026-05-09)
|
## v1.4.0 (2026-05-30)
|
||||||
|
- **重大重构**: 引入数据驱动的 API 编排模式。
|
||||||
|
- **新增**: `ActionRegistry` 支持注册 Go 结构体或 JSON 定义作为 API 模板。
|
||||||
|
- **新增**: `CallBy(name, payload)` 动态调用入口,自动处理模板实例化与动态参数合并。
|
||||||
|
- **新增**: `SetConfig` 支持内存配置热更新,并自动识别 `**` 前缀的加密字符串为 `SafeBuf`。
|
||||||
|
- **新增**: 支持 `form` 和 `multipart` 请求格式。
|
||||||
|
- **新增**: 跨语言签名支持,提供 `SetJSRunner` 钩子允许执行 JS 签名逻辑。
|
||||||
|
- **JSMOD**: 注册 `call`, `setConfig`, `registerAction`, `registerSigner` 到 `jsmod`。
|
||||||
|
|
||||||
|
## v1.3.3 (2026-05-30)
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- **Frictionless Memory Safety**: introduced `HttpRequest.SetHeader(key, values...)` with automatic secret extraction and buffer tracking.
|
- **Frictionless Memory Safety**: introduced `HttpRequest.SetHeader(key, values...)` with automatic secret extraction and buffer tracking.
|
||||||
|
|||||||
66
action.go
66
action.go
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
@ -37,6 +38,71 @@ type ValidatableAction interface {
|
|||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormatAction 定义请求体格式
|
||||||
|
type FormatAction interface {
|
||||||
|
GetFormat() string // 返回: "json", "form", "multipart"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionRegistry 存储已注册的 Action 模板或类型
|
||||||
|
var actionRegistry = make(map[string]any)
|
||||||
|
|
||||||
|
// RegisterAction 注册 API 动作。
|
||||||
|
// definition 可以是一个结构体实例(用作类型模板)或一个 map[string]any(用作动态定义)。
|
||||||
|
func RegisterAction(name string, definition any) {
|
||||||
|
if definition == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t := reflect.TypeOf(definition)
|
||||||
|
for t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() == reflect.Struct {
|
||||||
|
actionRegistry[name] = t
|
||||||
|
} else if m, ok := definition.(map[string]any); ok {
|
||||||
|
ga := &GenericAction{
|
||||||
|
name: name,
|
||||||
|
url: cast.String(m["url"]),
|
||||||
|
method: cast.String(m["method"]),
|
||||||
|
signer: cast.String(m["signer"]),
|
||||||
|
format: cast.String(m["format"]),
|
||||||
|
payload: make(map[string]any),
|
||||||
|
}
|
||||||
|
if p, ok := m["payload"].(map[string]any); ok {
|
||||||
|
ga.payload = p
|
||||||
|
}
|
||||||
|
actionRegistry[name] = ga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericAction 是一个动态 Action 容器,实现了所有 API 相关接口
|
||||||
|
type GenericAction struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
method string
|
||||||
|
signer string
|
||||||
|
format string
|
||||||
|
payload map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *GenericAction) ActionName() string { return a.name }
|
||||||
|
func (a *GenericAction) SignerName() string { return a.signer }
|
||||||
|
func (a *GenericAction) GetURL() string { return a.url }
|
||||||
|
func (a *GenericAction) GetMethod() string { return a.method }
|
||||||
|
func (a *GenericAction) GetFormat() string { return a.format }
|
||||||
|
func (a *GenericAction) Config() map[string]any {
|
||||||
|
return map[string]any{
|
||||||
|
"url": a.url,
|
||||||
|
"method": a.method,
|
||||||
|
"signer": a.signer,
|
||||||
|
"format": a.format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (a *GenericAction) MarshalJSON() ([]byte, error) {
|
||||||
|
return cast.ToJSONBytes(a.payload)
|
||||||
|
}
|
||||||
|
|
||||||
// HttpRequest 内部使用的请求描述结构,供 Signer 使用
|
// HttpRequest 内部使用的请求描述结构,供 Signer 使用
|
||||||
type HttpRequest struct {
|
type HttpRequest struct {
|
||||||
Url string
|
Url string
|
||||||
|
|||||||
67
api.go
67
api.go
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
@ -85,16 +86,31 @@ func Call[T any](action Action) (*T, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 发起底层 HTTP 调用
|
// 7. 处理请求格式 (Form/Multipart)
|
||||||
|
payload := httpReq.Payload
|
||||||
|
if fa, ok := action.(FormatAction); ok {
|
||||||
|
switch strings.ToLower(fa.GetFormat()) {
|
||||||
|
case "form":
|
||||||
|
var f http.Form
|
||||||
|
cast.Convert(&f, payload)
|
||||||
|
payload = f
|
||||||
|
case "multipart":
|
||||||
|
var m http.Multipart
|
||||||
|
cast.Convert(&m, payload)
|
||||||
|
payload = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 发起底层 HTTP 调用
|
||||||
timeout := cast.Duration(actionConfig["timeout"])
|
timeout := cast.Duration(actionConfig["timeout"])
|
||||||
client := http.NewClient(timeout)
|
client := http.NewClient(timeout)
|
||||||
|
|
||||||
res := client.Do(httpReq.Method, httpReq.Url, httpReq.Payload, headerSlice(httpReq)...)
|
res := client.Do(httpReq.Method, httpReq.Url, payload, headerSlice(httpReq)...)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
return nil, res.Error
|
return nil, res.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. 解析响应
|
// 9. 解析响应
|
||||||
var response T
|
var response T
|
||||||
if err := res.To(&response); err != nil {
|
if err := res.To(&response); err != nil {
|
||||||
var temp any
|
var temp any
|
||||||
@ -105,6 +121,51 @@ func Call[T any](action Action) (*T, error) {
|
|||||||
return &response, nil
|
return &response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CallBy 通过动作名称和动态 Payload 发起调用
|
||||||
|
func CallBy[T any](name string, payload any) (*T, error) {
|
||||||
|
tmpl, ok := actionRegistry[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("action not found: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var action Action
|
||||||
|
if t, ok := tmpl.(reflect.Type); ok {
|
||||||
|
// Go 结构体模式
|
||||||
|
inst := reflect.New(t).Interface()
|
||||||
|
if payload != nil {
|
||||||
|
cast.Convert(inst, payload)
|
||||||
|
}
|
||||||
|
action = inst.(Action)
|
||||||
|
} else if ga, ok := tmpl.(*GenericAction); ok {
|
||||||
|
// JSON 定义模式 (深拷贝模板以防并发冲突)
|
||||||
|
newGA := &GenericAction{
|
||||||
|
name: ga.name,
|
||||||
|
url: ga.url,
|
||||||
|
method: ga.method,
|
||||||
|
signer: ga.signer,
|
||||||
|
format: ga.format,
|
||||||
|
payload: make(map[string]any),
|
||||||
|
}
|
||||||
|
// 复制模板 Payload
|
||||||
|
for k, v := range ga.payload {
|
||||||
|
newGA.payload[k] = v
|
||||||
|
}
|
||||||
|
// 合并动态 Payload
|
||||||
|
if payload != nil {
|
||||||
|
if m, ok := payload.(map[string]any); ok {
|
||||||
|
for k, v := range m {
|
||||||
|
newGA.payload[k] = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cast.Convert(&newGA.payload, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action = newGA
|
||||||
|
}
|
||||||
|
|
||||||
|
return Call[T](action)
|
||||||
|
}
|
||||||
|
|
||||||
func preprocessSecrets(m map[string]any, opened *[]*safe.SecretPlaintext) {
|
func preprocessSecrets(m map[string]any, opened *[]*safe.SecretPlaintext) {
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
if sb, ok := v.(*safe.SafeBuf); ok {
|
if sb, ok := v.(*safe.SafeBuf); ok {
|
||||||
|
|||||||
@ -83,7 +83,7 @@ func TestBuiltinSigners(t *testing.T) {
|
|||||||
if err := signer.Sign(req, config); err != nil {
|
if err := signer.Sign(req, config); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
expected := "Basic " + encoding.Base64ToString([]byte("admin:123"))
|
expected := "Basic " + encoding.Base64([]byte("admin:123"))
|
||||||
if req.GetHeader("Authorization") != expected {
|
if req.GetHeader("Authorization") != expected {
|
||||||
t.Errorf("expected %s, got %s", expected, req.GetHeader("Authorization"))
|
t.Errorf("expected %s, got %s", expected, req.GetHeader("Authorization"))
|
||||||
}
|
}
|
||||||
|
|||||||
80
config.go
80
config.go
@ -30,6 +30,27 @@ func SetEncryptKeys(key, iv []byte) {
|
|||||||
crypto.SetDefaultAES(key, iv)
|
crypto.SetDefaultAES(key, iv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetConfig 允许在内存中动态添加或覆盖配置,适用于从数据库或知识库加载配置的场景。
|
||||||
|
// 它会自动扫描 map 中的字符串,识别并解密以 "**" 开头的加密内容并转换为 SafeBuf。
|
||||||
|
func SetConfig(name string, conf map[string]any) {
|
||||||
|
configMutex.Lock()
|
||||||
|
defer configMutex.Unlock()
|
||||||
|
|
||||||
|
// 1. 扫描并自动解密敏感数据
|
||||||
|
decryptMapWithPrefix(conf)
|
||||||
|
// 注意:解密出的 SafeBufs 是常驻内存的配置,由 GlobalConfigs 持有,不需要立即关闭
|
||||||
|
|
||||||
|
// 2. 合并到全局树
|
||||||
|
if GlobalConfigs[name] == nil {
|
||||||
|
GlobalConfigs[name] = conf
|
||||||
|
} else if dst, ok := GlobalConfigs[name].(map[string]any); ok {
|
||||||
|
MergeMap(dst, conf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddConfig 是 SetConfig 的兼容性别名
|
||||||
|
func AddConfig(name string, conf map[string]any) { SetConfig(name, conf) }
|
||||||
|
|
||||||
// Load 加载指定的配置文件并合并到 GlobalConfigs
|
// Load 加载指定的配置文件并合并到 GlobalConfigs
|
||||||
func Load(name string) error {
|
func Load(name string) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
@ -41,11 +62,7 @@ func Load(name string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
configMutex.Lock()
|
SetConfig(name, conf)
|
||||||
defer configMutex.Unlock()
|
|
||||||
|
|
||||||
// 合并到全局树
|
|
||||||
MergeMap(GlobalConfigs, conf)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +77,8 @@ func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
|
|||||||
// 1. 获取 api 根节点
|
// 1. 获取 api 根节点
|
||||||
curr, ok := GlobalConfigs["api"].(map[string]any)
|
curr, ok := GlobalConfigs["api"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return res, nil
|
// 尝试直接从根开始找
|
||||||
|
curr = GlobalConfigs
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 逐级导航并合并
|
// 2. 逐级导航并合并
|
||||||
@ -81,6 +99,7 @@ func GetActionConfig(actionName string) (map[string]any, []*safe.SafeBuf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 这里的解密主要是为了处理文件中加载的 ENC() 格式 (兼容旧版)
|
||||||
safeBufs := decryptMap(res)
|
safeBufs := decryptMap(res)
|
||||||
return res, safeBufs
|
return res, safeBufs
|
||||||
}
|
}
|
||||||
@ -91,6 +110,19 @@ func fill(action any, actionConfig map[string]any) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ga, ok := action.(*GenericAction); ok {
|
||||||
|
// 对于通用动作,将配置合并到 payload map 中(仅合并 payload 不存在的 key)
|
||||||
|
for k, v := range actionConfig {
|
||||||
|
if k == "url" || k == "method" || k == "signer" || k == "format" || k == "headers" || k == "timeout" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := ga.payload[k]; !exists {
|
||||||
|
ga.payload[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(action)
|
v := reflect.ValueOf(action)
|
||||||
for v.Kind() == reflect.Ptr {
|
for v.Kind() == reflect.Ptr {
|
||||||
v = v.Elem()
|
v = v.Elem()
|
||||||
@ -179,14 +211,46 @@ func MergeMap(dst, src map[string]any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decryptMapWithPrefix(m map[string]any) []*safe.SafeBuf {
|
||||||
|
var safeBufs []*safe.SafeBuf
|
||||||
|
for k, v := range m {
|
||||||
|
if s, ok := v.(string); ok && strings.HasPrefix(s, "**") {
|
||||||
|
raw := s[2:]
|
||||||
|
var b64 []byte
|
||||||
|
var err error
|
||||||
|
if b64, err = encoding.UnURLBase64(raw); err != nil {
|
||||||
|
b64, err = encoding.UnBase64(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && len(b64) > 0 {
|
||||||
|
if dec, err := confAES.DecryptBytes(b64); err == nil {
|
||||||
|
sb := safe.NewSafeBufAndErase(dec)
|
||||||
|
m[k] = sb
|
||||||
|
safeBufs = append(safeBufs, sb)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if subMap, ok := v.(map[string]any); ok {
|
||||||
|
safeBufs = append(safeBufs, decryptMapWithPrefix(subMap)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return safeBufs
|
||||||
|
}
|
||||||
|
|
||||||
func decryptMap(m map[string]any) []*safe.SafeBuf {
|
func decryptMap(m map[string]any) []*safe.SafeBuf {
|
||||||
var safeBufs []*safe.SafeBuf
|
var safeBufs []*safe.SafeBuf
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
if s, ok := v.(string); ok {
|
if s, ok := v.(string); ok {
|
||||||
var b64 []byte
|
var b64 []byte
|
||||||
var err error
|
var err error
|
||||||
if b64, err = encoding.UnUrlBase64FromString(s); err != nil {
|
// 兼容旧版 ENC(...) 格式扫描
|
||||||
b64, err = encoding.UnBase64FromString(s)
|
inner := s
|
||||||
|
if strings.HasPrefix(s, "ENC(") && strings.HasSuffix(s, ")") {
|
||||||
|
inner = s[4 : len(s)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if b64, err = encoding.UnURLBase64(inner); err != nil {
|
||||||
|
b64, err = encoding.UnBase64(inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && len(b64) > 0 {
|
if err == nil && len(b64) > 0 {
|
||||||
|
|||||||
23
go.mod
23
go.mod
@ -3,20 +3,21 @@ module apigo.cc/go/api
|
|||||||
go 1.25.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/cast v1.3.2
|
apigo.cc/go/cast v1.5.0
|
||||||
apigo.cc/go/config v1.3.0
|
apigo.cc/go/config v1.5.0
|
||||||
apigo.cc/go/crypto v1.3.0
|
apigo.cc/go/crypto v1.5.0
|
||||||
apigo.cc/go/encoding v1.3.0
|
apigo.cc/go/encoding v1.5.0
|
||||||
apigo.cc/go/http v1.3.0
|
apigo.cc/go/http v1.5.0
|
||||||
apigo.cc/go/safe v1.3.0
|
apigo.cc/go/jsmod v1.5.0
|
||||||
|
apigo.cc/go/safe v1.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
apigo.cc/go/file v1.3.0 // indirect
|
apigo.cc/go/file v1.5.0 // indirect
|
||||||
apigo.cc/go/id v1.3.0 // indirect
|
apigo.cc/go/id v1.5.0 // indirect
|
||||||
apigo.cc/go/log v1.3.0 // indirect
|
apigo.cc/go/log v1.5.0 // indirect
|
||||||
apigo.cc/go/rand v1.3.0 // indirect
|
apigo.cc/go/rand v1.5.0 // indirect
|
||||||
apigo.cc/go/shell v1.3.0 // indirect
|
apigo.cc/go/shell v1.5.0 // indirect
|
||||||
golang.org/x/crypto v0.51.0 // indirect
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.54.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
|||||||
46
go.sum
46
go.sum
@ -1,25 +1,27 @@
|
|||||||
apigo.cc/go/cast v1.3.2 h1:hh9MWDSwh3T/kQdCHjFpjDwHrh2A05Q4wt1AAWs8NBI=
|
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
||||||
apigo.cc/go/cast v1.3.2/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
||||||
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
|
apigo.cc/go/config v1.5.0 h1:Yuz9QEb11XXG4XkhDi/ueT2M1T3Q9PElE5tiakvjehs=
|
||||||
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
|
apigo.cc/go/config v1.5.0/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM=
|
||||||
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
|
apigo.cc/go/crypto v1.5.0 h1:Nxz7a6VKCdvaF258IU0NkjQyureOLxfR308Sy2iftUI=
|
||||||
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
|
apigo.cc/go/crypto v1.5.0/go.mod h1:F9M6nXv+5328r1ZwbTvI6fcr8VdgqHVzALOcsdv6ntE=
|
||||||
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
|
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
|
||||||
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
|
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
|
||||||
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
|
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw=
|
||||||
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
|
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0=
|
||||||
apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg=
|
apigo.cc/go/http v1.5.0 h1:GGIu0dhMjTiYygxH9NWOzz6AY+WZjfyTL1qZ8G9vI1U=
|
||||||
apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U=
|
apigo.cc/go/http v1.5.0/go.mod h1:CIIH7HS6wdicLpSgkEVozdDcHlM9W9ygmmzJvzhAKWg=
|
||||||
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
|
apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8=
|
||||||
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
|
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA=
|
||||||
apigo.cc/go/log v1.3.0 h1:61Z80WGN6SnhgxgoR8xuVYIieMdjlJKmf8JX1HXzp0Y=
|
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
||||||
apigo.cc/go/log v1.3.0/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
|
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||||
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
|
apigo.cc/go/log v1.5.0 h1:kQuLLtbt33mEuc/xJVcy8NODXkso/QKSZWNclKrSpsI=
|
||||||
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
apigo.cc/go/log v1.5.0/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
|
||||||
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
|
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
|
||||||
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
|
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
|
||||||
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
|
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
|
||||||
apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4=
|
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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
|||||||
27
js_export.go
Normal file
27
js_export.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"apigo.cc/go/jsmod"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
jsmod.Register("api", map[string]any{
|
||||||
|
"Call": call,
|
||||||
|
"SetConfig": SetConfig,
|
||||||
|
"RegisterAction": RegisterAction,
|
||||||
|
"RegisterSigner": registerSigner,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// call 提供给 JS 的私有入口
|
||||||
|
func call(ctx context.Context, name string, payload any) (any, error) {
|
||||||
|
// 将 ctx 传入以透传追踪信息给 JS 签名器
|
||||||
|
return CallBy[any](name, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerSigner 允许从 JS 注册动态签名逻辑
|
||||||
|
func registerSigner(name string, code string) {
|
||||||
|
RegisterSigner(name, &jsSigner{code: code})
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ func TestSafeConfigDecryption(t *testing.T) {
|
|||||||
|
|
||||||
plaintext := "my-secret-password"
|
plaintext := "my-secret-password"
|
||||||
ciphertext, _ := confAES.EncryptBytes([]byte(plaintext))
|
ciphertext, _ := confAES.EncryptBytes([]byte(plaintext))
|
||||||
b64 := encoding.Base64ToString(ciphertext)
|
b64 := encoding.Base64(ciphertext)
|
||||||
|
|
||||||
GlobalConfigs = map[string]any{
|
GlobalConfigs = map[string]any{
|
||||||
"api": map[string]any{
|
"api": map[string]any{
|
||||||
@ -51,7 +51,7 @@ func TestSafeConfigDecryption(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedAuth := "Basic " + encoding.Base64ToString([]byte("admin:"+plaintext))
|
expectedAuth := "Basic " + encoding.Base64([]byte("admin:"+plaintext))
|
||||||
if req.GetHeader("Authorization") != expectedAuth {
|
if req.GetHeader("Authorization") != expectedAuth {
|
||||||
t.Errorf("expected %s, got %s", expectedAuth, req.GetHeader("Authorization"))
|
t.Errorf("expected %s, got %s", expectedAuth, req.GetHeader("Authorization"))
|
||||||
}
|
}
|
||||||
|
|||||||
53
signer.go
53
signer.go
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"apigo.cc/go/safe"
|
"apigo.cc/go/safe"
|
||||||
@ -16,6 +17,14 @@ var signers = map[string]Signer{
|
|||||||
"bearer": &bearerSigner{},
|
"bearer": &bearerSigner{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var jsRunner func(ctx context.Context, code string, args map[string]any) (map[string]any, error)
|
||||||
|
|
||||||
|
// SetJSRunner 设置 JS 执行器钩子,由 go/js 引擎在启动时注入。
|
||||||
|
// 这避免了 api 模块直接依赖 go/js 产生的循环引用。
|
||||||
|
func SetJSRunner(runner func(context.Context, string, map[string]any) (map[string]any, error)) {
|
||||||
|
jsRunner = runner
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterSigner 注册全局签名器
|
// RegisterSigner 注册全局签名器
|
||||||
func RegisterSigner(name string, s Signer) {
|
func RegisterSigner(name string, s Signer) {
|
||||||
signers[name] = s
|
signers[name] = s
|
||||||
@ -26,6 +35,50 @@ func GetSigner(name string) Signer {
|
|||||||
return signers[name]
|
return signers[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsSigner 包装 JS 代码为 Go Signer 接口
|
||||||
|
type jsSigner struct {
|
||||||
|
code string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *jsSigner) Sign(req *HttpRequest, config map[string]any) error {
|
||||||
|
if jsRunner == nil {
|
||||||
|
return errors.New("jsRunner is not set, cannot execute js signer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备传递给 JS 的参数
|
||||||
|
args := map[string]any{
|
||||||
|
"method": req.Method,
|
||||||
|
"url": req.Url,
|
||||||
|
"payload": req.Payload,
|
||||||
|
"headers": req.headers,
|
||||||
|
"config": config,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行 JS 签名逻辑
|
||||||
|
// 注意:这里默认使用 Background context,除非底层能透传,低代码环境中会由 js_export 注入正确的 ctx
|
||||||
|
res, err := jsRunner(context.Background(), s.code, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将结果(通常是修改后的 headers)写回请求对象
|
||||||
|
if newHeaders, ok := res["headers"].(map[string]any); ok {
|
||||||
|
for k, v := range newHeaders {
|
||||||
|
req.SetHeader(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 JS 返回了新的 URL 或 Method,也支持修改
|
||||||
|
if newUrl, ok := res["url"].(string); ok {
|
||||||
|
req.Url = newUrl
|
||||||
|
}
|
||||||
|
if newMethod, ok := res["method"].(string); ok {
|
||||||
|
req.Method = newMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 快速应用签名
|
// 快速应用签名
|
||||||
func sign(name string, req *HttpRequest, config map[string]any) error {
|
func sign(name string, req *HttpRequest, config map[string]any) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user