切换为 goja

This commit is contained in:
dragon
2025-11-17 17:33:40 +08:00
parent b20207de4c
commit 7778e95194
29 changed files with 634 additions and 575 deletions

View File

@@ -4,9 +4,11 @@
**This project is part of Dragon's Zone HomeLab**
This project focuses on functional implementation and does not consider any performance optimizations or large-scale deployment scenarios. Any issues arising from this are not related to the project.
This project focuses on functional implementation and does not consider any performance optimizations or large-scale
deployment scenarios. Any issues arising from this are not related to the project.
**Note**: The project recently added custom renderers and reverse proxy functionality, which may lead to serious security and performance issues. If not needed, it can be turned off in the settings.
**Note**: The project recently added custom renderers and reverse proxy functionality, which may lead to serious
security and performance issues. If not needed, it can be turned off in the settings.
## Get Started
@@ -29,15 +31,13 @@ For specific configurations, check [`config.yaml`](./config.yaml).
Create `.pages.yaml` in the project's `gh-pages` branch and fill in the following content:
```yaml
v-route: true # Virtual routing
alias: # CNAME
- "example.com"
- "example2.com"
templates: # Renderer
gotemplate: '**/*.tmpl,**/index.html'
proxy:
/api: https://github.com/api
ignore: .git/**,.pages.yaml
routes:
- path: "**"
js:
exec: index.js
```
## TODO

View File

@@ -29,15 +29,13 @@ make gitea-pages
在项目的 `gh-pages` 分支创建 `.pages.yaml`,填入如下内容
```yaml
v-route: true # 虚拟路由
alias: # CNAME
- "example.com"
- "example2.com"
templates: # 渲染器
gotemplate: '**/*.tmpl,**/index.html'
proxy:
/api: https://github.com/api
ignore: .git/**,.pages.yaml
routes:
- path: "**"
js:
exec: index.js
```
## TODO

View File

@@ -5,7 +5,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>qjs 验证</title>
<title>js 验证</title>
</head>
<body>
<p>Hello World</p>

View File

@@ -1,8 +1,8 @@
routes:
- path: "api/dev/**"
qjs:
js:
exec: "index.js"
debug: true
- path: "api/prod/**"
qjs:
js:
exec: "json.js"

View File

@@ -5,7 +5,7 @@
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Quickjs 概念验证</title>
<title>JS 概念验证</title>
</head>
<body>

View File

@@ -1,7 +1,7 @@
response.write("hello world")
console.log("hello world")
console.log(req.methaaod)
function testError(){
throw Error("Method not implemented")
}
response.setHeader("content-type", "application/json")
testError()

10
examples/kv/.pages.yaml Normal file
View File

@@ -0,0 +1,10 @@
routes:
- path: "get"
js:
exec: "index.js"
debug: true
- path: "put"
js:
exec: "index.js"
debug: true

14
examples/kv/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JS 概念验证</title>
</head>
<body>
<a href="/get">查询数据</a>
<a href="/put">更新数据</a>
</body>
</html>

10
examples/kv/index.js Normal file
View File

@@ -0,0 +1,10 @@
var db = kv.repo("self");
if(request.path == "put"){
data = db.get('key')
if(data == undefined){
db.set('key','0')
}else {
db.set('key',(parseInt(data)+1).toString())
}
}
response.write("当前存储的数值为 " + db.get('key'))

28
go.mod
View File

