Developer SDK

JavaScript examples

Install the SDK · View source on GitHub ↗

integration/companion/main.go

// The native companion owns the connected DeviceLocal. JavaScript owns each
// application's codec, receive callback, peer selection, and acknowledgments.
package main

import (
	"context"
	"crypto/subtle"
	"errors"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gorilla/websocket"
	"github.com/urnetwork/sdk"
)

func allowedRequest(r *http.Request, token, origin string) bool {
	got := r.URL.Query().Get("token")
	return len(token) >= 32 && subtle.ConstantTimeCompare([]byte(got), []byte(token)) == 1 && (r.Header.Get("Origin") == "" || r.Header.Get("Origin") == origin)
}

func run() error {
	if len(os.Args) == 2 && os.Args[1] == "--version" {
		version := sdk.Version
		if version == "" {
			version = "development SDK"
		}
		fmt.Println("URnetwork native messaging companion", version)
		return nil
	}
	jwt, instance := os.Getenv("URNETWORK_CLIENT_JWT"), os.Getenv("URNETWORK_INSTANCE_ID")
	token := os.Getenv("URNETWORK_COMPANION_TOKEN")
	if jwt == "" || instance == "" || len(token) < 32 {
		return errors.New("set scoped URNETWORK_CLIENT_JWT, persistent URNETWORK_INSTANCE_ID, and a random URNETWORK_COMPANION_TOKEN of at least 32 characters")
	}
	id, err := sdk.ParseId(instance)
	if err != nil {
		return err
	}
	origin := os.Getenv("URNETWORK_COMPANION_ORIGIN")
	if origin != "" {
		u, err := url.Parse(origin)
		if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.Path != "" || u.RawQuery != "" || u.Fragment != "" || u.User != nil {
			return errors.New("URNETWORK_COMPANION_ORIGIN must be an exact http(s) origin without path")
		}
	}
	address := os.Getenv("URNETWORK_COMPANION_ADDRESS")
	if address == "" {
		address = "127.0.0.1:8787"
	}
	host, _, err := net.SplitHostPort(address)
	if err != nil || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() {
		return errors.New("URNETWORK_COMPANION_ADDRESS must bind a numeric loopback address")
	}
	listener, err := net.Listen("tcp", address)
	if err != nil {
		return err
	}
	defer listener.Close()
	ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()
	manager := sdk.NewNetworkSpaceManagerNoStorage()
	defer manager.Close()
	space := manager.UpdateNetworkSpaceValues(sdk.NewNetworkSpaceKey("ur.network", "main"), &sdk.NetworkSpaceValues{MigrationHostName: "bringyour.com"})
	space.GetApi().SetByJwt(jwt)
	device, err := sdk.NewDeviceLocalWithDefaults(space, jwt, "JavaScript messaging companion", "node-companion", "1", id, false)
	if err != nil {
		return err
	}
	defer device.Close()
	device.SetProvideMode(sdk.ProvideModeNetwork)
	readyDeadline := time.Now().Add(30 * time.Second)
	for !device.GetProviderConnected() {
		if time.Now().After(readyDeadline) {
			return errors.New("native provider did not connect within 30 seconds")
		}
		select {
		case <-ctx.Done():
			return nil
		case <-time.After(50 * time.Millisecond):
		}
	}
	rpc := sdk.NewHostedDeviceRpcListener(ctx)
	defer rpc.Close()
	// This transport entry point disables remote route/provide setters. The
	// underlying local device retains its ordinary full provider identity.
	device.StartHostedRpc(rpc, sdk.NewId().String())
	active := make(chan struct{}, 1)
	upgrader := websocket.Upgrader{HandshakeTimeout: 10 * time.Second, CheckOrigin: func(r *http.Request) bool { return allowedRequest(r, token, origin) }}
	mux := http.NewServeMux()
	mux.HandleFunc("/device-rpc", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet || !allowedRequest(r, token, origin) {
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		select {
		case active <- struct{}{}:
			defer func() { <-active }()
		default:
			http.Error(w, "companion already in use", http.StatusConflict)
			return
		}
		ws, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			return
		}
		defer ws.Close()
		_ = rpc.ServeWs(ws)
	})
	server := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 30 * time.Second}
	go func() {
		<-ctx.Done()
		_ = rpc.Close()
		stop, done := context.WithTimeout(context.Background(), 3*time.Second)
		defer done()
		_ = server.Shutdown(stop)
	}()
	fmt.Printf("Companion for client %s, instance %s listening at ws://%s/device-rpc\n", device.GetClientId(), id, listener.Addr())
	err = server.Serve(listener)
	if errors.Is(err, http.ErrServerClosed) {
		return nil
	}
	return err
}

func main() {
	if err := run(); err != nil {
		log.Print(err)
		os.Exit(1)
	}
}