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