@@ -5,9 +5,13 @@ go 1.25.3
require (
code.gitea.io/sdk/gitea v0.22.1
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
github.com/buke/quickjs-go v0.6.6
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7
github.com/dop251/goja_nodejs v0.0.0-20251015164255-5e94316bedaf
github.com/go-task/slim-sprig/v3 v3.0.0
github.com/gobwas/glob v0.2.3
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.27.0
@@ -17,16 +21,13 @@ require (
require (
github.com/42wim/httpsig v1.2.3 // indirect
github.com/buke/quickjs-go v0.6.6 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 // indirect
github.com/dop251/goja_nodejs v0.0.0-20251015164255-5e94316bedaf // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
@@ -34,10 +35,9 @@ require (
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
@@ -49,16 +49,16 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.5.0 // indirect
go.etcd.io/etcd/api/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/v3 v3.6.5 // indirect
go.etcd.io/etcd/api/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.6 // indirect
go.etcd.io/etcd/client/v3 v3.6.6 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251110190251-83f479183930 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)

21
go.sum
View File

@@ -21,6 +21,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 h1:jxmXU5V9tXxJnydU5v/m9SG8TRUa/Z7IXODBpMs/P+U=
github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dop251/goja_nodejs v0.0.0-20251015164255-5e94316bedaf h1:gbmvliZnCut4NjaPSNOQlfqBoZ9C5Dpf72mHMMYhgVE=
@@ -55,6 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE=
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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
@@ -114,10 +118,16 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA=
go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ=
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8=
go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk=
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U=
go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo=
go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
@@ -142,6 +152,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -151,6 +163,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -163,10 +177,13 @@ golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -181,10 +198,14 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101/go.mod h1:E17fc4PDhkr22dE3RgnH2hEubUaky6ZwW4VhANxyspg=
google.golang.org/genproto/googleapis/api v0.0.0-20251110190251-83f479183930 h1:8BWFtrvJRbplrKV5VHlIm4YM726eeBPPAL2QDNWhRrU=
google.golang.org/genproto/googleapis/api v0.0.0-20251110190251-83f479183930/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930 h1:tK4fkUnnRhig9TsTp4otV1FxwBFYgbKUq1RY0V6KZ4U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251110190251-83f479183930/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=

View File

@@ -2,7 +2,7 @@ package filters
import (
"gopkg.d7z.net/gitea-pages/pkg/core"
"gopkg.d7z.net/gitea-pages/pkg/filters/quickjs"
"gopkg.d7z.net/gitea-pages/pkg/filters/goja"
)
func DefaultFilters() map[string]core.FilterInstance {
@@ -14,6 +14,6 @@ func DefaultFilters() map[string]core.FilterInstance {
"_404_": FilterInstDefaultNotFound,
"failback": FilterInstFailback,
"template": FilterInstTemplate,
"qjs": quickjs.FilterInstQuickJS,
"js": goja.FilterInstGoJa,
}
}

View File

@@ -26,7 +26,8 @@ var FilterInstDefaultNotFound core.FilterInstance = func(config core.FilterParam
}
writer.WriteHeader(http.StatusNotFound)
_, _ = io.Copy(writer, open.Body)
return nil
}
return nil
return err
}, nil
}

View File

@@ -0,0 +1,95 @@
<!doctype html>
<html lang="zh">
<head>
<title>调试控制台</title>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<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>Debug Output</h1>
<div class="section">
<div class="section-header">执行结果</div>
<div class="section-content">
<pre class="output"><code>{{ .Output }}</code></pre>
</div>
</div>
<div class="section">
<div class="section-header">控制台日志</div>
<div class="section-content">
<div class="log">{{- range .Logs }}<p>{{ .Time.Format "2006/01/02 15:04:05" }} - {{ .Level }} : {{ .Message }}</p>{{- end }}</div>
</div>
</div>
<div class="section">
<div class="section-header">响应头</div>
<div class="section-content">
<div class="log">{{- range $key, $value := .Headers }}<p>{{ $key }} = {{ $value }}</p>{{- end }}</div>
</div>
</div>
<div class="section">
<div class="section-header">执行状态</div>
<div class="section-content">
{{- if .Error }}
<div class="error">{{ .Error }}</div>
{{- else }}
<div class="success">执行成功</div>
{{- end }}
</div>
</div>
</body>
</html>

View File

