Skip to content

Commit 9f182a5

Browse files
committed
feat: add expr engine; support anonymous object
1 parent ba6b077 commit 9f182a5

10 files changed

Lines changed: 418 additions & 28 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ require (
1919
gvisor.dev/gvisor v0.0.0-20250820192457-dde98974cb6c
2020
)
2121

22+
require github.com/expr-lang/expr v1.17.6
23+
2224
require (
2325
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
2426
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
99
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1010
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
1111
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
12+
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
13+
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
1214
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
1315
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
1416
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=

http/expr.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package http
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/expr-lang/expr"
8+
"github.com/expr-lang/expr/ast"
9+
"github.com/expr-lang/expr/vm"
10+
"github.com/mrhaoxx/OpenNG/utils"
11+
)
12+
13+
type exprbased struct {
14+
*vm.Program
15+
}
16+
17+
type MethodAsFuncPatcher struct{}
18+
19+
func (MethodAsFuncPatcher) Visit(node *ast.Node) {
20+
call, ok := (*node).(*ast.CallNode)
21+
if !ok {
22+
return
23+
}
24+
m, ok := call.Callee.(*ast.MemberNode)
25+
if !ok || !m.Method {
26+
return
27+
}
28+
var name string
29+
switch p := m.Property.(type) {
30+
case *ast.StringNode:
31+
name = p.Value
32+
case *ast.IdentifierNode:
33+
name = p.Value
34+
default:
35+
return
36+
}
37+
38+
if t := m.Node.Type(); t != nil {
39+
if meth, ok := t.MethodByName(name); ok && meth.Type.NumOut() > 0 {
40+
return
41+
}
42+
}
43+
44+
newCall := &ast.CallNode{
45+
Callee: &ast.IdentifierNode{Value: "__call"},
46+
Arguments: append([]ast.Node{m.Node, &ast.StringNode{Value: name}}, call.Arguments...),
47+
}
48+
ast.Patch(node, newCall)
49+
50+
(*node).SetType(reflect.TypeOf((*any)(nil)).Elem())
51+
}
52+
53+
var caller = expr.Function(
54+
"__call",
55+
func(params ...any) (any, error) {
56+
recv := params[0]
57+
method := params[1].(string)
58+
args := params[2:]
59+
60+
if recv == nil {
61+
return nil, nil
62+
}
63+
64+
v := reflect.ValueOf(recv)
65+
m := v.MethodByName(method)
66+
if !m.IsValid() {
67+
return nil, fmt.Errorf("no such method: %s", method)
68+
}
69+
70+
in := make([]reflect.Value, len(args))
71+
for i, a := range args {
72+
in[i] = reflect.ValueOf(a)
73+
}
74+
out := m.Call(in)
75+
76+
switch len(out) {
77+
case 0:
78+
return true, nil
79+
case 1:
80+
return out[0].Interface(), nil
81+
default:
82+
if err, ok := out[len(out)-1].Interface().(error); ok && err != nil {
83+
return nil, err
84+
}
85+
return out[0].Interface(), nil
86+
}
87+
},
88+
new(func(any, string, ...any) any),
89+
)
90+
91+
func NewExprbased(expression string) (*exprbased, error) {
92+
program, err := expr.Compile(expression, expr.Env(&HttpCtx{}), expr.AsBool(), expr.Patch(MethodAsFuncPatcher{}),
93+
caller)
94+
95+
if err != nil {
96+
return nil, err
97+
}
98+
return &exprbased{
99+
Program: program,
100+
}, nil
101+
}
102+
103+
func (e *exprbased) HandleHTTP(ctx *HttpCtx) Ret {
104+
output, err := expr.Run(e.Program, ctx)
105+
if err != nil {
106+
panic(err)
107+
}
108+
ret, _ := output.(bool)
109+
return Ret(ret)
110+
}
111+
112+
func (e *exprbased) Hosts() utils.GroupRegexp {
113+
return nil
114+
}

http/http.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ func (c *HttpCtx) Redirect(url string, code int) {
5151
http.Redirect(c.Resp, c.Req, url, code)
5252
}
5353

