diff --git a/Makefile b/Makefile
index f008b9d..1e742d3 100644
--- a/Makefile
+++ b/Makefile
@@ -21,9 +21,9 @@ dist/gitea-pages-$(GOOS)-$(GOARCH).tar.gz: $(shell find . -type f -name "*.go"
@echo Compile $@ via $(GO_DIST_NAME) && \
mkdir -p dist && \
rm -f dist/$(GO_DIST_NAME) && \
- GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/$(GO_DIST_NAME) . && \
+ GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/$(GO_DIST_NAME) cmd/server && \
cd dist && \
- tar zcf gitea-pages-$(GOOS)-$(GOARCH).tar.gz $(GO_DIST_NAME) ../LICENSE ../config.yaml ../errors.html.tmpl ../README.md ../README_*.md && \
+ tar zcf gitea-pages-$(GOOS)-$(GOARCH).tar.gz $(GO_DIST_NAME) ../LICENSE ../config.yaml ../cmd/server/errors.html.tmpl ../README.md ../README_*.md && \
rm -f $(GO_DIST_NAME)
gitea-pages: $(shell find . -type f -name "*.go" ) go.mod go.sum
diff --git a/cmd/local/main.go b/cmd/local/main.go
new file mode 100644
index 0000000..68e8c76
--- /dev/null
+++ b/cmd/local/main.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "flag"
+ "os"
+)
+
+var (
+ org = "pub"
+ domain = "fbi.com"
+ repo = org + "." + domain
+ path = ""
+
+ port = 8080
+)
+
+func init() {
+ dir, _ := os.Getwd()
+ path = dir
+ flag.StringVar(&org, "org", org, "org")
+ flag.StringVar(&repo, "repo", repo, "repo")
+ flag.StringVar(&domain, "domain", domain, "domain")
+ flag.StringVar(&path, "path", path, "path")
+ flag.IntVar(&port, "port", port, "port")
+ flag.Parse()
+}
+
+func main() {
+}
diff --git a/config.go b/cmd/server/config.go
similarity index 100%
rename from config.go
rename to cmd/server/config.go
diff --git a/errors.html.tmpl b/cmd/server/errors.html.tmpl
similarity index 100%
rename from errors.html.tmpl
rename to cmd/server/errors.html.tmpl
diff --git a/main.go b/cmd/server/main.go
similarity index 100%
rename from main.go
rename to cmd/server/main.go
diff --git a/pkg/core/vfs.go b/pkg/core/vfs.go
index 9b25f6b..f5821c5 100644
--- a/pkg/core/vfs.go
+++ b/pkg/core/vfs.go
@@ -16,6 +16,7 @@ type PageVFS struct {
commitID string
}
+// todo: 限制最大文件加载大小
func NewPageVFS(
client *http.Client,
backend Backend,
diff --git a/pkg/filters/common.go b/pkg/filters/common.go
index 051b3ba..22c4e91 100644
--- a/pkg/filters/common.go
+++ b/pkg/filters/common.go
@@ -1,6 +1,9 @@
package filters
-import "gopkg.d7z.net/gitea-pages/pkg/core"
+import (
+ "gopkg.d7z.net/gitea-pages/pkg/core"
+ "gopkg.d7z.net/gitea-pages/pkg/filters/quickjs"
+)
func DefaultFilters() map[string]core.FilterInstance {
return map[string]core.FilterInstance{
@@ -11,6 +14,6 @@ func DefaultFilters() map[string]core.FilterInstance {
"_404_": FilterInstDefaultNotFound,
"failback": FilterInstFailback,
"template": FilterInstTemplate,
- "qjs": FilterInstQuickJS,
+ "qjs": quickjs.FilterInstQuickJS,
}
}
diff --git a/pkg/filters/quickjs.go b/pkg/filters/quickjs/quickjs.go
similarity index 58%
rename from pkg/filters/quickjs.go
rename to pkg/filters/quickjs/quickjs.go
index 6bcf0a0..ae432f5 100644
--- a/pkg/filters/quickjs.go
+++ b/pkg/filters/quickjs/quickjs.go
@@ -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 := `
+
+
+ QuickJS Debug
+
+
+
+ QuickJS Debug Output
+
+
+
+
+
`
+
+ // 转义输出内容
+ output := outputBuffer.String()
+ if output == "" {
+ output = "(无输出)"
+ }
+ html += htmlEscape(output)
+
+ html += `
+
+
+
+
+
+
+
`
+
+ // 转义日志内容
+ logs := logBuffer.String()
+ if logs == "" {
+ logs = "(无日志)"
+ }
+ html += htmlEscape(logs)
+
+ html += `
+
+
+
+
+
+
`
+
+ if jsError != nil {
+ html += `
错误: ` + htmlEscape(jsError.Error()) + `
`
+ } else {
+ html += `
执行成功
`
+ }
+
+ 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
}
diff --git a/pkg/filters/redirect.go b/pkg/filters/redirect.go
index 2d77110..83e1a47 100644
--- a/pkg/filters/redirect.go
+++ b/pkg/filters/redirect.go
@@ -9,6 +9,7 @@ import (
"slices"
"strings"
+ "github.com/pkg/errors"
"go.uber.org/zap"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
@@ -24,7 +25,7 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor
return nil, err
}
if len(param.Targets) == 0 {
- return nil, fmt.Errorf("no targets")
+ return nil, errors.New("no targets")
}
if param.Code == 0 {
param.Code = http.StatusFound
@@ -49,8 +50,7 @@ var FilterInstRedirect core.FilterInstance = func(config core.FilterParams) (cor
http.Redirect(writer, request, target.String(), param.Code)
return nil
- } else {
- return next(ctx, writer, request, metadata)
}
+ return next(ctx, writer, request, metadata)
}, nil
}
diff --git a/tests/core/test.go b/tests/core/test.go
index 0977c43..fca1bcd 100644
--- a/tests/core/test.go
+++ b/tests/core/test.go
@@ -73,8 +73,12 @@ func (t *TestServer) AddFile(path, data string, args ...interface{}) {
}
func (t *TestServer) OpenFile(url string) ([]byte, *http.Response, error) {
+ return t.OpenRequest(http.MethodGet, url, nil)
+}
+
+func (t *TestServer) OpenRequest(method, url string, body io.Reader) ([]byte, *http.Response, error) {
recorder := httptest.NewRecorder()
- t.server.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, url, nil))
+ t.server.ServeHTTP(recorder, httptest.NewRequest(method, url, body))
response := recorder.Result()
if response.Body != nil {
defer response.Body.Close()
diff --git a/tests/filter_proxy_test.go b/tests/filter_proxy_test.go
index aa6d21b..ddde6ef 100644
--- a/tests/filter_proxy_test.go
+++ b/tests/filter_proxy_test.go
@@ -7,7 +7,8 @@ import (
"gopkg.d7z.net/gitea-pages/tests/core"
)
-func test_proxy(t *testing.T) {
+func TestProxy(t *testing.T) {
+ t.Skip()
server := core.NewDefaultTestServer()
hs := core.NewServer()
defer server.Close()
@@ -40,7 +41,8 @@ proxy:
assert.Equal(t, 404, resp.StatusCode)
}
-func test_cname_proxy(t *testing.T) {
+func TestCnameProxy(t *testing.T) {
+ t.Skip()
server := core.NewDefaultTestServer()
hs := core.NewServer()
defer server.Close()
diff --git a/tests/filter_qjs_test.go b/tests/filter_qjs_test.go
index b9cf0f3..3a77655 100644
--- a/tests/filter_qjs_test.go
+++ b/tests/filter_qjs_test.go
@@ -1,6 +1,7 @@
package tests
import (
+ "net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -15,7 +16,7 @@ func Test_JS(t *testing.T) {
function get(a,b) {
return a + b;
}
-request.Write()
+response.write('512 + 512 = ' + get(512,512))
`)
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
routes:
@@ -30,5 +31,28 @@ routes:
data, _, err = server.OpenFile("https://org1.example.com/repo1/api/v1/get")
assert.NoError(t, err)
assert.Equal(t, "512 + 512 = 1024", string(data))
-
+}
+
+func Test_JS_Request(t *testing.T) {
+ server := core.NewDefaultTestServer()
+ defer server.Close()
+ server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
+ server.AddFile("org1/repo1/gh-pages/index.js", `response.write(request.method+' /'+request.path)`)
+ server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
+routes:
+- path: "api/v1/**"
+ qjs:
+ exec: "index.js"
+`)
+ data, _, err := server.OpenFile("https://org1.example.com/repo1/")
+ assert.NoError(t, err)
+ assert.Equal(t, "hello world", string(data))
+
+ data, _, err = server.OpenFile("https://org1.example.com/repo1/api/v1/fetch")
+ assert.NoError(t, err)
+ assert.Equal(t, "GET /api/v1/fetch", string(data))
+
+ data, _, err = server.OpenRequest(http.MethodPost, "https://org1.example.com/repo1/api/v1/fetch", nil)
+ assert.NoError(t, err)
+ assert.Equal(t, "POST /api/v1/fetch", string(data))
}