@@ -1,17 +1,20 @@
package goja
import (
"context"
"net/http"
"path/filepath"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/require"
"github.com/dop251/goja_nodejs/url"
"github.com/pkg/errors"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
var FilterInstGoja core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
var FilterInstGoJa core.FilterInstance = func(config core.FilterParams) (core.FilterCall, error) {
var param struct {
Exec string `json:"exec"`
Debug bool `json:"debug"`
@@ -22,18 +25,39 @@ var FilterInstGoja core.FilterInstance = func(config core.FilterParams) (core.Fi
if param.Exec == "" {
return nil, errors.Errorf("no exec specified")
}
return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error {
return func(ctx core.FilterContext, w http.ResponseWriter, request *http.Request, next core.NextCall) error {
js, err := ctx.ReadString(ctx, param.Exec)
if err != nil {
return err
}
newRegistry(ctx)
debug := NewDebug(param.Debug && request.URL.Query().Get("debug") == "true", request, w)
registry := newRegistry(ctx)
registry.RegisterNativeModule(console.ModuleName, console.RequireWithPrinter(debug))
vm := goja.New()
_ = registry.Enable(vm)
console.Enable(vm)
url.Enable(vm)
vm.GlobalObject().Set("")
return nil
if err = RequestInject(ctx, vm, request); err != nil {
return err
}
if err = ResponseInject(vm, debug, request); err != nil {
return err
}
if err = KVInject(ctx, vm); err != nil {
return err
}
coreCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
go func() {
select {
case <-coreCtx.Done():
vm.Interrupt(coreCtx.Err())
return
}
}()
_, err = vm.RunScript(param.Exec, js)
cancel()
return debug.Flush(err)
}, nil
}

View File

@@ -0,0 +1,118 @@
package goja
import (
"bytes"
_ "embed"
"html/template"
"net/http"
"time"
)
//go:embed debug.tmpl
var errorPageStr string
var errorPage = template.Must(template.New("error.tmpl").Parse(errorPageStr))
type DebugData struct {
enabled bool
status int
headers http.Header
body *bytes.Buffer
logs []debugDataLog
request *http.Request
parent http.ResponseWriter
}
func NewDebug(debug bool, request *http.Request, writer http.ResponseWriter) *DebugData {
return &DebugData{
enabled: debug,
status: http.StatusOK,
headers: make(http.Header),
body: new(bytes.Buffer),
logs: []debugDataLog{},
parent: writer,
request: request,
}
}
type debugDataLog struct {
Level string
Time time.Time
Message string
}
func (d *DebugData) Log(msg string) {
if !d.enabled {
return
}
d.logs = append(d.logs, debugDataLog{
Level: "log",
Time: time.Now(),
Message: msg,
})
}
func (d *DebugData) Warn(msg string) {
if !d.enabled {
return
}
d.logs = append(d.logs, debugDataLog{
Level: "warn",
Time: time.Now(),
Message: msg,
})
}
func (d *DebugData) Error(msg string) {
if !d.enabled {
return
}
d.logs = append(d.logs, debugDataLog{
Level: "error",
Time: time.Now(),
Message: msg,
})
}
func (d *DebugData) Header() http.Header {
if d.enabled {
return d.headers
}
return d.parent.Header()
}
func (d *DebugData) Write(i []byte) (int, error) {
if d.enabled {
return d.body.Write(i)
}
return d.parent.Write(i)
}
func (d *DebugData) WriteHeader(statusCode int) {
if d.enabled {
d.status = statusCode
} else {
d.parent.WriteHeader(statusCode)
}
}
func (d *DebugData) Flush(err error) error {
if !d.enabled {
return err
}
data := map[string]interface{}{
"Logs": d.logs,
"Output": d.body.String(),
"Headers": d.Header(),
}
d.body.Reset()
if err != nil {
d.status = http.StatusInternalServerError
data["Error"] = err.Error()
}
d.parent.Header().Set("Content-Type", "text/html; charset=utf-8")
d.parent.WriteHeader(d.status)
_ = errorPage.Execute(d.parent, data)
return nil
}

View File

@@ -0,0 +1,57 @@
package goja
import (
"os"
"strings"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gopkg.d7z.net/gitea-pages/pkg/core"
"gopkg.d7z.net/middleware/kv"
)
func KVInject(ctx core.FilterContext, jsCtx *goja.Runtime) error {
return jsCtx.GlobalObject().Set("kv", map[string]interface{}{
"repo": func(group string) goja.Value {
return kvResult(ctx.RepoDB)(ctx, jsCtx, group)
},
"org": func(group string) goja.Value {
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 {
group = strings.TrimSpace(group)
if group == "" {
panic("kv: invalid group name")
}
db := db.Child(group)
return jsCtx.ToValue(map[string]interface{}{
"get": func(key string) goja.Value {
get, err := db.Get(ctx, key)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
panic(err)
}
return goja.Null()
}
return jsCtx.ToValue(get)
},
"set": func(key string, value string) {
err := db.Put(ctx, key, value, kv.TTLKeep)
if err != nil {
panic(err)
}
},
"delete": func(key string) bool {
b, err := db.Delete(ctx, key)
if err != nil {
panic(err)
}
return b
},
})
}
}

View File

@@ -1 +1,77 @@
package goja
import (
"io"
"net/http"
"strings"
"github.com/dop251/goja"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
func RequestInject(ctx core.FilterContext, jsCtx *goja.Runtime, req *http.Request) error {
url := *req.URL
url.Path = ctx.Path
// 预计算头信息以提高性能
headers := make(map[string]string)
headerNames := make([]string, 0, len(req.Header))
rawHeaderNames := make([]string, 0, len(req.Header))
for key, values := range req.Header {
if len(values) > 0 {
headers[key] = values[0]
}
headerNames = append(headerNames, strings.ToLower(key))
rawHeaderNames = append(rawHeaderNames, key)
}
queryParams := make(map[string]string)
for key, values := range url.Query() {
if len(values) > 0 {
queryParams[key] = values[0]
}
}
return jsCtx.GlobalObject().Set("request", map[string]any{
"method": req.Method,
"url": url,
"rawPath": req.URL.Path,
"host": req.Host,
"remoteAddr": req.RemoteAddr,
"proto": req.Proto,
"httpVersion": req.Proto,
"path": url.Path,
"query": queryParams,
"headers": headers,
"get": func(key string) any {
get := req.Header.Get(key)
if get != "" {
return get
}
return nil
},
"getHeader": func(name string) string {
return req.Header.Get(name)
},
"getHeaderNames": func() []string {
return headerNames
},
"getHeaders": func() map[string]string {
return headers
},
"getRawHeaderNames": func() []string {
return rawHeaderNames
},
"hasHeader": func(name string) bool {
return req.Header.Get(name) != ""
},
"readBody": func() []byte {
body, err := io.ReadAll(req.Body)
if err != nil {
panic(err)
}
return body
},
"protocol": req.Proto,
})
}

View File

@@ -0,0 +1,149 @@
package goja
import (
"encoding/json"
"net/http"
"time"
"github.com/dop251/goja"
)
func ResponseInject(jsCtx *goja.Runtime, writer http.ResponseWriter, req *http.Request) error {
return jsCtx.GlobalObject().Set("response", map[string]any{
// 响应头操作
"setHeader": func(key string, value string) {
writer.Header().Set(key, value)
},
"getHeader": func(key string) string {
return writer.Header().Get(key)
},
"removeHeader": func(key string) {
writer.Header().Del(key)
},
"hasHeader": func(key string) bool {
_, exists := writer.Header()[key]
return exists
},
// 状态码操作
"setStatus": func(statusCode int) {
writer.WriteHeader(statusCode)
},
"statusCode": func(statusCode int) {
writer.WriteHeader(statusCode)
},
// 写入响应
"write": func(data string) {
_, err := writer.Write([]byte(data))
if err != nil {
panic(err)
}
},
"writeHead": func(statusCode int, headers ...map[string]string) {
// 处理可选的 headers 参数
if len(headers) > 0 {
for key, value := range headers[0] {
writer.Header().Set(key, value)
}
}
writer.WriteHeader(statusCode)
},
"end": func(data ...string) {
if len(data) > 0 {
_, err := writer.Write([]byte(data[0]))
if err != nil {
panic(err)
}
}
// 在实际的 HTTP 处理中,我们通常不手动结束响应
},
// 重定向
"redirect": func(location string, statusCode ...int) {
code := 302
if len(statusCode) > 0 {
code = statusCode[0]
}
http.Redirect(writer, req, location, code)
},
// JSON 响应
"json": func(data goja.Value) {
writer.Header().Set("Content-Type", "application/json")
var jsonStr string
export := data.Export()
switch v := export.(type) {
case string:
jsonStr = v
default:
marshal, err := json.Marshal(v)
if err != nil {
panic(err)
}
jsonStr = string(marshal)
}
_, err := writer.Write([]byte(jsonStr))
if err != nil {
panic(err)
}
},
// 设置 cookie
"setCookie": func(name string, value string, options ...map[string]interface{}) {
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
}
// 处理可选参数
if len(options) > 0 && options[0] != nil {
opts := options[0]
if maxAge, ok := opts["maxAge"].(int); ok {
cookie.MaxAge = maxAge
}
if expires, ok := opts["expires"].(int64); ok {
cookie.Expires = time.Unix(expires, 0)
}
if path, ok := opts["path"].(string); ok {
cookie.Path = path
}
if domain, ok := opts["domain"].(string); ok {
cookie.Domain = domain
}
if secure, ok := opts["secure"].(bool); ok {
cookie.Secure = secure
}
if httpOnly, ok := opts["httpOnly"].(bool); ok {
cookie.HttpOnly = httpOnly
}
if sameSite, ok := opts["sameSite"].(string); ok {
switch sameSite {
case "lax":
cookie.SameSite = http.SameSiteLaxMode
case "strict":
cookie.SameSite = http.SameSiteStrictMode
case "none":
cookie.SameSite = http.SameSiteNoneMode
}
}
}
http.SetCookie(writer, cookie)
},
})
}

View File

@@ -1,135 +0,0 @@
package quickjs
import (
"errors"
"net/http"
"strings"
"time"
"github.com/buke/quickjs-go"
)
type DebugData struct {
Status int `json:"status"`
Header http.Header `json:"header"`
Body string `json:"body"`
Logs []DebugDataLog `json:"logs"`
}
type DebugDataLog struct {
Level string `json:"level"`
Time time.Time `json:"time"`
Message string `json:"message"`
}
// 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"><pre><code><strong>Message:</strong></br>`
var q *quickjs.Error
if errors.As(jsError, &q) {
html += q.Message + "</br></br>"
html += `<strong>Stack:</strong></br>` + q.Stack
} else {
html += jsError.Error()
}
html += `</pre></code></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(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
`"`, "&quot;",
"'", "&#39;",
).Replace(s)
}

View File

@@ -1,2 +0,0 @@
const req = request;
const resp = response;

View File

@@ -1,101 +0,0 @@
package quickjs
import (
"bytes"
_ "embed"
"io"
"net/http"
"os"
"strings"
"github.com/buke/quickjs-go"
"github.com/pkg/errors"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
//go:embed inject.js
var inject string
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(&param); err != nil {
return nil, err
}
if param.Exec == "" {
return nil, errors.Errorf("no exec specified")
}
return func(ctx core.FilterContext, writer http.ResponseWriter, request *http.Request, next core.NextCall) error {
js, err := ctx.ReadString(ctx, param.Exec)
if err != nil {
return err
}
rt := quickjs.NewRuntime()
rt.SetExecuteTimeout(5)
rt.SetMemoryLimit(10 * 1024 * 1024)
defer rt.Close()
jsCtx := rt.NewContext()
defer jsCtx.Close()
cacheKey := "qjs/" + param.Exec
var bytecode []byte
cacheData, err := ctx.Cache.Get(ctx, cacheKey)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
if bytecode, err = jsCtx.Compile(js,
quickjs.EvalFlagCompileOnly(true),
quickjs.EvalFileName(param.Exec)); err != nil {
return err
}
if err = ctx.Cache.Put(ctx, cacheKey, map[string]string{}, bytes.NewBuffer(bytecode)); err != nil {
return err
}
} else {
defer cacheData.Close()
if bytecode, err = io.ReadAll(cacheData); err != nil {
return err
}
}
// 在 debug 模式下,我们需要拦截输出
var (
outputBuffer strings.Builder
logBuffer strings.Builder
jsError error
)
global := jsCtx.Globals()
global.Set("request", createRequestObject(jsCtx, request, ctx))
// 根据是否 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))
}
jsCtx.Eval(inject)
ret := jsCtx.EvalBytecode(bytecode)
defer ret.Free()
jsCtx.Loop()
if ret.IsException() {
err := jsCtx.Exception()
jsError = err
}
// 如果在 debug 模式下,返回 HTML 调试页面
if param.Debug {
return renderDebugPage(writer, &outputBuffer, &logBuffer, jsError)
}
return jsError
}, nil
}

View File

@@ -1,40 +0,0 @@
package quickjs
import (
"fmt"
"log"
"strings"
"github.com/buke/quickjs-go"
)
// createConsoleObject 创建 console 对象用于日志输出
func createConsoleObject(ctx *quickjs.Context, buf *strings.Builder) *quickjs.Value {
console := ctx.NewObject()
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())
}
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", 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
}

