Skip to content

Commit

Permalink
Use .localhost prefixes for different origins
Browse files Browse the repository at this point in the history
  • Loading branch information
kegsay committed Jul 8, 2024
1 parent a72753e commit a41e33a
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 25 deletions.
28 changes: 28 additions & 0 deletions internal/api/js/chrome/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
var (
browserInstance *Browser
browserInstanceMu = &sync.Mutex{}
origins *Origins
)

// GlobalBrowser returns the browser singleton, making it if needed.
Expand All @@ -28,6 +29,7 @@ func GlobalBrowser() (*Browser, error) {
if browserInstance == nil {
var err error
browserInstance, err = NewBrowser()
origins = NewOrigins()
return browserInstance, err
}
return browserInstance, nil
Expand Down Expand Up @@ -106,3 +108,29 @@ func (b *Browser) NewTab(baseJSURL string, onConsoleLog func(s string)) (*Tab, e
cancel: closeTab,
}, nil
}

// For clients which want persistent storage, we need to ensure when the browser
// starts up a 2nd+ time we serve the same URL so the browser uses the same origin
type Origins struct {
clientToBaseURL map[string]string
mu *sync.RWMutex
}

func NewOrigins() *Origins {
return &Origins{
clientToBaseURL: make(map[string]string),
mu: &sync.RWMutex{},
}
}

func (o *Origins) StoreBaseURL(userID, deviceID, baseURL string) {
o.mu.Lock()
defer o.mu.Unlock()
o.clientToBaseURL[userID+deviceID] = baseURL
}

func (o *Origins) GetBaseURL(userID, deviceID string) (baseURL string) {
o.mu.RLock()
defer o.mu.RUnlock()
return o.clientToBaseURL[userID+deviceID]
}
14 changes: 10 additions & 4 deletions internal/api/js/chrome/chrome.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,21 @@ func MustRunAsyncFn[T any](t ct.TestLike, ctx context.Context, js string) *T {
return result
}

