重构项目
This commit is contained in:
4
Makefile
4
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
|
||||
|
||||
29
cmd/local/main.go
Normal file
29
cmd/local/main.go
Normal file
@@ -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() {
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type PageVFS struct {
|
||||
commitID string
|
||||
}
|
||||
|
||||
// todo: 限制最大文件加载大小
|
||||
func NewPageVFS(
|
||||
client *http.Client,
|
||||
backend Backend,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package filters
|
||||
package quickjs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
var FilterInstQuickJS core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
|
||||
var param struct {
|
||||
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("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))
|
||||
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, " "))
|
||||
return ctx.NewNull()
|
||||
}
|
||||
}
|
||||
message := fmt.Sprintf("[%s] %s", level, strings.Join(messages, " "))
|
||||
|
||||
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")))
|
||||
// 总是输出到系统日志
|
||||
log.Print(message)
|
||||
|
||||
// 添加 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()
|
||||
// 如果有缓冲区,也写入缓冲区
|
||||
if buffer != nil {
|
||||
buffer.WriteString(message + "\n")
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user