From 043b00bbb79b3c6c37aeca9e8e6dc5fcc64e405a Mon Sep 17 00:00:00 2001 From: dragon Date: Wed, 19 Nov 2025 15:56:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=20js=20websocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/js/.pages.yaml | 8 - examples/js/json.js | 4 - examples/{event => js_event}/.pages.yaml | 0 examples/{event => js_event}/index.html | 0 examples/{event => js_event}/index.js | 0 examples/js_hello_world/.pages.yaml | 9 + examples/{js => js_hello_world}/index.html | 3 +- examples/{js => js_hello_world}/index.js | 0 examples/js_hello_world/json.js | 4 + examples/{kv => js_kv}/.pages.yaml | 0 examples/{kv => js_kv}/index.html | 0 examples/{kv => js_kv}/index.js | 0 examples/js_ws/.pages.yaml | 4 + examples/js_ws/index.html | 378 +++++++++++++++++++++ examples/js_ws/index.js | 18 + go.mod | 1 + go.sum | 2 + pkg/filters/common.go | 2 +- pkg/filters/goja/goja.go | 66 +++- pkg/filters/goja/var_debug.go | 11 + pkg/filters/goja/var_kv.go | 57 ++-- pkg/filters/goja/var_response.go | 20 +- pkg/filters/goja/var_websocket.go | 65 ++++ pkg/providers/cache.go | 18 +- pkg/utils/resp.go | 16 +- 25 files changed, 617 insertions(+), 69 deletions(-) delete mode 100644 examples/js/.pages.yaml delete mode 100644 examples/js/json.js rename examples/{event => js_event}/.pages.yaml (100%) rename examples/{event => js_event}/index.html (100%) rename examples/{event => js_event}/index.js (100%) create mode 100644 examples/js_hello_world/.pages.yaml rename examples/{js => js_hello_world}/index.html (69%) rename examples/{js => js_hello_world}/index.js (100%) create mode 100644 examples/js_hello_world/json.js rename examples/{kv => js_kv}/.pages.yaml (100%) rename examples/{kv => js_kv}/index.html (100%) rename examples/{kv => js_kv}/index.js (100%) create mode 100644 examples/js_ws/.pages.yaml create mode 100644 examples/js_ws/index.html create mode 100644 examples/js_ws/index.js create mode 100644 pkg/filters/goja/var_websocket.go diff --git a/examples/js/.pages.yaml b/examples/js/.pages.yaml deleted file mode 100644 index d2c0ea4..0000000 --- a/examples/js/.pages.yaml +++ /dev/null @@ -1,8 +0,0 @@ -routes: -- path: "api/dev/**" - js: - exec: "index.js" - debug: true -- path: "api/prod/**" - js: - exec: "json.js" \ No newline at end of file diff --git a/examples/js/json.js b/examples/js/json.js deleted file mode 100644 index 329d90f..0000000 --- a/examples/js/json.js +++ /dev/null @@ -1,4 +0,0 @@ -resp.setHeader("content-type", "application/json"); -resp.write(JSON.stringify({ - 'method': req.method, -})) diff --git a/examples/event/.pages.yaml b/examples/js_event/.pages.yaml similarity index 100% rename from examples/event/.pages.yaml rename to examples/js_event/.pages.yaml diff --git a/examples/event/index.html b/examples/js_event/index.html similarity index 100% rename from examples/event/index.html rename to examples/js_event/index.html diff --git a/examples/event/index.js b/examples/js_event/index.js similarity index 100% rename from examples/event/index.js rename to examples/js_event/index.js diff --git a/examples/js_hello_world/.pages.yaml b/examples/js_hello_world/.pages.yaml new file mode 100644 index 0000000..3dacba9 --- /dev/null +++ b/examples/js_hello_world/.pages.yaml @@ -0,0 +1,9 @@ +routes: +- path: "hello" + js: + exec: "index.js" + debug: true +- path: "json" + js: + exec: "json.js" + debug: true \ No newline at end of file diff --git a/examples/js/index.html b/examples/js_hello_world/index.html similarity index 69% rename from examples/js/index.html rename to examples/js_hello_world/index.html index b77ec73..d2bd60c 100644 --- a/examples/js/index.html +++ b/examples/js_hello_world/index.html @@ -8,6 +8,7 @@ JS 概念验证 - +Hello World(Debug) | +Json (Debug) \ No newline at end of file diff --git a/examples/js/index.js b/examples/js_hello_world/index.js similarity index 100% rename from examples/js/index.js rename to examples/js_hello_world/index.js diff --git a/examples/js_hello_world/json.js b/examples/js_hello_world/json.js new file mode 100644 index 0000000..6dbb215 --- /dev/null +++ b/examples/js_hello_world/json.js @@ -0,0 +1,4 @@ +response.setHeader("content-type", "application/json"); +response.write(JSON.stringify({ + 'method': request.method, +})) diff --git a/examples/kv/.pages.yaml b/examples/js_kv/.pages.yaml similarity index 100% rename from examples/kv/.pages.yaml rename to examples/js_kv/.pages.yaml diff --git a/examples/kv/index.html b/examples/js_kv/index.html similarity index 100% rename from examples/kv/index.html rename to examples/js_kv/index.html diff --git a/examples/kv/index.js b/examples/js_kv/index.js similarity index 100% rename from examples/kv/index.js rename to examples/js_kv/index.js diff --git a/examples/js_ws/.pages.yaml b/examples/js_ws/.pages.yaml new file mode 100644 index 0000000..b09e854 --- /dev/null +++ b/examples/js_ws/.pages.yaml @@ -0,0 +1,4 @@ +routes: + - path: "ws" + js: + exec: "index.js" \ No newline at end of file diff --git a/examples/js_ws/index.html b/examples/js_ws/index.html new file mode 100644 index 0000000..a3c090b --- /dev/null +++ b/examples/js_ws/index.html @@ -0,0 +1,378 @@ + + + + + + WebSocket聊天客户端 + + + +
+
+
+