func RunHeadless(onConsoleLog func(s string), listenPort int) (*Tab, error) {
// Run a headless JS SDK instance for the given user/device ID.
func RunHeadless(userID, deviceID string, onConsoleLog func(s string)) (*Tab, error) {
// make a Chrome browser
browser, err := GlobalBrowser()
if err != nil {
return nil, fmt.Errorf("GlobalBrowser: %s", err)
}

// Host the JS SDK
baseJSURL, closeSDKInstance, err := NewJSSDKWebsite(JSSDKInstanceOpts{
Port: listenPort,
})
baseURL := origins.GetBaseURL(userID, deviceID)
opts, err := NewJSSDKInstanceOptsFromURL(baseURL, userID, deviceID)
if err != nil {
return nil, fmt.Errorf("NewJSSDKInstanceOptsFromURL: %v", err)
}
baseJSURL, closeSDKInstance, err := NewJSSDKWebsite(opts)
if err != nil {
return nil, fmt.Errorf("failed to create new js sdk instance: %s", err)
}
Expand All @@ -73,6 +77,8 @@ func RunHeadless(onConsoleLog func(s string), listenPort int) (*Tab, error) {
closeSDKInstance()
return nil, fmt.Errorf("failed to create new tab: %s", err)
}
// we will have a random high numbered port now, so remember it.
origins.StoreBaseURL(userID, deviceID, baseJSURL)

// when we close the tab, close the hosted files too
tab.SetCloseServer(closeSDKInstance)
Expand Down
46 changes: 43 additions & 3 deletions internal/api/js/chrome/tab.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,60 @@ import (
"io/fs"
"net"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"sync"
)

//go:embed dist
var jsSDKDistDirectory embed.FS

var nonAlphanumericRegex = regexp.MustCompile(`[^a-zA-Z0-9]+`)

type JSSDKInstanceOpts struct {
// The specific port this instance should be hosted on.
// Required. The prefix to use when constructing base URLs.
// This is used to namespace storage between tests by prefixing the URL e.g
// 'foo.localhost:12345' vs 'bar.localhost:12345'. We cannot simply use the
// port number alone because it is randomly allocated, which means the port
// number can be reused. If this happens, the 2nd+ test with the same port
// number will fail with:
// 'Error: the account in the store doesn't match the account in the constructor'
// By prefixing, we ensure we are treated as a different origin whilst keeping
// routing the same.
HostPrefix string
// Optional. The specific port this instance should be hosted on.
// If 0, uses a random high numbered port.
// This is crucial for persistent storage which relies on a stable port number
// across restarts.
Port int
}

// NewJSSDKInstanceOptsFromURL returns SDK options based on a pre-existing base URL. If the
// base URL doesn't exist yet, create the information from the provided user/device ID.
func NewJSSDKInstanceOptsFromURL(baseURL, userID, deviceID string) (*JSSDKInstanceOpts, error) {
if baseURL == "" {
return &JSSDKInstanceOpts{
HostPrefix: nonAlphanumericRegex.ReplaceAllString(userID+deviceID, ""),
}, nil
}
u, _ := url.Parse(baseURL)
portStr := u.Port()
port, err := strconv.Atoi(portStr)
if portStr == "" || err != nil {
return nil, fmt.Errorf("failed to extract port from base url %s", baseURL)
}

return &JSSDKInstanceOpts{
HostPrefix: strings.Split(u.Hostname(), ".")[0],
Port: port,
}, nil
}

// NewJSSDKWebsite hosts the JS SDK HTML/JS on a random high-numbered port
// and runs a Go web server to serve those files.
func NewJSSDKWebsite(opts JSSDKInstanceOpts) (baseURL string, close func(), err error) {
func NewJSSDKWebsite(opts *JSSDKInstanceOpts) (baseURL string, close func(), err error) {
// strip /dist so /index.html loads correctly as does /assets/xxx.js
c, err := fs.Sub(jsSDKDistDirectory, "dist")
if err != nil {
Expand All @@ -42,7 +80,9 @@ func NewJSSDKWebsite(opts JSSDKInstanceOpts) (baseURL string, close func(), err
if err != nil {
panic(err)
}
baseURL = "http://" + ln.Addr().String()
baseURL = fmt.Sprintf(
"http://%s.localhost:%v", opts.HostPrefix, ln.Addr().(*net.TCPAddr).Port,
)
fmt.Println("JS SDK listening on", baseURL)
wg.Done()
srv.Serve(ln)
Expand Down
20 changes: 2 additions & 18 deletions internal/api/js/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package js
import (
"encoding/json"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
Expand All @@ -23,10 +21,6 @@ const (
indexedDBCryptoName = "complement-crypto:crypto"
)

// For clients which want persistent storage, we need to ensure when the browser
// starts up a 2nd+ time we serve the same URL so the browser uses the same origin
var userDeviceToPort = map[string]int{}

var logFile *os.File

func SetupJSLogs(filename string) {
Expand Down Expand Up @@ -69,8 +63,7 @@ func NewJSClient(t ct.TestLike, opts api.ClientCreationOpts) (api.Client, error)
opts: opts,
verificationChannelMu: &sync.Mutex{},
}
portKey := opts.UserID + opts.DeviceID
tab, err := chrome.RunHeadless(func(s string) {
tab, err := chrome.RunHeadless(opts.UserID, opts.DeviceID, func(s string) {
writeToLog("[%s,%s] console.log %s\n", opts.UserID, opts.DeviceID, s)

msg := unpackControlMessage(t, s)
Expand All @@ -86,7 +79,7 @@ func NewJSClient(t ct.TestLike, opts api.ClientCreationOpts) (api.Client, error)
for _, l := range listeners {
l(msg)
}
}, userDeviceToPort[portKey])
})
if err != nil {
return nil, fmt.Errorf("failed to RunHeadless: %s", err)
}
Expand Down Expand Up @@ -117,15 +110,6 @@ func NewJSClient(t ct.TestLike, opts api.ClientCreationOpts) (api.Client, error)
await window.__store.startup();
`, indexedDBName))
store = "window.__store"
//cryptoStore = fmt.Sprintf(`new IndexedDBCryptoStore(indexedDB, "%s")`, indexedDBCryptoName)
// remember the port for same-origin to remember the store
u, _ := url.Parse(tab.BaseURL)
portStr := u.Port()
port, err := strconv.Atoi(portStr)
if portStr == "" || err != nil {
ct.Fatalf(t, "failed to extract port from base url %s", tab.BaseURL)
}
userDeviceToPort[portKey] = port
t.Logf("user=%s device=%s will be served from %s due to persistent storage", opts.UserID, opts.DeviceID, tab.BaseURL)
}

Expand Down

0 comments on commit a41e33a

Please sign in to comment.