From 53f3a5697221c4786a29806d16c2e6882d67db67 Mon Sep 17 00:00:00 2001 From: GoEdgeLab Date: Sat, 21 Aug 2021 20:45:11 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=B6=E6=AE=B5=E6=80=A7=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/js/console.go | 50 ++++++++++ internal/js/console_test.go | 38 ++++++++ internal/js/http.go | 36 +++++++ internal/js/request.go | 82 ++++++++++++++++ internal/js/request_interface.go | 19 ++++ internal/js/request_test.go | 124 ++++++++++++++++++++++++ internal/js/response.go | 39 ++++++++ internal/js/response_test.go | 16 ++++ internal/js/url.go | 90 ++++++++++++++++++ internal/js/url_test.go | 18 ++++ internal/js/vm.go | 153 ++++++++++++++++++++++++++++++ internal/js/vm_test.go | 158 +++++++++++++++++++++++++++++++ 12 files changed, 823 insertions(+) create mode 100644 internal/js/console.go create mode 100644 internal/js/console_test.go create mode 100644 internal/js/http.go create mode 100644 internal/js/request.go create mode 100644 internal/js/request_interface.go create mode 100644 internal/js/request_test.go create mode 100644 internal/js/response.go create mode 100644 internal/js/response_test.go create mode 100644 internal/js/url.go create mode 100644 internal/js/url_test.go create mode 100644 internal/js/vm.go create mode 100644 internal/js/vm_test.go diff --git a/internal/js/console.go b/internal/js/console.go new file mode 100644 index 0000000..e44ed03 --- /dev/null +++ b/internal/js/console.go @@ -0,0 +1,50 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "encoding/json" + "github.com/iwind/TeaGo/logs" + "reflect" +) + +type Console struct { +} + +func (this *Console) Log(args ...interface{}) { + for index, arg := range args { + if arg != nil { + switch arg.(type) { + case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, string: + default: + var argType = reflect.TypeOf(arg) + + // 是否有String()方法,如果有直接调用 + method, ok := argType.MethodByName("String") + if ok && method.Type.NumIn() == 1 && method.Type.NumOut() == 1 && method.Type.Out(0).Kind() == reflect.String { + args[index] = method.Func.Call([]reflect.Value{reflect.ValueOf(arg)})[0].String() + continue + } + + // 转为JSON + argJSON, err := this.toJSON(arg) + if err != nil { + if argType.Kind() == reflect.Func { + args[index] = "[function]" + } else { + args[index] = "[object]" + } + } else { + args[index] = string(argJSON) + } + } + } else { + args[index] = "null" + } + } + logs.Println(append([]interface{}{"[js][console]"}, args...)...) +} + +func (this *Console) toJSON(o interface{}) ([]byte, error) { + return json.Marshal(o) +} diff --git a/internal/js/console_test.go b/internal/js/console_test.go new file mode 100644 index 0000000..d2fadb6 --- /dev/null +++ b/internal/js/console_test.go @@ -0,0 +1,38 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "testing" +) + +func TestConsole_Log(t *testing.T) { + { + vm := NewVM() + _, err := vm.RunString("console.log('Hello', 'world')") + if err != nil { + t.Fatal(err) + } + } + { + vm := NewVM() + _, err := vm.RunString("console.log(null, true, false, 10, 10.123)") + if err != nil { + t.Fatal(err) + } + } + { + vm := NewVM() + _, err := vm.RunString("console.log({ a:1, b:2 })") + if err != nil { + t.Fatal(err) + } + } + { + vm := NewVM() + _, err := vm.RunString("console.log(console.log)") + if err != nil { + t.Fatal(err) + } + } +} diff --git a/internal/js/http.go b/internal/js/http.go new file mode 100644 index 0000000..b0464ae --- /dev/null +++ b/internal/js/http.go @@ -0,0 +1,36 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +type HTTP struct { + r RequestInterface + + req *Request + resp *Response + + onRequest func(req *Request, resp *Response) +} + +func NewHTTP(r RequestInterface) *HTTP { + return &HTTP{ + req: NewRequest(r), + resp: NewResponse(r), + } +} + +func (this *HTTP) OnRequest(callback func(req *Request, resp *Response)) { + // TODO 考虑是否支持多个callback + this.onRequest = callback +} + +func (this *HTTP) OnData(callback func(req *Request, resp *Response)) { + // TODO +} + +func (this *HTTP) OnResponse(callback func(req *Request, resp *Response)) { + // TODO +} + +func (this *HTTP) TriggerRequest() { + this.onRequest(this.req, this.resp) +} diff --git a/internal/js/request.go b/internal/js/request.go new file mode 100644 index 0000000..aae2399 --- /dev/null +++ b/internal/js/request.go @@ -0,0 +1,82 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "bytes" + "io/ioutil" + "net" +) + +type Request struct { + r RequestInterface +} + +func NewRequest(r RequestInterface) *Request { + return &Request{ + r: r, + } +} + +func (this *Request) Proto() string { + return this.r.JSRequest().Proto +} + +func (this *Request) Method() string { + return this.r.JSRequest().Method +} + +func (this *Request) Header() map[string][]string { + return this.r.JSRequest().Header +} + +func (this *Request) AddHeader(name string, value string) { + this.r.JSRequest().Header[name] = append(this.r.JSRequest().Header[name], value) +} + +func (this *Request) SetHeader(name string, value string) { + this.r.JSRequest().Header[name] = []string{value} +} + +func (this *Request) RemoteAddr() string { + var remoteAddr = this.r.JSRequest().RemoteAddr + host, _, err := net.SplitHostPort(remoteAddr) + if err == nil { + return host + } + return remoteAddr +} + +func (this *Request) Url() *URL { + return NewURL(this.r.JSRequest().URL) +} + +func (this *Request) ContentLength() int64 { + return this.r.JSRequest().ContentLength +} + +func (this *Request) Body() []byte { + var bodyReader = this.r.JSRequest().Body + if bodyReader == nil { + return []byte{} + } + data, err := ioutil.ReadAll(bodyReader) + if err != nil { + this.r.JSLog("read body failed: " + err.Error()) + } + return data +} + +func (this *Request) CopyBody() []byte { + var bodyReader = this.r.JSRequest().Body + if bodyReader == nil { + return []byte{} + } + + data, err := ioutil.ReadAll(bodyReader) + if err != nil { + this.r.JSLog("read body failed: " + err.Error()) + } + this.r.JSRequest().Body = ioutil.NopCloser(bytes.NewReader(data)) + return data +} diff --git a/internal/js/request_interface.go b/internal/js/request_interface.go new file mode 100644 index 0000000..6f31242 --- /dev/null +++ b/internal/js/request_interface.go @@ -0,0 +1,19 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import "net/http" + +type RequestInterface interface { + // JSRequest 请求 + JSRequest() *http.Request + + // JSWriter 响应 + JSWriter() http.ResponseWriter + + // JSStop 中止请求 + JSStop() + + // JSLog 打印日志 + JSLog(msg ...interface{}) +} diff --git a/internal/js/request_test.go b/internal/js/request_test.go new file mode 100644 index 0000000..116fe88 --- /dev/null +++ b/internal/js/request_test.go @@ -0,0 +1,124 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js_test + +import ( + "bytes" + "github.com/TeaOSLab/EdgeNode/internal/js" + "github.com/iwind/TeaGo/logs" + "io/ioutil" + "net/http" + "testing" +) + +type testRequest struct { + rawRequest *http.Request + rawResponse *testResponse +} + +func (this *testRequest) JSRequest() *http.Request { + if this.rawRequest != nil { + return this.rawRequest + } + req, _ := http.NewRequest(http.MethodGet, "https://iwind:123456@goedge.cn/docs?name=Libai&age=20", nil) + req.Header.Set("Server", "edgejs/1.0") + req.Header.Set("Content-Type", "application/json") + req.Body = ioutil.NopCloser(bytes.NewReader([]byte("123456"))) + this.rawRequest = req + return req +} + +func (this *testRequest) JSWriter() http.ResponseWriter { + if this.rawResponse != nil { + return this.rawResponse + } + this.rawResponse = &testResponse{} + return this.rawResponse +} + +func (this *testRequest) JSStop() { + +} + +func (this *testRequest) JSLog(msg ...interface{}) { + logs.Println(msg...) +} + +type testResponse struct { + statusCode int + header http.Header +} + +func (this *testResponse) Header() http.Header { + if this.header == nil { + this.header = http.Header{} + } + return this.header +} + +func (this *testResponse) Write(p []byte) (int, error) { + return len(p), nil +} + +func (this *testResponse) WriteHeader(statusCode int) { + this.statusCode = statusCode +} + +func TestRequest(t *testing.T) { + vm := js.NewVM() + vm.SetRequest(&testRequest{}) + + // 事件监听 + _, err := vm.RunString(` + http.onRequest(function (req, resp) { + console.log(req.proto()) + + let url = req.url() + console.log(url, "port:", url.port(), "args:", url.args()) + console.log("username:", url.username(), "password:", url.password()) + console.log("uri:", url.uri(), "path:", url.path()) + + req.addHeader("Server", "1.0") + + + resp.write("this is response") + console.log(resp) + + console.log(req.body()) + }) +`) + if err != nil { + t.Fatal(err) + } + + // 触发事件 + _, err = vm.RunString(`http.triggerRequest()`) + if err != nil { + t.Fatal(err) + } +} + +func TestRequest_Header(t *testing.T) { + var req = js.NewRequest(&testRequest{}) + logs.PrintAsJSON(req.Header(), t) + + req.AddHeader("Content-Length", "10") + req.AddHeader("Vary", "1.0") + req.AddHeader("Vary", "2.0") + logs.PrintAsJSON(req.Header(), t) + + req.SetHeader("Vary", "3.0") + logs.PrintAsJSON(req.Header(), t) +} + +func TestRequest_Body(t *testing.T) { + var req = js.NewRequest(&testRequest{}) + t.Log(string(req.Body())) + t.Log(string(req.Body())) +} + +func TestRequest_CopyBody(t *testing.T) { + var req = js.NewRequest(&testRequest{}) + t.Log(string(req.CopyBody())) + t.Log(string(req.CopyBody())) +} diff --git a/internal/js/response.go b/internal/js/response.go new file mode 100644 index 0000000..ad3d4cf --- /dev/null +++ b/internal/js/response.go @@ -0,0 +1,39 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +type Response struct { + r RequestInterface +} + +func NewResponse(r RequestInterface) *Response { + return &Response{ + r: r, + } +} + +func (this *Response) Write(s string) error { + _, err := this.r.JSWriter().Write([]byte(s)) + return err +} + +func (this *Response) Reply(status int) { + this.SetStatus(status) + this.r.JSStop() +} + +func (this *Response) Header() map[string][]string { + return this.r.JSWriter().Header() +} + +func (this *Response) AddHeader(name string, value string) { + this.r.JSWriter().Header()[name] = append(this.r.JSWriter().Header()[name], value) +} + +func (this *Response) SetHeader(name string, value string) { + this.r.JSWriter().Header()[name] = []string{value} +} + +func (this *Response) SetStatus(statusCode int) { + this.r.JSWriter().WriteHeader(statusCode) +} diff --git a/internal/js/response_test.go b/internal/js/response_test.go new file mode 100644 index 0000000..1f41f1e --- /dev/null +++ b/internal/js/response_test.go @@ -0,0 +1,16 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js_test + +import ( + "github.com/TeaOSLab/EdgeNode/internal/js" + "testing" +) + +func TestNewResponse(t *testing.T) { + var resp = js.NewResponse(&testRequest{}) + resp.AddHeader("Vary", "1.0") + resp.AddHeader("Vary", "2.0") + resp.SetHeader("Server", "edgejs/1.0") + t.Logf("%#v", resp.Header()) +} diff --git a/internal/js/url.go b/internal/js/url.go new file mode 100644 index 0000000..3dc6a24 --- /dev/null +++ b/internal/js/url.go @@ -0,0 +1,90 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "github.com/dop251/goja" + "github.com/iwind/TeaGo/types" + "net/url" +) + +type URL struct { + u *url.URL +} + +func NewURL(u *url.URL) *URL { + return &URL{ + u: u, + } +} + +func (this *URL) JSNew(args []goja.Value) *URL { + var urlString = "" + if len(args) == 1 { + urlString = args[0].String() + } + u, _ := url.Parse(urlString) + if u == nil { + u = &url.URL{} + } + return NewURL(u) +} + +func (this *URL) Port() int { + return types.Int(this.u.Port()) +} + +func (this *URL) Args() map[string][]string { + return this.u.Query() +} + +func (this *URL) Arg(name string) string { + return this.u.Query().Get(name) +} + +func (this *URL) Username() string { + if this.u.User != nil { + return this.u.User.Username() + } + return "" +} + +func (this *URL) Password() string { + if this.u.User != nil { + password, _ := this.u.User.Password() + return password + } + return "" +} + +func (this *URL) Uri() string { + return this.u.RequestURI() +} + +func (this *URL) Path() string { + return this.u.Path +} + +func (this *URL) Host() string { + return this.u.Host +} + +func (this *URL) Fragment() string { + return this.u.Fragment +} + +func (this *URL) Hash() string { + if len(this.u.Fragment) > 0 { + return "#" + this.u.Fragment + } else { + return "" + } +} + +func (this *URL) Scheme() string { + return this.u.Scheme +} + +func (this *URL) String() string { + return this.u.String() +} diff --git a/internal/js/url_test.go b/internal/js/url_test.go new file mode 100644 index 0000000..def511a --- /dev/null +++ b/internal/js/url_test.go @@ -0,0 +1,18 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "net/url" + "testing" +) + +func TestURL(t *testing.T) { + raw, err := url.Parse("https://iwind:123456@goedge.cn/docs?name=Libai&age=20#a=b") + if err != nil { + t.Fatal(err) + } + var u = NewURL(raw) + t.Log("host:", u.Host()) + t.Log("hash:", u.Hash()) +} diff --git a/internal/js/vm.go b/internal/js/vm.go new file mode 100644 index 0000000..b1d2715 --- /dev/null +++ b/internal/js/vm.go @@ -0,0 +1,153 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "errors" + "github.com/dop251/goja" + "github.com/iwind/TeaGo/logs" + "reflect" + "strings" +) + +var sharedPrograms []*goja.Program +var sharedConsole = &Console{} + +func init() { + // compile programs +} + +type VM struct { + vm *goja.Runtime +} + +func NewVM() *VM { + vm := goja.New() + vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + + // programs + for _, program := range sharedPrograms { + _, _ = vm.RunProgram(program) + } + + v := &VM{vm: vm} + v.initVM() + return v +} + +func (this *VM) Set(name string, obj interface{}) error { + return this.vm.Set(name, obj) +} + +func (this *VM) AddConstructor(name string, instance interface{}) error { + objType := reflect.TypeOf(instance) + + if objType.Kind() != reflect.Ptr { + return errors.New("instance should be pointer") + } + + // construct + newMethod, ok := objType.MethodByName("JSNew") + if !ok { + return errors.New("can not find 'JSNew()' method in '" + objType.Elem().Name() + "'") + } + + var err = this.Set(name, func(call goja.ConstructorCall) *goja.Object { + if newMethod.Type.NumIn() != 2 { + this.throw(errors.New(objType.Elem().Name() + ".JSNew() should accept a '[]goja.Value' argument")) + return nil + } + if newMethod.Type.In(1).String() != "[]goja.Value" { + this.throw(errors.New(objType.Elem().Name() + ".JSNew() should accept a '[]goja.Value' argument")) + return nil + } + + // new + var results = newMethod.Func.Call([]reflect.Value{reflect.ValueOf(instance), reflect.ValueOf(call.Arguments)}) + if len(results) == 0 { + this.throw(errors.New(objType.Elem().Name() + ".JSNew() should return a valid instance")) + return nil + } + var result = results[0] + if result.Type() != objType { + this.throw(errors.New(objType.Elem().Name() + ".JSNew() should return a same instance")) + return nil + } + + // methods + var resultType = result.Type() + var numMethod = result.NumMethod() + for i := 0; i < numMethod; i++ { + var method = resultType.Method(i) + var methodName = strings.ToLower(method.Name[:1]) + method.Name[1:] + err := call.This.Set(methodName, result.MethodByName(method.Name).Interface()) + if err != nil { + this.throw(err) + continue + } + } + + // 支持属性 + var numField = result.Elem().Type().NumField() + for i := 0; i < numField; i++ { + var field = result.Elem().Field(i) + if !field.CanInterface() { + continue + } + var fieldType = objType.Elem().Field(i) + tag, ok := fieldType.Tag.Lookup("json") + if !ok { + tag = fieldType.Name + tag = strings.ToLower(tag[:1]) + tag[1:] + } else { + // TODO 校验tag是否符合变量语法 + } + err := call.This.Set(tag, field.Interface()) + if err != nil { + this.throw(err) + continue + } + } + + return nil + }) + return err +} + +func (this *VM) RunString(str string) (goja.Value, error) { + defer func() { + e := recover() + if e != nil { + // TODO 需要打印trace + logs.Println("panic:", e) + } + }() + return this.vm.RunString(str) +} + +func (this *VM) SetRequest(req RequestInterface) { + { + err := this.vm.Set("http", NewHTTP(req)) + if err != nil { + this.throw(err) + } + } +} + +func (this *VM) initVM() { + { + err := this.vm.Set("console", sharedConsole) + if err != nil { + this.throw(err) + } + } +} + +func (this *VM) throw(err error) { + if err == nil { + return + } + + // TODO + logs.Println("js:VM:error: " + err.Error()) +} diff --git a/internal/js/vm_test.go b/internal/js/vm_test.go new file mode 100644 index 0000000..5024875 --- /dev/null +++ b/internal/js/vm_test.go @@ -0,0 +1,158 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package js + +import ( + "github.com/dop251/goja" + "testing" + "time" +) + +func TestNewVM(t *testing.T) { + before := time.Now() + defer func() { + t.Log(time.Since(before).Seconds()*1000, "ms") + }() + + vm := NewVM() + { + v, err := vm.RunString("JSON.stringify({\"a\":\"b\"})") + if err != nil { + t.Fatal(err) + } + t.Log("JSON.stringify():", v) + } + { + v, err := vm.RunString(`JSON.parse('{\"a\":\"b\"}')`) + if err != nil { + t.Fatal(err) + } + t.Log("JSON.parse():", v) + } + { + err := vm.AddConstructor("Url", &URL{}) + if err != nil { + t.Fatal("add constructor error:", err) + } + _, err = vm.RunString(` +{ + let u = new Url("https://goedge.cn/docs?v=1") + console.log("host:", u.host(), u.uri()) +} +{ + let u = new Url("https://teaos.cn/downloads?v=1") + console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url() + console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url("a", "b", "c") + console.log("host:", u.host(), u.uri()) +} +`) + if err != nil { + t.Fatal("add constructor error:" + err.Error()) + } + } +} + +func TestVM_Program(t *testing.T) { + var s = ` +{ + let u = new Url("https://goedge.cn/docs?v=1") + //console.log("host:", u.host(), u.uri()) +} +{ + let u = new Url("https://teaos.cn/downloads?v=1") + //console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url() + //console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url("a", "b", "c") + //console.log("host:", u.host(), u.uri()) +} +` + program := goja.MustCompile("s", s, true) + + before := time.Now() + defer func() { + t.Log(time.Since(before).Seconds()*1000, "ms") + }() + + vm := NewVM() + err := vm.AddConstructor("Url", &URL{}) + if err != nil { + t.Fatal("add constructor error:", err) + } + //_, err = vm.RunString(s) + _, err = vm.vm.RunProgram(program) + if err != nil { + t.Fatal("add constructor error:" + err.Error()) + } +} + +func Benchmark_Program(b *testing.B) { + var s = ` +{ + let u = new Url("https://goedge.cn/docs?v=1") + //console.log("host:", u.host(), u.uri()) +} +{ + let u = new Url("https://teaos.cn/downloads?v=1") + //console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url() + //console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url("a", "b", "c") + //console.log("host:", u.host(), u.uri()) +} +{ + let u = new Url("https://goedge.cn/docs?v=1") + //console.log("host:", u.host(), u.uri()) +} +{ + let u = new Url("https://teaos.cn/downloads?v=1") + //console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url() + //console.log("host:", u.host(), u.uri()) +} + +{ + let u = new Url("a", "b", "c") + //console.log("host:", u.host(), u.uri()) +} +` + program := goja.MustCompile("s", s, true) + + vm := NewVM() + + err := vm.AddConstructor("Url", &URL{}) + if err != nil { + b.Fatal("add constructor error:", err) + } + + for i := 0; i < b.N; i++ { + //_, err = vm.RunString(s) + _, err = vm.vm.RunProgram(program) + if err != nil { + b.Fatal("add constructor error:" + err.Error()) + } + } +}