View File

@@ -1 +0,0 @@
package quickjs

View File

@@ -1,64 +0,0 @@
package quickjs
import (
"io"
"net/http"
"github.com/buke/quickjs-go"
"gopkg.d7z.net/gitea-pages/pkg/core"
)
// createRequestObject 创建表示 HTTP 请求的 JavaScript 对象
func createRequestObject(ctx *quickjs.Context, req *http.Request, filterCtx core.FilterContext) *quickjs.Value {
obj := ctx.NewObject()
// 基本属性
obj.Set("method", ctx.NewString(req.Method))
url := *req.URL
url.Path = filterCtx.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))
obj.Set("httpVersion", ctx.NewString(req.Proto))
// 解析查询参数
queryObj := ctx.NewObject()
for key, values := range url.Query() {
if len(values) > 0 {
queryObj.Set(key, ctx.NewString(values[0]))
}
}
obj.Set("query", queryObj)
// 添加 headers
headersObj := ctx.NewObject()
for key, values := range req.Header {
if len(values) > 0 {
headersObj.Set(key, ctx.NewString(values[0]))
}
}
obj.Set("headers", headersObj)
// 请求方法
obj.Set("get", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
key := args[0].String()
return ctx.NewString(req.Header.Get(key))
}
return ctx.NewNull()
}))
// 读取请求体
obj.Set("readBody", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
body, err := io.ReadAll(req.Body)
if err != nil {
return ctx.NewError(err)
}
return ctx.NewString(string(body))
}))
return obj
}

