ref: c9c7b73c0740cf8d6bdd6469226b4d4b2001cb9e
parent: 5f020655d08a351cb5b74d0ba7f6a998eb5be528
author: Philip Silva <philip.silva@protonmail.com>
date: Sat Feb 6 12:31:47 EST 2021
ajax
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
- Server-side rendered websites
- Images (pre-loaded all at once though)
- TLS
-- experimental JS/DOM without AJAX can be activated (basically script tags are evaluated)
+- experimental JS/DOM can be activated (very basic jQuery examples work)
- file downloads
# Install
@@ -66,7 +66,20 @@
# JS support
-Very experimental support for that, the whole page is re-rendered after click events. (http://psilva.sdf.org/demo.gif) Mostly based on goja (ECMAScript 5.1) and github.com/fgnass/domino (DOM implementation in JS). Some sort of DOM diffing is needed, also AJAX functions, `getComputedStyle` etc. are either missing or stubs. Very simple jQuery based code works though, e.g. jQuery UI Tab view https://jqueryui.com/resources/demos/tabs/default.html or the toggle buttons on https://golang.org/pkg There is also highly experimental ES6 support with Babel.
+It's more like a demo and it's not really clear right now how much sandboxing
+is really needed. A rudimentary AJAX implementation
+is there though.
+
+Use on your own Risk!
+
+![Demo](http://psilva.sdf.org/demo.gif "Demo")
+
+Mostly based on goja (ECMAScript 5.1) and https://github.com/fgnass/domino
+(DOM implementation in JS). Some sort of DOM diffing
+is needed, also AJAX functions, `getComputedStyle` etc. are either missing or stubs.
+Very simple jQuery based code works though, e.g. jQuery UI Tab view
+https://jqueryui.com/resources/demos/tabs/default.html or the toggle buttons on
+https://golang.org/pkg There is also highly experimental ES6 support with Babel.
Try on Plan 9 with e.g.:
--- a/browser/browser.go
+++ b/browser/browser.go
@@ -245,7 +245,7 @@
} else {
log.Printf("box background: %f", err)
}
-
+
if p, err = n.Tlbr("padding"); err != nil {
log.Errorf("padding: %v", err)
}
@@ -1262,6 +1262,10 @@
addr = b.URL().Scheme + "://" + b.URL().Host + addr
}
return url.Parse(addr)
+}
+
+func (b *Browser) Origin() *url.URL {
+ return b.History.URL()
}
func (b *Browser) Back() (e duit.Event) {
--- a/browser/experimental_test.go
+++ b/browser/experimental_test.go
@@ -40,7 +40,7 @@
doc, err := html.Parse(buf)
if err != nil { t.Fatalf(err.Error()) }
nt := nodes.NewNodeTree(doc, style.Map{}, make(map[*html.Node]style.Map), nil)
- d := domino.NewDomino(h, nt)
+ d := domino.NewDomino(h, nil, nt)
d.Start()
jq, err := ioutil.ReadFile("../domino/jquery-3.5.1.js")
if err != nil {
--- a/browser/website.go
+++ b/browser/website.go
@@ -120,7 +120,7 @@
log.Infof("Stop existing JS instance")
w.d.Stop()
}
- w.d = domino.NewDomino(w.html, nt)
+ w.d = domino.NewDomino(w.html, browser, nt)
w.d.Start()
jsProcessed, err := processJS2(w.d, codes)
if err == nil {
--- a/domino/domino.go
+++ b/domino/domino.go
@@ -1,8 +1,10 @@
package domino
import (
+ "errors"
"fmt"
"github.com/dop251/goja"
+ "github.com/dop251/goja/parser"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/require"
@@ -12,8 +14,12 @@
"github.com/psilva261/opossum"
"github.com/psilva261/opossum/logger"
"github.com/psilva261/opossum/nodes"
+ "net/http"
+ "os"
+ "path/filepath"
"strconv"
"strings"
+ "syscall"
"time"
)
@@ -26,6 +32,7 @@
}
type Domino struct {
+ fetcher opossum.Fetcher
loop *eventloop.EventLoop
html string
nt *nodes.Node
@@ -33,9 +40,10 @@
domChanged chan int
}
-func NewDomino(html string, nt *nodes.Node) (d *Domino) {
+func NewDomino(html string, fetcher opossum.Fetcher, nt *nodes.Node) (d *Domino) {
d = &Domino{
html: html,
+ fetcher: fetcher,
nt: nt,
}
return
@@ -100,6 +108,22 @@
log.Infof("js code: %v", code[:maxWidth])
}
+func srcLoader(fn string) ([]byte, error) {
+ path := filepath.FromSlash(fn)
+ if !strings.Contains(path, "/domino-lib/") || !strings.HasSuffix(path, ".js") {
+ return nil, require.ModuleFileDoesNotExistError
+ }
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) {
+ err = require.ModuleFileDoesNotExistError
+ } else {
+ log.Errorf("srcLoader: handling of require('%v') is not implemented", fn)
+ }
+ }
+ return data, err
+}
+
func (d *Domino) Exec(script string, initial bool) (res string, err error) {
script = strings.Replace(script, "const ", "var ", -1)
script = strings.Replace(script, "let ", "var ", -1)
@@ -152,6 +176,44 @@
userAgent: 'opossum'
};
HTMLElement = domino.impl.HTMLElement;
+
+ function XMLHttpRequest() {
+ var _method, _uri;
+ var h = {};
+ var ls = {};
+
+ this.readyState = 0;
+
+ var cb = function(data, err) {
+ if (data !== '') {
+ this.responseText = data;
+ this.readyState = 4;
+ this.state = 200;
+ this.status = 200;
+ if (ls['load']) ls['load'].bind(this)();
+ if (this.onload) this.onload.bind(this)();
+ if (this.onreadystatechange) this.onreadystatechange.bind(this)();
+ }
+ }.bind(this);
+
+ this.addEventListener = function(k, fn) {
+ ls[k] = fn;
+ };
+ this.open = function(method, uri) {
+ _method = method;
+ _uri = uri;
+ };
+ this.setRequestHeader = function(k, v) {
+ h[k] = v;
+ };
+ this.send = function(data) {
+ opossum.xhr(_method, _uri, h, data, cb);
+ this.readyState = 2;
+ };
+ this.getAllResponseHeaders = function() {
+ return '';
+ };
+ }
` + script
if !initial {
SCRIPT = script
@@ -169,6 +231,8 @@
log.Printf("RunOnLoop")
if initial {
+ vm.SetParserOptions(parser.WithDisableSourceMaps)
+
// find domino-lib folder
registry := require.NewRegistry(
require.WithGlobalFolders(
@@ -176,6 +240,9 @@
"..", // tests
"../..", // go run
),
+ require.WithLoader(
+ require.SourceLoader(srcLoader),
+ ),
)
console.Enable(vm)
@@ -186,6 +253,7 @@
HTML string `json:"html"`
Referrer func() string `json:"referrer"`
Style func(string, string, string, string) string `json:"style"`
+ XHR func(string, string, map[string]string, string, func(string, string)) `json:"xhr"`
}
vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
@@ -205,6 +273,7 @@
}
return res[0].Css(prop)
},
+ XHR: d.xhr,
})
}
@@ -421,6 +490,42 @@
f(doc)
return
+}
+
+func (d *Domino) xhr(method, uri string, h map[string]string, data string, cb func(data string, err string)) {
+ c := &http.Client{}
+ u, err := d.fetcher.LinkedUrl(uri)
+ if err != nil {
+ cb("", err.Error())
+ return
+ }
+ if u.Host != d.fetcher.Origin().Host {
+ log.Infof("origin: %v", d.fetcher.Origin())
+ log.Infof("uri: %v", uri)
+ cb("", "cannot do crossorigin request to " + u.String())
+ return
+ }
+ fmt.Printf("data=%+v\n", data)
+ req, err := http.NewRequest(method, u.String(), strings.NewReader(data))
+ if err != nil {
+ cb("", err.Error())
+ return
+ }
+ for k, v := range h {
+ req.Header.Add(k, v)
+ }
+ resp, err := c.Do(req)
+ if err != nil {
+ cb("", err.Error())
+ return
+ }
+ defer resp.Body.Close()
+ bs, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ cb("", err.Error())
+ return
+ }
+ cb(string(bs), "")
}
// AJAX:
--- a/domino/domino_test.go
+++ b/domino/domino_test.go
@@ -2,12 +2,15 @@
import (
"io/ioutil"
+ "github.com/psilva261/opossum"
"github.com/psilva261/opossum/logger"
"github.com/psilva261/opossum/nodes"
"github.com/psilva261/opossum/style"
"golang.org/x/net/html"
+ "net/url"
"strings"
"testing"
+ "time"
)
const simpleHTML = `
@@ -28,7 +31,7 @@
}
func TestSimple(t *testing.T) {
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
s := `
var state = 'empty';
@@ -56,7 +59,7 @@
}
func TestGlobals(t *testing.T) {
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
}
@@ -65,7 +68,7 @@
if err != nil {
t.Fatalf("%v", err)
}
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
$(document).ready(function() {
@@ -98,7 +101,7 @@
if err != nil {
t.Fatalf("%v", err)
}
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
$(document).ready(function() {
@@ -135,7 +138,7 @@
</body>
</html>
`
- d := NewDomino(h, nil)
+ d := NewDomino(h, nil, nil)
r := strings.NewReader(h)
doc, err := html.Parse(r)
if err != nil { t.Fatalf(err.Error()) }
@@ -161,7 +164,7 @@
if err != nil {
t.Fatalf("%v", err)
}
- d := NewDomino(string(buf), nil)
+ d := NewDomino(string(buf), nil, nil)
d.Start()
for i, fn := range []string{"initfuncs.js", "jquery-1.8.2.js", "goversion.js", "godocs.js"} {
buf, err := ioutil.ReadFile("godoc/"+fn)
@@ -181,7 +184,7 @@
if err != nil {
t.Fatalf("%v", err)
}
- d := NewDomino(string(buf), nil)
+ d := NewDomino(string(buf), nil, nil)
d.Start()
for i, fn := range []string{"initfuncs.js", "jquery-1.8.2.js", "playground.js", "goversion.js", "godocs.js", "golang.js"} {
buf, err := ioutil.ReadFile("godoc/"+fn)
@@ -212,7 +215,7 @@
if err != nil {
t.Fatalf("%v", err)
}
- d := NewDomino(string(buf), nil)
+ d := NewDomino(string(buf), nil, nil)
d.Start()
script := `
Object.assign(this, window);
@@ -258,7 +261,7 @@
//elem.dispatchEvent(event);
console.log(window.location.href);
`
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err = d.Exec(SCRIPT, true)
if err != nil {
@@ -288,7 +291,7 @@
});
});
`
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err = d.Exec(SCRIPT, true)
if err != nil {
@@ -353,7 +356,7 @@
//elem.dispatchEvent(event);
console.log(window.location.href);
`
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err = d.Exec(SCRIPT, true)
if err != nil {
@@ -377,7 +380,7 @@
}
func TestTrackChanges(t *testing.T) {
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
_, err := d.Exec(``, true)
if err != nil {
@@ -500,7 +503,7 @@
}*/
func TestES6(t *testing.T) {
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
console.log('Hello!!');
@@ -522,7 +525,7 @@
}
func TestWindowParent(t *testing.T) {
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
console.log('Hello!!')
@@ -543,7 +546,7 @@
}
func TestReferrer(t *testing.T) {
- d := NewDomino(simpleHTML, nil)
+ d := NewDomino(simpleHTML, nil, nil)
d.Start()
script := `
document.referrer;
@@ -556,5 +559,98 @@
if res != "https://example.com" {
t.Fatal()
}
+ d.Stop()
+}
+
+type MockBrowser struct {
+ origin *url.URL
+ linkedUrl *url.URL
+}
+
+func (mb *MockBrowser) LinkedUrl(string) (*url.URL, error) {
+ return mb.linkedUrl, nil
+}
+
+func (mb *MockBrowser) Origin() (*url.URL) {
+ return mb.origin
+}
+
+func (mb *MockBrowser) Get(*url.URL) (bs []byte, ct opossum.ContentType, err error) {
+ return
+}
+
+func TestXMLHttpRequest(t *testing.T) {
+ mb := &MockBrowser{}
+ mb.origin, _ = url.Parse("https://example.com")
+ mb.linkedUrl, _ = url.Parse("https://example.com")
+ d := NewDomino(simpleHTML, mb, nil)
+ d.Start()
+ script := `
+ var oReq = new XMLHttpRequest();
+ var loaded = false;
+ oReq.addEventListener("load", function() {
+ console.log('loaded!!!!! !!! 11!!!1!!elf!!!1!');
+ loaded = true;
+ });
+ console.log(oReq.open);
+ console.log('open:');
+ oReq.open("GET", "http://www.example.org/example.txt");
+ console.log('send:');
+ oReq.send();
+ console.log('return:');
+ `
+ _, err := d.Exec(script, true)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ <-time.After(time.Second)
+ res, err := d.Exec("oReq.responseText;", false)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ t.Logf("res=%v", res)
+ if !strings.Contains(res, "<html") {
+ t.Fatal()
+ }
+ d.Stop()
+}
+
+func TestJQueryAjax(t *testing.T) {
+ mb := &MockBrowser{}
+ mb.origin, _ = url.Parse("https://example.com")
+ mb.linkedUrl, _ = url.Parse("https://example.com")
+ buf, err := ioutil.ReadFile("jquery-3.5.1.js")
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ d := NewDomino(simpleHTML, mb, nil)
+ d.Start()
+ script := `
+ var res;
+ $.ajax({
+ url: '/',
+ success: function() {
+ console.log('success!!!');
+ res = 'success';
+ },
+ error: function() {
+ console.log('error!!!');
+ res = 'err';
+ }
+ });
+ `
+ _, err = d.Exec(string(buf) + ";" + script, true)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err = d.CloseDoc(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ <-time.After(time.Second)
+ res, err := d.Exec("res;", false)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ t.Logf("res=%v", res)
d.Stop()
}
--- a/opossum.go
+++ b/opossum.go
@@ -17,6 +17,8 @@
}
type Fetcher interface {
+ Origin() *url.URL
+
// LinkedUrl relative to current page
LinkedUrl(string) (*url.URL, error)
@@ -57,7 +59,7 @@
}
func (c ContentType) IsCSS() bool {
- return c.MediaType != "text/html"
+ return c.MediaType != "text/html"
}
func (c ContentType) IsJS() bool {
@@ -70,7 +72,7 @@
}
func (c ContentType) IsPlain() bool {
- return c.MediaType == "text/plain"
+ return c.MediaType == "text/plain"
}
func (c ContentType) IsDownload() bool {
@@ -79,7 +81,7 @@
}
func (c ContentType) IsSvg() bool {
- return c.MediaType == "image/svg+xml"
+ return c.MediaType == "image/svg+xml"
}
func (c ContentType) Utf8(buf []byte) []byte {