WebSocket聊天客户端

+
+
+ 未连接 +
+
+ +
+ +
+
+ 💬 +

连接服务器开始聊天

+
+
+ +
+ + +
+
+ + + + \ No newline at end of file diff --git a/examples/js_ws/index.js b/examples/js_ws/index.js new file mode 100644 index 0000000..afea883 --- /dev/null +++ b/examples/js_ws/index.js @@ -0,0 +1,18 @@ +let ws = websocket(); +let shouldExit = false; +while (!shouldExit) { + let data = ws.readText(); + switch (data) { + case "exit": + shouldExit = true; + break; + case "panic": + throw Error("错误"); + case "date": + ws.writeText(new Date().toJSON()) + break + default: + ws.writeText("收到信息:" + data) + break; + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index c95eccd..b171daf 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/klauspost/compress v1.18.1 // indirect diff --git a/go.sum b/go.sum index f8ef24c..0d33ea5 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,8 @@ github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLx github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= diff --git a/pkg/filters/common.go b/pkg/filters/common.go index b9e5d6d..a560efb 100644 --- a/pkg/filters/common.go +++ b/pkg/filters/common.go @@ -27,7 +27,7 @@ func DefaultFilters(config map[string]map[string]any) (map[string]core.FilterIns if !ok { item = make(map[string]any) } - if item["_disable"] == true { + if it, ok := item["Enabled"]; ok && it == false { zap.L().Debug("skip filter", zap.String("key", key)) continue } diff --git a/pkg/filters/goja/goja.go b/pkg/filters/goja/goja.go index 2793ab2..454ba57 100644 --- a/pkg/filters/goja/goja.go +++ b/pkg/filters/goja/goja.go @@ -3,8 +3,10 @@ package goja import ( "context" "errors" + "io" "net/http" "path/filepath" + "sync" "time" "github.com/dop251/goja" @@ -15,7 +17,20 @@ import ( "gopkg.d7z.net/gitea-pages/pkg/core" ) -func FilterInstGoJa(_ core.Params) (core.FilterInstance, error) { +func FilterInstGoJa(gl core.Params) (core.FilterInstance, error) { + var global struct { + Timeout time.Duration `json:"timeout"` + EnableDebug bool `json:"debug"` + EnableWebsocket bool `json:"websocket"` + } + global.EnableDebug = true + global.EnableWebsocket = true + if err := gl.Unmarshal(&global); err != nil { + return nil, err + } + if global.Timeout == 0 { + global.Timeout = 60 * time.Second + } return func(config core.Params) (core.FilterCall, error) { var param struct { Exec string `json:"exec"` @@ -36,19 +51,21 @@ func FilterInstGoJa(_ core.Params) (core.FilterInstance, error) { if err != nil { return err } - debug := NewDebug(param.Debug && request.URL.Query().Get("debug") == "true", request, w) + debug := NewDebug(global.EnableDebug && param.Debug && request.URL.Query().Get("debug") == "true", request, w) registry := newRegistry(ctx) registry.RegisterNativeModule(console.ModuleName, console.RequireWithPrinter(debug)) loop := eventloop.NewEventLoop(eventloop.WithRegistry(registry), eventloop.EnableConsole(true)) stop := make(chan struct{}, 1) shutdown := make(chan struct{}, 1) - timeout, cancelFunc := context.WithTimeout(ctx, 3*time.Second) + defer close(shutdown) + timeout, cancelFunc := context.WithTimeout(ctx, global.Timeout) defer cancelFunc() count := 0 + closers := NewClosers() + defer closers.Close() go func() { defer func() { shutdown <- struct{}{} - close(shutdown) }() select { case <-timeout.Done(): @@ -67,6 +84,14 @@ func FilterInstGoJa(_ core.Params) (core.FilterInstance, error) { if err = KVInject(ctx, vm); err != nil { panic(err) } + if global.EnableWebsocket { + var closer io.Closer + closer, err = WebsocketInject(vm, debug, request, cancelFunc) + if err != nil { + panic(err) + } + closers.AddCloser(closer.Close) + } _, err = vm.RunProgram(prg) }) stop <- struct{}{} @@ -90,3 +115,36 @@ func newRegistry(ctx core.FilterContext) *require.Registry { })) return registry } + +type Closers struct { + mu sync.Mutex + closers []func() error +} + +func NewClosers() *Closers { + return &Closers{ + closers: make([]func() error, 0), + } +} + +func (c *Closers) AddCloser(closer func() error) { + c.mu.Lock() + c.closers = append(c.closers, closer) + c.mu.Unlock() +} + +func (c *Closers) Close() error { + c.mu.Lock() + defer c.mu.Unlock() + var errs []error + for i := len(c.closers) - 1; i >= 0; i-- { + if err := c.closers[i](); err != nil { + errs = append(errs, err) + } + } + c.closers = nil + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} diff --git a/pkg/filters/goja/var_debug.go b/pkg/filters/goja/var_debug.go index 323f9d0..d69254f 100644 --- a/pkg/filters/goja/var_debug.go +++ b/pkg/filters/goja/var_debug.go @@ -1,11 +1,15 @@ package goja import ( + "bufio" "bytes" _ "embed" "html/template" + "net" "net/http" "time" + + "github.com/pkg/errors" ) //go:embed debug.tmpl @@ -89,6 +93,13 @@ func (d *DebugData) Write(i []byte) (int, error) { return d.parent.Write(i) } +func (d *DebugData) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hijacker, ok := d.parent.(http.Hijacker); ok { + return hijacker.Hijack() + } + return nil, nil, errors.New("not hijackable") +} + func (d *DebugData) WriteHeader(statusCode int) { if d.enabled { d.status = statusCode diff --git a/pkg/filters/goja/var_kv.go b/pkg/filters/goja/var_kv.go index 10609ee..372f8da 100644 --- a/pkg/filters/goja/var_kv.go +++ b/pkg/filters/goja/var_kv.go @@ -12,61 +12,46 @@ import ( func KVInject(ctx core.FilterContext, jsCtx *goja.Runtime) error { return jsCtx.GlobalObject().Set("kv", map[string]interface{}{ - "repo": func(group string) goja.Value { + "repo": func(group string) (goja.Value, error) { return kvResult(ctx.RepoDB)(ctx, jsCtx, group) }, - "org": func(group string) goja.Value { + "org": func(group string) (goja.Value, error) { return kvResult(ctx.OrgDB)(ctx, jsCtx, group) }, }) } -func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runtime, group string) goja.Value { - return func(ctx core.FilterContext, jsCtx *goja.Runtime, group string) goja.Value { +func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runtime, group string) (goja.Value, error) { + return func(ctx core.FilterContext, jsCtx *goja.Runtime, group string) (goja.Value, error) { group = strings.TrimSpace(group) if group == "" { - panic("kv: invalid group name") + return goja.Undefined(), errors.New("invalid group") } db := db.Child(group).(kv.CursorPagedKV) return jsCtx.ToValue(map[string]interface{}{ - "get": func(key string) goja.Value { + "get": func(key string) (goja.Value, error) { get, err := db.Get(ctx, key) if err != nil { if !errors.Is(err, os.ErrNotExist) { - panic(err) + return nil, err } - return goja.Null() + return goja.Null(), nil } - return jsCtx.ToValue(get) + return jsCtx.ToValue(get), nil }, - "set": func(key, value string) { - err := db.Put(ctx, key, value, kv.TTLKeep) - if err != nil { - panic(err) - } + "set": func(key, value string) error { + return db.Put(ctx, key, value, kv.TTLKeep) }, - "delete": func(key string) bool { - b, err := db.Delete(ctx, key) - if err != nil { - panic(err) - } - return b + "delete": func(key string) (bool, error) { + return db.Delete(ctx, key) }, - "putIfNotExists": func(key, value string) bool { - exists, err := db.PutIfNotExists(ctx, key, value, kv.TTLKeep) - if err != nil { - panic(err) - } - return exists + "putIfNotExists": func(key, value string) (bool, error) { + return db.PutIfNotExists(ctx, key, value, kv.TTLKeep) }, - "compareAndSwap": func(key, oldValue, newValue string) bool { - swap, err := db.CompareAndSwap(ctx, key, oldValue, newValue) - if err != nil { - panic(err) - } - return swap + "compareAndSwap": func(key, oldValue, newValue string) (bool, error) { + return db.CompareAndSwap(ctx, key, oldValue, newValue) }, - "list": func(limit int64, cursor string) map[string]any { + "list": func(limit int64, cursor string) (map[string]any, error) { if limit <= 0 { limit = 100 } @@ -75,14 +60,14 @@ func kvResult(db kv.CursorPagedKV) func(ctx core.FilterContext, jsCtx *goja.Runt Cursor: cursor, }) if err != nil { - panic(err) + return nil, err } return map[string]any{ "keys": list.Keys, "cursor": list.Cursor, "hasNext": list.HasMore, - } + }, nil }, - }) + }), nil } } diff --git a/pkg/filters/goja/var_response.go b/pkg/filters/goja/var_response.go index 8cc53f9..17340a4 100644 --- a/pkg/filters/goja/var_response.go +++ b/pkg/filters/goja/var_response.go @@ -18,7 +18,6 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R "getHeader": func(key string) string { return writer.Header().Get(key) }, - "removeHeader": func(key string) { writer.Header().Del(key) }, @@ -38,11 +37,12 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R }, // 写入响应 - "write": func(data string) { + "write": func(data string) error { _, err := writer.Write([]byte(data)) if err != nil { - panic(err) + return err } + return nil }, "writeHead": func(statusCode int, headers ...map[string]string) { @@ -55,14 +55,14 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R writer.WriteHeader(statusCode) }, - "end": func(data ...string) { + "end": func(data ...string) error { if len(data) > 0 { _, err := writer.Write([]byte(data[0])) if err != nil { - panic(err) + return err } } - // 在实际的 HTTP 处理中,我们通常不手动结束响应 + return nil }, // 重定向 @@ -75,7 +75,7 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R }, // JSON 响应 - "json": func(data goja.Value) { + "json": func(data goja.Value) error { writer.Header().Set("Content-Type", "application/json") var jsonStr string @@ -86,14 +86,12 @@ func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.R default: marshal, err := json.Marshal(v) if err != nil { - panic(err) + return err } jsonStr = string(marshal) } _, err := writer.Write([]byte(jsonStr)) - if err != nil { - panic(err) - } + return err }, // 设置 cookie diff --git a/pkg/filters/goja/var_websocket.go b/pkg/filters/goja/var_websocket.go new file mode 100644 index 0000000..93a098d --- /dev/null +++ b/pkg/filters/goja/var_websocket.go @@ -0,0 +1,65 @@ +package goja + +import ( + "context" + "io" + "net/http" + + "github.com/dop251/goja" + "github.com/gorilla/websocket" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func WebsocketInject(jsCtx *goja.Runtime, w http.ResponseWriter, request *http.Request, cancelFunc context.CancelFunc) (io.Closer, error) { + closers := NewClosers() + return closers, jsCtx.GlobalObject().Set("websocket", func() (any, error) { + upgrader := websocket.Upgrader{} + conn, err := upgrader.Upgrade(w, request, nil) + if err != nil { + return nil, err + } + cancelFunc() + zap.L().Debug("websocket upgrader created") + closers.AddCloser(conn.Close) + return map[string]interface{}{ + "TypeTextMessage": websocket.TextMessage, + "TypeBinaryMessage": websocket.BinaryMessage, + "readText": func() (string, error) { + _, p, err := conn.ReadMessage() + if err != nil { + return "", err + } + return string(p), nil + }, + "read": func() (any, error) { + messageType, p, err := conn.ReadMessage() + if err != nil { + return nil, err + } + return map[string]interface{}{ + "type": messageType, + "data": p, + }, nil + }, + "writeText": func(data string) error { + return conn.WriteMessage(websocket.TextMessage, []byte(data)) + }, + "write": func(mType int, data any) error { + if item, ok := data.(goja.Value); ok { + data = item.Export() + } + var dataRaw []byte + switch it := data.(type) { + case []byte: + dataRaw = it + case string: + dataRaw = []byte(it) + default: + return errors.Errorf("invalid type for websocket text: %T", data) + } + return conn.WriteMessage(mType, dataRaw) + }, + }, nil + }) +} diff --git a/pkg/providers/cache.go b/pkg/providers/cache.go index f8e909e..068cf4e 100644 --- a/pkg/providers/cache.go +++ b/pkg/providers/cache.go @@ -102,7 +102,7 @@ func (c *ProviderCache) Open(ctx context.Context, owner, repo, commit, path stri return nil, os.ErrNotExist } else if lastCache != nil { h := lastCache.Metadata - if h["Not-Found"] == "true" { + if h["_404_"] == "true" { return nil, os.ErrNotExist } respHeader := make(http.Header) @@ -130,11 +130,23 @@ func (c *ProviderCache) Open(ctx context.Context, owner, repo, commit, path stri if open != nil { _ = open.Body.Close() } + // 当上游返回错误时,缓存404结果 + if errors.Is(err, os.ErrNotExist) { + if err = c.cacheBlob.Put(ctx, key, map[string]string{ + "_404_": "true", + }, bytes.NewBuffer(nil), time.Hour); err != nil { + zap.L().Warn("缓存404失败", zap.Error(err)) + } + } return nil, err } if open.StatusCode == http.StatusNotFound { - // TODO: 缓存 404 路由 - //_ = c.cache.Put(ctx, key, nil, time.Hour) + // 缓存404路由 + if err = c.cacheBlob.Put(ctx, key, map[string]string{ + "_404_": "true", + }, bytes.NewBuffer(nil), time.Hour); err != nil { + zap.L().Warn("缓存404失败", zap.Error(err)) + } _ = open.Body.Close() return nil, os.ErrNotExist } diff --git a/pkg/utils/resp.go b/pkg/utils/resp.go index e941241..3f6f840 100644 --- a/pkg/utils/resp.go +++ b/pkg/utils/resp.go @@ -1,6 +1,12 @@ package utils -import "net/http" +import ( + "bufio" + "net" + "net/http" + + "github.com/pkg/errors" +) type WrittenResponseWriter struct { write bool @@ -18,6 +24,14 @@ func (w *WrittenResponseWriter) Header() http.Header { return w.base.Header() } +func (w *WrittenResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + w.write = true + if hijacker, ok := w.base.(http.Hijacker); ok { + return hijacker.Hijack() + } + return nil, nil, errors.New("not hijackable") +} + func (w *WrittenResponseWriter) Write(b []byte) (int, error) { w.write = true return w.base.Write(b)