54-
func (c *HttpCtx) WriteString(s string) {
55-
io.WriteString(c.Resp, s)
54+
func (c *HttpCtx) WriteString(s string) (n int, err error) {
55+
return io.WriteString(c.Resp, s)
5656
}
5757

5858
func (c *HttpCtx) SetCookie(k *http.Cookie) {
@@ -174,6 +174,7 @@ func (h *Midware) head(rw http.ResponseWriter, r *http.Request, conn *tcp.Conn)
174174
writer: nil,
175175
stdrw: rw,
176176
acceptEncoding: r.Header.Get("Accept-Encoding"),
177+
HeaderRef: rw.Header(),
177178
}
178179

179180
r.Header.Del("Accept-Encoding") // we don't want the backend to encode. WE DO IT.
@@ -207,6 +208,8 @@ type NgResponseWriter struct {
207208
writer io.Writer
208209
stdrw http.ResponseWriter
209210

211+
HeaderRef http.Header
212+
210213
acceptEncoding string
211214

212215
code int
@@ -241,13 +244,13 @@ func (w *NgResponseWriter) Header() http.Header {
241244

242245
func (w *NgResponseWriter) BypassEncoding() {
243246
if w.acceptEncoding != "" {
244-
w.Header().Set("Accept-Encoding", w.acceptEncoding)
247+
w.HeaderRef.Set("Accept-Encoding", w.acceptEncoding)
245248
w.acceptEncoding = ""
246249
}
247250
}
248251

249252
func (w *NgResponseWriter) initForWrite() {
250-
w.Header().Set("Server", utils.ServerSign)
253+
w.HeaderRef.Set("Server", utils.ServerSign)
251254
switch w.code {
252255
case StatusSwitchingProtocols: // do nothing
253256
return
@@ -256,11 +259,11 @@ func (w *NgResponseWriter) initForWrite() {
256259
w.code = StatusOK
257260
fallthrough
258261
default: // init encode
259-
if w.Header().Get("Content-Encoding") == "" &&
262+
if w.HeaderRef.Get("Content-Encoding") == "" &&
260263
w.acceptEncoding != "" { // if the content hasn't encoded,then we encode it here
261-
ContentLength, _ := strconv.ParseUint(w.Header().Get("Content-Length"), 10, 64) // get the content length
262-
ContentType := w.Header().Get("Content-Type") // get the content type
263-
IsDownloading := strings.HasPrefix(w.Header().Get("Content-Disposition"), "attachment") // check if this request is a download action
264+
ContentLength, _ := strconv.ParseUint(w.HeaderRef.Get("Content-Length"), 10, 64) // get the content length
265+
ContentType := w.HeaderRef.Get("Content-Type") // get the content type
266+
IsDownloading := strings.HasPrefix(w.HeaderRef.Get("Content-Disposition"), "attachment") // check if this request is a download action
264267
switch {
265268
case IsDownloading: // don't encode if is downloading
266269
// case ContentLength <= 1024: // don't encode if size too small
@@ -281,8 +284,8 @@ func (w *NgResponseWriter) initForWrite() {
281284
}
282285
}
283286
if w.encoding != EncodingRAW {
284-
w.Header().Add("Content-Encoding", w.encoding.String())
285-
w.Header().Del("Content-Length") // delete the content length that is no more correct after the encode
287+
w.HeaderRef.Add("Content-Encoding", w.encoding.String())
288+
w.HeaderRef.Del("Content-Length") // delete the content length that is no more correct after the encode
286289

287290
_w := encoderpool[w.encoding].Get().(io.Writer) //get encoder from pool
288291
_w.(reSetter).Reset(w.stdrw) // reset the encoder

http/midware.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package http
22

33
import (
4-
"fmt"
54
"net/http"
65
"strconv"
76
"strings"
@@ -166,7 +165,7 @@ func (h *Midware) Process(RequestCtx *HttpCtx) {
166165
}
167166

168167
if RequestCtx.Resp.code == 0 {
169-
RequestCtx.Resp.ErrorPage(http.StatusInternalServerError, fmt.Sprintf("Panic: %v", err))
168+
RequestCtx.Resp.ErrorPage(http.StatusInternalServerError, "Internal Server Error")
170169
}
171170
}
172171
}()
@@ -231,7 +230,7 @@ func NewHttpMidware(sni []string) *Midware {
231230
hmw.bufferedLookupForHost = utils.NewBufferedLookup(func(s string) []*ServiceStruct {
232231
ret := make([]*ServiceStruct, 0)
233232
for _, r := range hmw.current {
234-
if r.Hosts.MatchString(s) {
233+
if r.Hosts == nil || r.Hosts.MatchString(s) {
235234
ret = append(ret, r)
236235
}
237236
}

tcp/expr.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package tcp
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/expr-lang/expr"
8+
"github.com/expr-lang/expr/ast"
9+
"github.com/expr-lang/expr/vm"
10+
)
11+
12+
type exprbased struct {
13+
*vm.Program
14+
}
15+
16+
type MethodAsFuncPatcher struct{}
17+
18+
func (MethodAsFuncPatcher) Visit(node *ast.Node) {
19+
call, ok := (*node).(*ast.CallNode)
20+
if !ok {
21+
return
22+
}
23+
m, ok := call.Callee.(*ast.MemberNode)
24+
if !ok || !m.Method {
25+
return
26+
}
27+
var name string
28+
switch p := m.Property.(type) {
29+
case *ast.StringNode:
30+
name = p.Value
31+
case *ast.IdentifierNode:
32+
name = p.Value
33+
default:
34+
return
35+
}
36+
37+
if t := m.Node.Type(); t != nil {
38+
if meth, ok := t.MethodByName(name); ok && meth.Type.NumOut() > 0 {
39+
return
40+
}
41+
}
42+
43+
newCall := &ast.CallNode{
44+
Callee: &ast.IdentifierNode{Value: "__call"},
45+
Arguments: append([]ast.Node{m.Node, &ast.StringNode{Value: name}}, call.Arguments...),
46+
}
47+
ast.Patch(node, newCall)
48+
49+
(*node).SetType(reflect.TypeOf((*any)(nil)).Elem())
50+
}
51+
52+
var caller = expr.Function(
53+
"__call",
54+
func(params ...any) (any, error) {
55+
recv := params[0]
56+
method := params[1].(string)
57+
args := params[2:]
58+
59+
if recv == nil {
60+
return nil, nil
61+
}
62+
63+
v := reflect.ValueOf(recv)
64+
m := v.MethodByName(method)
65+
if !m.IsValid() {
66+
return nil, fmt.Errorf("no such method: %s", method)
67+
}
68+
69+
in := make([]reflect.Value, len(args))
70+
for i, a := range args {
71+
in[i] = reflect.ValueOf(a)
72+
}
73+
out := m.Call(in)
74+
75+
switch len(out) {
76+
case 0:
77+
return true, nil
78+
case 1:
79+
return out[0].Interface(), nil
80+
default:
81+
if err, ok := out[len(out)-1].Interface().(error); ok && err != nil {
82+
return nil, err
83+
}
84+
return out[0].Interface(), nil
85+
}
86+
},
87+
new(func(any, string, ...any) any),
88+
)
89+
90+
func NewExprbased(expression string) (*exprbased, error) {
91+
program, err := expr.Compile(expression, expr.Env(&Conn{}), expr.AsInt(), expr.Patch(MethodAsFuncPatcher{}),
92+
caller)
93+
94+
if err != nil {
95+
return nil, err
96+
}
97+
return &exprbased{
98+
Program: program,
99+
}, nil
100+
}
101+
102+
func (e *exprbased) Handle(ctx *Conn) SerRet {
103+
output, err := expr.Run(e.Program, ctx)
104+
if err != nil {
105+
panic(err)
106+
}
107+
ret, _ := output.(int)
108+
return SerRet(ret)
109+
}

ui/builtin.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,16 @@ var _builtin_refs_assertions = map[string]Assert{
511511
},
512512
},
513513
},
514+
"builtin::http::expr": {
515+
Type: "string",
516+
Required: true,
517+
Desc: "expression-based authentication backend",
518+
},
519+
"builtin::tcp::expr": {
520+
Type: "string",
521+
Required: true,
522+
Desc: "expression-based TCP backend",
523+
},
514524
"builtin::auth::knocked": {
515525
Type: "map",
516526
Sub: AssertMap{
@@ -1385,6 +1395,24 @@ var _builtin_refs = map[string]Inst{
13851395

13861396
return policyd, nil
13871397
},
1398+
"builtin::http::expr": func(spec *ArgNode) (any, error) {
1399+
expression := spec.ToString()
1400+
1401+
zlog.Debug().
1402+
Str("expression", expression).
1403+
Msg("new http expr backend")
1404+
1405+
return http.NewExprbased(expression)
1406+
},
1407+
"builtin::tcp::expr": func(spec *ArgNode) (any, error) {
1408+
expression := spec.ToString()
1409+
1410+
zlog.Debug().
1411+
Str("expression", expression).
1412+
Msg("new tcp expr backend")
1413+
1414+
return tcp.NewExprbased(expression)
1415+
},
13881416
"builtin::auth::knocked": func(spec *ArgNode) (any, error) {
13891417
timeout := spec.MustGet("timeout").ToDuration()
13901418

ui/parser.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,14 @@ func (node *ArgNode) IfCompatibleAndConvert(assertions Assert) bool {
216216
node.Type = "ptr"
217217
return true
218218
}
219+
if node.Type == "map" {
220+
if m, ok := node.Value.(map[string]*ArgNode); ok {
221+
if _, ok := m["kind"]; ok {
222+
node.Type = "ptr"
223+
return true
224+
}
225+
}
226+
}
219227
case "duration":
220228
if node.Type == "string" {
221229
if dur, err := time.ParseDuration(node.Value.(string)); err == nil {

0 commit comments

Comments
 (0)