|
|
|
|
@@ -1,7 +1,8 @@
|
|
|
|
|
package filters
|
|
|
|
|
package quickjs
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
@@ -15,7 +16,8 @@ import (
|
|
|
|
|
|
|
|
|
|
var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
|
|
|
|
|
var param struct {
|
|
|
|
|
Exec string `json:"exec"`
|
|
|
|
|
Exec string `json:"exec"`
|
|
|
|
|
Debug bool `json:"debug"`
|
|
|
|
|
}
|
|
|
|
|
if err := config.Unmarshal(¶m); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
@@ -29,37 +31,168 @@ var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var rt = quickjs.NewRuntime()
|
|
|
|
|
rt := quickjs.NewRuntime()
|
|
|
|
|
rt.SetExecuteTimeout(5)
|
|
|
|
|
defer rt.Close()
|
|
|
|
|
|
|
|
|
|
jsCtx := rt.NewContext()
|
|
|
|
|
defer jsCtx.Close()
|
|
|
|
|
|
|
|
|
|
// 在 debug 模式下,我们需要拦截输出
|
|
|
|
|
var (
|
|
|
|
|
outputBuffer strings.Builder
|
|
|
|
|
logBuffer strings.Builder
|
|
|
|
|
jsError error
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
global := jsCtx.Globals()
|
|
|
|
|
global.Set("request", createRequestObject(jsCtx, request))
|
|
|
|
|
global.Set("response", createResponseObject(jsCtx, writer, request))
|
|
|
|
|
global.Set("console", createConsoleObject(jsCtx))
|
|
|
|
|
global.Set("request", createRequestObject(jsCtx, request, metadata))
|
|
|
|
|
|
|
|
|
|
// 根据是否 debug 模式创建不同的 response 对象
|
|
|
|
|
if param.Debug {
|
|
|
|
|
// debug 模式下使用虚假的 writer 来捕获输出
|
|
|
|
|
global.Set("response", createResponseObject(jsCtx, &debugResponseWriter{
|
|
|
|
|
buffer: &outputBuffer,
|
|
|
|
|
header: make(http.Header),
|
|
|
|
|
}, request))
|
|
|
|
|
global.Set("console", createConsoleObject(jsCtx, &logBuffer))
|
|
|
|
|
} else {
|
|
|
|
|
global.Set("response", createResponseObject(jsCtx, writer, request))
|
|
|
|
|
global.Set("console", createConsoleObject(jsCtx, nil))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret := jsCtx.Eval(js)
|
|
|
|
|
defer ret.Free()
|
|
|
|
|
jsCtx.Loop()
|
|
|
|
|
|
|
|
|
|
if ret.IsException() {
|
|
|
|
|
err := jsCtx.Exception()
|
|
|
|
|
return err
|
|
|
|
|
jsError = err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
|
|
// 如果在 debug 模式下,返回 HTML 调试页面
|
|
|
|
|
if param.Debug {
|
|
|
|
|
return renderDebugPage(writer, &outputBuffer, &logBuffer, jsError)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return jsError
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createRequestObject 创建表示 HTTP 请求的 JavaScript 对象
|
|
|
|
|
func createRequestObject(ctx *quickjs.Context, req *http.Request) *quickjs.Value {
|
|
|
|
|
obj := ctx.NewObject()
|
|
|
|
|
// debugResponseWriter 用于在 debug 模式下捕获响应输出
|
|
|
|
|
type debugResponseWriter struct {
|
|
|
|
|
buffer *strings.Builder
|
|
|
|
|
header http.Header
|
|
|
|
|
status int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *debugResponseWriter) Header() http.Header {
|
|
|
|
|
return w.header
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *debugResponseWriter) Write(data []byte) (int, error) {
|
|
|
|
|
return w.buffer.Write(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *debugResponseWriter) WriteHeader(statusCode int) {
|
|
|
|
|
w.status = statusCode
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// renderDebugPage 渲染调试页面
|
|
|
|
|
func renderDebugPage(writer http.ResponseWriter, outputBuffer, logBuffer *strings.Builder, jsError error) error {
|
|
|
|
|
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
|
|
|
|
|
html := `<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>QuickJS Debug</title>
|
|
|
|
|
<style>
|
|
|
|
|
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
|
|
|
|
|
.section { margin-bottom: 30px; border: 1px solid #ddd; border-radius: 5px; }
|
|
|
|
|
.section-header { background: #f5f5f5; padding: 10px 15px; border-bottom: 1px solid #ddd; font-weight: bold; }
|
|
|
|
|
.section-content { padding: 15px; background: white; }
|
|
|
|
|
.output { white-space: pre-wrap; font-family: monospace; }
|
|
|
|
|
.log { white-space: pre-wrap; font-family: monospace; background: #f8f8f8; }
|
|
|
|
|
.error { color: #d00; background: #fee; padding: 10px; border-radius: 3px; }
|
|
|
|
|
.success { color: #080; background: #efe; padding: 10px; border-radius: 3px; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1>QuickJS Debug Output</h1>
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
<div class="section-header">执行结果</div>
|
|
|
|
|
<div class="section-content">
|
|
|
|
|
<div class="output">`
|
|
|
|
|
|
|
|
|
|
// 转义输出内容
|
|
|
|
|
output := outputBuffer.String()
|
|
|
|
|
if output == "" {
|
|
|
|
|
output = "(无输出)"
|
|
|
|
|
}
|
|
|
|
|
html += htmlEscape(output)
|
|
|
|
|
|
|
|
|
|
html += `</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
<div class="section-header">控制台日志</div>
|
|
|
|
|
<div class="section-content">
|
|
|
|
|
<div class="log">`
|
|
|
|
|
|
|
|
|
|
// 转义日志内容
|
|
|
|
|
logs := logBuffer.String()
|
|
|
|
|
if logs == "" {
|
|
|
|
|
logs = "(无日志)"
|
|
|
|
|
}
|
|
|
|
|
html += htmlEscape(logs)
|
|
|
|
|
|
|
|
|
|
html += `</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="section">
|
|
|
|
|
<div class="section-header">执行状态</div>
|
|
|
|
|
<div class="section-content">`
|
|
|
|
|
|
|
|
|
|
if jsError != nil {
|
|
|
|
|
html += `<div class="error"><strong>错误:</strong> ` + htmlEscape(jsError.Error()) + `</div>`
|
|
|
|
|
} else {
|
|
|
|
|
html += `<div class="success">执行成功</div>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html += `</div>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>`
|
|
|
|
|
|
|
|
|
|
_, err := writer.Write([]byte(html))
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// htmlEscape 转义 HTML 特殊字符
|
|
|
|
|
func htmlEscape(s string) string {
|
|
|
|
|
return strings.NewReplacer(
|
|
|
|
|
"&", "&",
|
|
|
|
|
"<", "<",
|
|
|
|
|
">", ">",
|
|
|
|
|
`"`, """,
|
|
|
|
|
"'", "'",
|
|
|
|
|
).Replace(s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createRequestObject 创建表示 HTTP 请求的 JavaScript 对象
|
|
|
|
|
func createRequestObject(ctx *quickjs.Context, req *http.Request, metadata *core.PageContent) *quickjs.Value {
|
|
|
|
|
obj := ctx.NewObject()
|
|
|
|
|
// 基本属性
|
|
|
|
|
obj.Set("method", ctx.NewString(req.Method))
|
|
|
|
|
obj.Set("url", ctx.NewString(req.URL.String()))
|
|
|
|
|
obj.Set("path", ctx.NewString(req.URL.Path))
|
|
|
|
|
obj.Set("query", ctx.NewString(req.URL.RawQuery))
|
|
|
|
|
url := *req.URL
|
|
|
|
|
url.Path = metadata.Path
|
|
|
|
|
obj.Set("url", ctx.NewString(url.String()))
|
|
|
|
|
obj.Set("path", ctx.NewString(url.Path))
|
|
|
|
|
obj.Set("rawPath", ctx.NewString(req.URL.Path))
|
|
|
|
|
obj.Set("query", ctx.NewString(url.RawQuery))
|
|
|
|
|
obj.Set("host", ctx.NewString(req.Host))
|
|
|
|
|
obj.Set("remoteAddr", ctx.NewString(req.RemoteAddr))
|
|
|
|
|
obj.Set("proto", ctx.NewString(req.Proto))
|
|
|
|
|
@@ -67,7 +200,7 @@ func createRequestObject(ctx *quickjs.Context, req *http.Request) *quickjs.Value
|
|
|
|
|
|
|
|
|
|
// 解析查询参数
|
|
|
|
|
queryObj := ctx.NewObject()
|
|
|
|
|
for key, values := range req.URL.Query() {
|
|
|
|
|
for key, values := range url.Query() {
|
|
|
|
|
if len(values) > 0 {
|
|
|
|
|
queryObj.Set(key, ctx.NewString(values[0]))
|
|
|
|
|
}
|
|
|
|
|
@@ -169,20 +302,20 @@ func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req
|
|
|
|
|
}
|
|
|
|
|
return ctx.NewNull()
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
obj.Set("writeHead", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
|
|
|
|
|
if len(args) >= 1 {
|
|
|
|
|
statusCode := int(args[0].ToInt32())
|
|
|
|
|
|
|
|
|
|
// 处理可选的 headers 参数
|
|
|
|
|
if len(args) >= 2 && args[1].IsObject() {
|
|
|
|
|
headersObj := args[1]
|
|
|
|
|
headersObj.Properties().ForEach(func(key string, value *quickjs.Value) bool {
|
|
|
|
|
writer.Header().Set(key, value.String())
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
names, err := headersObj.PropertyNames()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ctx.NewError(err)
|
|
|
|
|
}
|
|
|
|
|
for _, key := range names {
|
|
|
|
|
writer.Header().Set(key, headersObj.Get(key).String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
writer.WriteHeader(statusCode)
|
|
|
|
|
}
|
|
|
|
|
return ctx.NewNull()
|
|
|
|
|
@@ -261,11 +394,11 @@ func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if secure := options.Get("secure"); !secure.IsNull() {
|
|
|
|
|
cookie.Secure = secure.Bool()
|
|
|
|
|
cookie.Secure = secure.ToBool()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if httpOnly := options.Get("httpOnly"); !httpOnly.IsNull() {
|
|
|
|
|
cookie.HttpOnly = httpOnly.Bool()
|
|
|
|
|
cookie.HttpOnly = httpOnly.ToBool()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -278,48 +411,32 @@ func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// createConsoleObject 创建 console 对象用于日志输出
|
|
|
|
|
func createConsoleObject(ctx *quickjs.Context) *quickjs.Value {
|
|
|
|
|
func createConsoleObject(ctx *quickjs.Context, buf *strings.Builder) *quickjs.Value {
|
|
|
|
|
console := ctx.NewObject()
|
|
|
|
|
|
|
|
|
|
logFunc := func(level string) func(*quickjs.Context, *quickjs.Value, []*quickjs.Value) *quickjs.Value {
|
|
|
|
|
logFunc := func(level string, buffer *strings.Builder) func(*quickjs.Context, *quickjs.Value, []*quickjs.Value) *quickjs.Value {
|
|
|
|
|
return func(q *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
|
|
|
|
|
var messages []string
|
|
|
|
|
for _, arg := range args {
|
|
|
|
|
messages = append(messages, arg.String())
|
|
|
|
|
}
|
|
|
|
|
log.Printf("[" + level + "] " + strings.Join(messages, " "))
|
|
|
|
|
message := fmt.Sprintf("[%s] %s", level, strings.Join(messages, " "))
|
|
|
|
|
|
|
|
|
|
// 总是输出到系统日志
|
|
|
|
|
log.Print(message)
|
|
|
|
|
|
|
|
|
|
// 如果有缓冲区,也写入缓冲区
|
|
|
|
|
if buffer != nil {
|
|
|
|
|
buffer.WriteString(message + "\n")
|
|
|
|
|
}
|
|
|
|
|
return ctx.NewNull()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.Set("log", ctx.NewFunction(logFunc("INFO")))
|
|
|
|
|
console.Set("info", ctx.NewFunction(logFunc("INFO")))
|
|
|
|
|
console.Set("warn", ctx.NewFunction(logFunc("WARN")))
|
|
|
|
|
console.Set("error", ctx.NewFunction(logFunc("ERROR")))
|
|
|
|
|
console.Set("debug", ctx.NewFunction(logFunc("DEBUG")))
|
|
|
|
|
|
|
|
|
|
// 添加 time 和 timeEnd 方法用于性能测量
|
|
|
|
|
timers := make(map[string]time.Time)
|
|
|
|
|
|
|
|
|
|
console.Set("time", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
label := args[0].String()
|
|
|
|
|
timers[label] = time.Now()
|
|
|
|
|
}
|
|
|
|
|
return ctx.NewNull()
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
console.Set("timeEnd", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
label := args[0].String()
|
|
|
|
|
if start, exists := timers[label]; exists {
|
|
|
|
|
elapsed := time.Since(start)
|
|
|
|
|
log.Printf("[TIMER] %s: %v", label, elapsed)
|
|
|
|
|
delete(timers, label)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ctx.NewNull()
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
console.Set("log", ctx.NewFunction(logFunc("INFO", buf)))
|
|
|
|
|
console.Set("info", ctx.NewFunction(logFunc("INFO", buf)))
|
|
|
|
|
console.Set("warn", ctx.NewFunction(logFunc("WARN", buf)))
|
|
|
|
|
console.Set("error", ctx.NewFunction(logFunc("ERROR", buf)))
|
|
|
|
|
console.Set("debug", ctx.NewFunction(logFunc("DEBUG", buf)))
|
|
|
|
|
return console
|
|
|
|
|
}
|