View File

@@ -1,181 +0,0 @@
package quickjs
import (
"net/http"
"time"
"github.com/buke/quickjs-go"
)
// createResponseObject 创建表示 HTTP 响应的 JavaScript 对象
func createResponseObject(ctx *quickjs.Context, writer http.ResponseWriter, req *http.Request) *quickjs.Value {
obj := ctx.NewObject()
// 响应头操作
obj.Set("setHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) >= 2 {
key := args[0].String()
value := args[1].String()
writer.Header().Set(key, value)
}
return ctx.NewNull()
}))
obj.Set("getHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
key := args[0].String()
return ctx.NewString(writer.Header().Get(key))
}
return ctx.NewNull()
}))
obj.Set("removeHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
key := args[0].String()
writer.Header().Del(key)
}
return ctx.NewNull()
}))
obj.Set("hasHeader", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
key := args[0].String()
_, exists := writer.Header()[key]
return ctx.NewBool(exists)
}
return ctx.NewBool(false)
}))
// 状态码操作
obj.Set("setStatus", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
writer.WriteHeader(int(args[0].ToInt32()))
}
return ctx.NewNull()
}))
obj.Set("statusCode", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
writer.WriteHeader(int(args[0].ToInt32()))
}
return ctx.NewNull()
}))
// 写入响应
obj.Set("write", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
data := args[0].String()
_, err := writer.Write([]byte(data))
if err != nil {
return ctx.NewError(err)
}
}
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]
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()
}))
obj.Set("end", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
data := args[0].String()
_, err := writer.Write([]byte(data))
if err != nil {
return ctx.NewError(err)
}
}
// 在实际的 HTTP 处理中,我们通常不手动结束响应
return ctx.NewNull()
}))
// 重定向
obj.Set("redirect", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
location := args[0].String()
statusCode := 302
if len(args) > 1 {
statusCode = int(args[1].ToInt32())
}
http.Redirect(writer, req, location, statusCode)
}
return ctx.NewNull()
}))
// JSON 响应
obj.Set("json", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) > 0 {
writer.Header().Set("Content-Type", "application/json")
jsonData := args[0].String()
_, err := writer.Write([]byte(jsonData))
if err != nil {
return ctx.NewError(err)
}
}
return ctx.NewNull()
}))
// 设置 cookie
obj.Set("setCookie", ctx.NewFunction(func(c *quickjs.Context, value *quickjs.Value, args []*quickjs.Value) *quickjs.Value {
if len(args) >= 2 {
name := args[0].String()
value := args[1].String()
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
}
// 处理可选参数
if len(args) >= 3 && args[2].IsObject() {
options := args[2]
if maxAge := options.Get("maxAge"); !maxAge.IsNull() {
cookie.MaxAge = int(maxAge.ToInt32())
}
if expires := options.Get("expires"); !expires.IsNull() {
if expires.IsNumber() {
cookie.Expires = time.Unix(expires.ToInt64(), 0)
}
}
if path := options.Get("path"); !path.IsNull() {
cookie.Path = path.String()
}
if domain := options.Get("domain"); !domain.IsNull() {
cookie.Domain = domain.String()
}
if secure := options.Get("secure"); !secure.IsNull() {
cookie.Secure = secure.ToBool()
}
if httpOnly := options.Get("httpOnly"); !httpOnly.IsNull() {
cookie.HttpOnly = httpOnly.ToBool()
}
}
http.SetCookie(writer, cookie)
}
return ctx.NewNull()
}))
return obj
}

View File

@@ -27,7 +27,12 @@ func NewDefaultTestServer() *TestServer {
func NewTestServer(domain string) *TestServer {
atom := zap.NewAtomicLevel()
atom.SetLevel(zap.DebugLevel)
getenv := os.Getenv("BM")
if getenv != "" {
atom.SetLevel(zap.ErrorLevel)
} else {
atom.SetLevel(zap.DebugLevel)
}
cfg := zap.NewProductionConfig()
cfg.Level = atom
logger, _ := cfg.Build()
@@ -88,7 +93,7 @@ func (t *TestServer) OpenRequest(method, url string, body io.Reader) ([]byte, *h
if response.Body != nil {
defer response.Body.Close()
}
if response.StatusCode != http.StatusOK {
if response.StatusCode >= 400 {
return nil, response, errors.New(response.Status)
}
all, _ := io.ReadAll(response.Body)

View File

@@ -2,13 +2,14 @@ package tests
import (
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.d7z.net/gitea-pages/tests/core"
)
func Test_JS(t *testing.T) {
func Test_GoJaJS(t *testing.T) {
server := core.NewDefaultTestServer()
defer server.Close()
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
@@ -16,24 +17,28 @@ func Test_JS(t *testing.T) {
function get(a,b) {
return a + b;
}
response.writeHead(201,{'X-Cache': 'ignore'});
console.log('hello world')
response.write('512 + 512 = ' + get(512,512))
`)
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
routes:
- path: "api/v1/**"
qjs:
js:
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/get")
data, resp, err := server.OpenFile("https://org1.example.com/repo1/api/v1/get")
assert.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode)
assert.Equal(t, "ignore", resp.Header.Get("X-Cache"))
assert.Equal(t, "512 + 512 = 1024", string(data))
}
func Test_JS_Request(t *testing.T) {
func Test_GoJa_Request(t *testing.T) {
server := core.NewDefaultTestServer()
defer server.Close()
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
@@ -41,7 +46,7 @@ func Test_JS_Request(t *testing.T) {
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
routes:
- path: "api/v1/**"
qjs:
js:
exec: "index.js"
`)
data, _, err := server.OpenFile("https://org1.example.com/repo1/")
@@ -57,8 +62,8 @@ routes:
assert.Equal(t, "POST /api/v1/fetch", string(data))
}
func Benchmark_JS_Request(b *testing.B) {
// 初始化服务器(在基准测试外执行,避免计入时间)
func Benchmark_GoJa_Request(b *testing.B) {
_ = os.Setenv("BM", "1")
server := core.NewDefaultTestServer()
defer server.Close()
server.AddFile("org1/repo1/gh-pages/index.html", "hello world")
@@ -66,7 +71,7 @@ func Benchmark_JS_Request(b *testing.B) {
server.AddFile("org1/repo1/gh-pages/.pages.yaml", `
routes:
- path: "api/v1/**"
qjs:
js:
exec: "index.js"
`)