├── .dockerignore ├── .github └── workflows │ ├── make.yaml │ ├── mirror.yaml │ └── yarn.yaml ├── .gitignore ├── .proxyrc ├── @types ├── asyncify-wasm.d.ts ├── wasmer__wasm-transformer.d.ts └── wrtc.d.ts ├── CODE_OF_CONDUCT.md ├── Dockerfile.unisockets-runner ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── cmd ├── c_echo_client │ ├── main.c │ └── unisockets.h ├── c_echo_server │ ├── main.c │ └── unisockets.h ├── go_echo_client │ └── main.go ├── go_echo_server │ └── main.go ├── unisockets_runner │ └── main.ts └── unisockets_runner_web │ ├── index.html │ └── main.ts ├── go.mod ├── index.ts ├── package.json ├── pkg ├── unisockets │ ├── unisockets.go │ ├── unisockets.h │ ├── unisockets_native_posix_go.go │ ├── unisockets_native_posix_tinygo.go │ ├── unisockets_tinygo.go │ ├── unisockets_wasm_jssi.go │ └── unisockets_wasm_wasi_tinygo.go └── web │ ├── signaling │ ├── errors │ │ ├── alias-does-not-exist.ts │ │ ├── bind-rejected.ts │ │ ├── channel-does-not-exist.ts │ │ ├── client-closed.ts │ │ ├── client-does-not-exist.ts │ │ ├── connection-does-not-exist.ts │ │ ├── connection-rejected.ts │ │ ├── memory-does-not-exist.ts │ │ ├── port-already-allocated-error.ts │ │ ├── sdp-invalid.ts │ │ ├── shutdown-rejected.ts │ │ ├── socket-does-not-exist.ts │ │ ├── subnet-does-not-exist.ts │ │ ├── suffix-does-not-exist.ts │ │ └── unimplemented-operation.ts │ ├── models │ │ ├── alias.ts │ │ └── member.ts │ ├── operations │ │ ├── accept.ts │ │ ├── accepting.ts │ │ ├── acknowledgement.ts │ │ ├── alias.ts │ │ ├── answer.ts │ │ ├── bind.ts │ │ ├── candidate.ts │ │ ├── connect.ts │ │ ├── goodbye.ts │ │ ├── greeting.ts │ │ ├── knock.ts │ │ ├── offer.ts │ │ ├── operation.ts │ │ └── shutdown.ts │ └── services │ │ ├── signaling-client.ts │ │ ├── signaling-server.test.ts │ │ ├── signaling-server.ts │ │ └── signaling-service.ts │ ├── sockets │ └── sockets.ts │ ├── transport │ └── transporter.ts │ └── utils │ ├── getAsBinary.ts │ ├── htons.ts │ └── logger.ts ├── protocol.puml ├── rollup.config.js ├── tsconfig.json ├── vendor ├── go │ └── wasm_exec.js └── tinygo │ └── wasm_exec.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .parcel-cache 4 | out 5 | -------------------------------------------------------------------------------- /.github/workflows/make.yaml: -------------------------------------------------------------------------------- 1 | name: make CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | make: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Build with make 12 | run: make -j$(nproc) 13 | -------------------------------------------------------------------------------- /.github/workflows/mirror.yaml: -------------------------------------------------------------------------------- 1 | name: Mirror 2 | 3 | on: [push] 4 | 5 | jobs: 6 | mirror: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: "0" 13 | - uses: spyoungtech/mirror-action@master 14 | with: 15 | REMOTE: "https://gitlab.mi.hdm-stuttgart.de/fp036/unisockets.git" 16 | GIT_USERNAME: ${{ secrets.GIT_USERNAME }} 17 | GIT_PASSWORD: ${{ secrets.GIT_PASSWORD }} 18 | -------------------------------------------------------------------------------- /.github/workflows/yarn.yaml: -------------------------------------------------------------------------------- 1 | name: Yarn CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | yarn: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2.3.1 12 | with: 13 | persist-credentials: false 14 | - name: Setup node 15 | uses: actions/setup-node@v2-beta 16 | with: 17 | node-version: "14" 18 | registry-url: "https://registry.npmjs.org" 19 | - name: Install dependencies with Yarn 20 | run: yarn 21 | - name: Build with make 22 | run: make -j$(nproc) 23 | - name: Build with Yarn 24 | run: yarn build 25 | - name: Install node-plantuml 26 | run: npm install -g node-plantuml 27 | - name: Build docs with Yarn 28 | run: yarn build:diagram && yarn build:protocol && yarn build:docs 29 | - name: Test with Yarn 30 | run: yarn test 31 | - name: Publish to npm 32 | if: ${{ github.ref == 'refs/heads/main' }} 33 | uses: pascalgn/npm-publish-action@1.3.5 34 | with: 35 | publish_command: "yarn" 36 | commit_pattern: '.*\:\ Release (\S+)' 37 | publish_args: "--access public" 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 41 | - name: Publish to GitHub pages 42 | if: ${{ github.ref == 'refs/heads/main' }} 43 | uses: JamesIves/github-pages-deploy-action@3.7.1 44 | with: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | BRANCH: gh-pages 47 | FOLDER: dist/docs 48 | CLEAN: true 49 | GIT_CONFIG_NAME: bot 50 | GIT_CONFIG_EMAIL: bot@example.com 51 | SINGLE_COMMIT: true 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | dist 4 | .parcel-cache 5 | out 6 | -------------------------------------------------------------------------------- /.proxyrc: -------------------------------------------------------------------------------- 1 | { 2 | "/out": { 3 | "target": "http://localhost:5000/" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /@types/asyncify-wasm.d.ts: -------------------------------------------------------------------------------- 1 | declare module "asyncify-wasm" { 2 | declare async function instantiate( 3 | binary: WebAssembly.Module, 4 | imports: any 5 | ): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /@types/wasmer__wasm-transformer.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@wasmer/wasm-transformer/lib/unoptimized/wasm-transformer.esm.js"; 2 | -------------------------------------------------------------------------------- /@types/wrtc.d.ts: -------------------------------------------------------------------------------- 1 | // Track https://github.com/node-webrtc/node-webrtc/pull/656 for when we can remove this 2 | 3 | /// 4 | 5 | declare module "wrtc" { 6 | export declare var MediaStream: { 7 | prototype: MediaStream; 8 | new (): MediaStream; 9 | new (stream: MediaStream): MediaStream; 10 | new (tracks: MediaStreamTrack[]): MediaStream; 11 | new ({ id }: { id: string }): MediaStream; 12 | }; 13 | 14 | export declare var MediaStreamTrack: { 15 | prototype: MediaStreamTrack; 16 | new (): MediaStreamTrack; 17 | }; 18 | 19 | export declare var RTCDataChannel: { 20 | prototype: RTCDataChannel; 21 | new (): RTCDataChannel; 22 | }; 23 | 24 | export declare var RTCDataChannelEvent: { 25 | prototype: RTCDataChannelEvent; 26 | new ( 27 | type: string, 28 | eventInitDict: RTCDataChannelEventInit 29 | ): RTCDataChannelEvent; 30 | }; 31 | 32 | export declare var RTCDtlsTransport: { 33 | prototype: RTCDtlsTransport; 34 | new (): RTCDtlsTransport; 35 | }; 36 | 37 | export declare var RTCIceCandidate: { 38 | prototype: RTCIceCandidate; 39 | new (candidateInitDict?: RTCIceCandidateInit): RTCIceCandidate; 40 | }; 41 | 42 | export declare var RTCIceTransport: { 43 | prototype: RTCIceTransport; 44 | new (): RTCIceTransport; 45 | }; 46 | 47 | export type ExtendedRTCConfiguration = RTCConfiguration & { 48 | portRange?: { 49 | min: number; 50 | max: number; 51 | }; 52 | sdpSemantics?: "plan-b" | "unified-plan"; 53 | }; 54 | 55 | export declare var RTCPeerConnection: { 56 | prototype: RTCPeerConnection; 57 | new (configuration?: ExtendedRTCConfiguration): RTCPeerConnection; 58 | generateCertificate( 59 | keygenAlgorithm: AlgorithmIdentifier 60 | ): Promise; 61 | getDefaultIceServers(): RTCIceServer[]; 62 | }; 63 | 64 | export declare var RTCPeerConnectionIceEvent: { 65 | prototype: RTCPeerConnectionIceEvent; 66 | new ( 67 | type: string, 68 | eventInitDict?: RTCPeerConnectionIceEventInit 69 | ): RTCPeerConnectionIceEvent; 70 | }; 71 | 72 | export declare var RTCRtpReceiver: { 73 | prototype: RTCRtpReceiver; 74 | new (): RTCRtpReceiver; 75 | getCapabilities(kind: string): RTCRtpCapabilities | null; 76 | }; 77 | 78 | export declare var RTCRtpSender: { 79 | prototype: RTCRtpSender; 80 | new (): RTCRtpSender; 81 | getCapabilities(kind: string): RTCRtpCapabilities | null; 82 | }; 83 | 84 | export declare var RTCRtpTransceiver: { 85 | prototype: RTCRtpTransceiver; 86 | new (): RTCRtpTransceiver; 87 | }; 88 | 89 | export declare var RTCSctpTransport: { 90 | prototype: RTCSctpTransport; 91 | new (): RTCSctpTransport; 92 | }; 93 | 94 | export declare var RTCSessionDescription: { 95 | prototype: RTCSessionDescription; 96 | new ( 97 | descriptionInitDict?: RTCSessionDescriptionInit 98 | ): RTCSessionDescription; 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at felicitas@pojtinger.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /Dockerfile.unisockets-runner: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | WORKDIR /opt 4 | 5 | COPY package.json package.json 6 | COPY yarn.lock yarn.lock 7 | 8 | RUN yarn 9 | 10 | COPY . . 11 | 12 | RUN yarn build:app:node 13 | 14 | RUN npm i -g . 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # All 2 | all: build 3 | 4 | # Build 5 | build: \ 6 | build-unisockets-runner \ 7 | build-server-native-posix-c \ 8 | build-server-native-posix-go \ 9 | build-server-native-posix-tinygo \ 10 | build-server-wasm-wasi-c \ 11 | build-server-wasm-jssi-go \ 12 | build-server-wasm-wasi-tinygo \ 13 | build-server-wasm-jssi-tinygo \ 14 | build-client-native-posix-c \ 15 | build-client-native-posix-go \ 16 | build-client-native-posix-tinygo \ 17 | build-client-wasm-wasi-c \ 18 | build-client-wasm-jssi-go \ 19 | build-client-wasm-wasi-tinygo \ 20 | build-client-wasm-jssi-tinygo 21 | 22 | build-unisockets-runner: 23 | @docker build -t alphahorizonio/unisockets-runner -f Dockerfile.unisockets-runner . 24 | 25 | build-server-native-posix-c: 26 | @docker run -v ${PWD}:/src:z silkeh/clang sh -c 'cd /src && mkdir -p out/c && clang ./cmd/c_echo_server/main.c -o out/c/echo_server' 27 | build-server-native-posix-go: 28 | @docker run -v ${PWD}:/src:z golang sh -c 'cd /src && go build -o out/go/echo_server ./cmd/go_echo_server/main.go' 29 | build-server-native-posix-tinygo: 30 | @docker run -v ${PWD}:/src:z tinygo/tinygo sh -c 'cd /src && mkdir -p out/tinygo && tinygo build -o out/tinygo/echo_server ./cmd/go_echo_server/main.go' 31 | build-server-wasm-wasi-c: 32 | @docker run -v ${PWD}:/src:z alphahorizonio/wasi-sdk sh -c 'cd /src && mkdir -p out/c && clang -Wl,--allow-undefined -DUNISOCKETS_WITH_ALIAS --sysroot=/opt/wasi-sdk-12.0/share/wasi-sysroot cmd/c_echo_server/main.c -o out/c/echo_server_original.wasm && wasm-opt --asyncify -O out/c/echo_server_original.wasm -o out/c/echo_server.wasm' 33 | build-server-wasm-jssi-go: 34 | @docker run -v ${PWD}:/src:z -e GOOS=js -e GOARCH=wasm golang sh -c 'cd /src && go build -o out/go/echo_server.wasm ./cmd/go_echo_server/main.go' 35 | build-server-wasm-wasi-tinygo: 36 | @docker run -v ${PWD}:/src:z tinygo/tinygo sh -c 'cd /src && mkdir -p out/tinygo && tinygo build -heap-size 20M -cflags "-DUNISOCKETS_WITH_CUSTOM_ARPA_INET" -target wasi -o out/tinygo/echo_server_wasi_original.wasm ./cmd/go_echo_server/main.go' 37 | @docker run -v ${PWD}:/src:z alphahorizonio/wasi-sdk sh -c 'cd /src && wasm-opt --asyncify -O out/tinygo/echo_server_wasi_original.wasm -o out/tinygo/echo_server_wasi.wasm' 38 | build-server-wasm-jssi-tinygo: 39 | @docker run -v ${PWD}:/src:z tinygo/tinygo sh -c 'cd /src && mkdir -p out/tinygo && tinygo build -heap-size 20M -cflags "-DUNISOCKETS_WITH_CUSTOM_ARPA_INET" -target wasm -o out/tinygo/echo_server_jssi.wasm ./cmd/go_echo_server/main.go' 40 | 41 | build-client-native-posix-c: 42 | @docker run -v ${PWD}:/src:z silkeh/clang sh -c 'cd /src && mkdir -p out/c && clang ./cmd/c_echo_client/main.c -o out/c/echo_client' 43 | build-client-native-posix-go: 44 | @docker run -v ${PWD}:/src:z golang sh -c 'cd /src && go build -o out/go/echo_client ./cmd/go_echo_client/main.go' 45 | build-client-native-posix-tinygo: 46 | @docker run -v ${PWD}:/src:z tinygo/tinygo sh -c 'cd /src && mkdir -p out/tinygo && tinygo build -o out/tinygo/echo_client ./cmd/go_echo_client/main.go' 47 | build-client-wasm-wasi-c: 48 | @docker run -v ${PWD}:/src:z alphahorizonio/wasi-sdk sh -c 'cd /src && mkdir -p out/c && clang -Wl,--allow-undefined -DUNISOCKETS_WITH_ALIAS --sysroot=/opt/wasi-sdk-12.0/share/wasi-sysroot cmd/c_echo_client/main.c -o out/c/echo_client_original.wasm && wasm-opt --asyncify -O out/c/echo_client_original.wasm -o out/c/echo_client.wasm' 49 | build-client-wasm-jssi-go: 50 | @docker run -v ${PWD}:/src:z -e GOOS=js -e GOARCH=wasm golang sh -c 'cd /src && go build -o out/go/echo_client.wasm ./cmd/go_echo_client/main.go' 51 | build-client-wasm-wasi-tinygo: 52 | @docker run -v ${PWD}:/src:z tinygo/tinygo sh -c 'cd /src && mkdir -p out/tinygo && tinygo build -heap-size 20M -cflags "-DUNISOCKETS_WITH_CUSTOM_ARPA_INET" -target wasi -o out/tinygo/echo_client_wasi_original.wasm ./cmd/go_echo_client/main.go' 53 | @docker run -v ${PWD}:/src:z alphahorizonio/wasi-sdk sh -c 'cd /src && wasm-opt --asyncify -O out/tinygo/echo_client_wasi_original.wasm -o out/tinygo/echo_client_wasi.wasm' 54 | build-client-wasm-jssi-tinygo: 55 | @docker run -v ${PWD}:/src:z tinygo/tinygo sh -c 'cd /src && mkdir -p out/tinygo && tinygo build -heap-size 20M -cflags "-DUNISOCKETS_WITH_CUSTOM_ARPA_INET" -target wasm -o out/tinygo/echo_client_jssi.wasm ./cmd/go_echo_client/main.go' 56 | 57 | # Clean 58 | clean: \ 59 | clean-server-native-posix-c \ 60 | clean-server-native-posix-go \ 61 | clean-server-native-posix-tinygo \ 62 | clean-server-wasm-wasi-c \ 63 | clean-server-wasm-jssi-go \ 64 | clean-server-wasm-wasi-tinygo \ 65 | clean-server-wasm-jssi-tinygo \ 66 | clean-client-native-posix-c \ 67 | clean-client-native-posix-go \ 68 | clean-client-native-posix-tinygo \ 69 | clean-client-wasm-wasi-c \ 70 | clean-client-wasm-jssi-go \ 71 | clean-client-wasm-wasi-tinygo \ 72 | clean-client-wasm-jssi-tinygo 73 | 74 | clean-server-native-posix-c: 75 | @rm -f out/c/echo_server 76 | clean-server-native-posix-go: 77 | @rm -f out/go/echo_server 78 | clean-server-native-posix-tinygo: 79 | @rm -f out/tinygo/echo_server 80 | clean-server-wasm-wasi-c: 81 | @rm -f out/c/echo_server_original.wasm 82 | @rm -f out/c/echo_server.wasm 83 | clean-server-wasm-jssi-go: 84 | @rm -f out/go/echo_server.wasm 85 | clean-server-wasm-wasi-tinygo: 86 | @rm -f out/tinygo/echo_server_wasi_original.wasm 87 | @rm -f out/tinygo/echo_server_wasi.wasm 88 | clean-server-wasm-jssi-tinygo: 89 | @rm -f out/tinygo/echo_server_jssi.wasm 90 | 91 | clean-client-native-posix-c: 92 | @rm -f out/c/echo_client 93 | clean-client-native-posix-go: 94 | @rm -f out/go/echo_client 95 | clean-client-native-posix-tinygo: 96 | @rm -f out/tinygo/echo_client 97 | clean-client-wasm-wasi-c: 98 | @rm -f out/c/echo_client_original.wasm 99 | @rm -f out/c/echo_client.wasm 100 | clean-client-wasm-jssi-go: 101 | @rm -f out/go/echo_client.wasm 102 | clean-client-wasm-wasi-tinygo: 103 | @rm -f out/tinygo/echo_client_wasi_original.wasm 104 | @rm -f out/tinygo/echo_client_wasi.wasm 105 | clean-client-wasm-jssi-tinygo: 106 | @rm -f out/tinygo/echo_client_jssi.wasm 107 | 108 | # Run 109 | run: \ 110 | run-signaling-server \ 111 | run-server-native-posix-c \ 112 | run-server-native-posix-go \ 113 | run-server-native-posix-tinygo \ 114 | run-server-wasm-wasi-c \ 115 | run-server-wasm-jssi-go \ 116 | run-server-wasm-wasi-tinygo \ 117 | run-server-wasm-jssi-tinygo \ 118 | run-client-native-posix-c \ 119 | run-client-native-posix-go \ 120 | run-client-native-posix-tinygo \ 121 | run-client-wasm-wasi-c \ 122 | run-client-wasm-jssi-go \ 123 | run-client-wasm-wasi-tinygo \ 124 | run-client-wasm-jssi-tinygo 125 | 126 | run-signaling-server: build-unisockets-runner 127 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runSignalingServer true' 128 | 129 | run-server-native-posix-c: 130 | @./out/c/echo_server 131 | run-server-native-posix-go: 132 | @./out/go/echo_server 133 | run-server-native-posix-tinygo: 134 | @./out/tinygo/echo_server 135 | run-server-wasm-wasi-c: 136 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useC true --useWASI true --binaryPath ./out/c/echo_server.wasm' 137 | run-server-wasm-jssi-go: 138 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useGo true --useJSSI true --binaryPath ./out/go/echo_server.wasm' 139 | run-server-wasm-wasi-tiny: 140 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useTinyGo true --useWASI true --binaryPath ./out/tinygo/echo_server_wasi.wasm' 141 | run-server-wasm-jssi-tinygo: 142 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useTinyGo true --useJSSI true --binaryPath ./out/tinygo/echo_server_wasi.wasm' 143 | 144 | run-client-native-posix-c: 145 | @./out/c/echo_client 146 | run-client-native-posix-go: 147 | @./out/go/echo_client 148 | run-client-native-posix-tinygo: 149 | @./out/tinygo/echo_client 150 | run-client-wasm-wasi-c: 151 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useC true --useWASI true --binaryPath ./out/c/echo_client.wasm' 152 | run-client-wasm-jssi-go: 153 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useGo true --useJSSI true --binaryPath ./out/go/echo_client.wasm' 154 | run-client-wasm-wasi-tiny: 155 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useTinyGo true --useWASI true --binaryPath ./out/tinygo/echo_client_wasi.wasm' 156 | run-client-wasm-jssi-tinygo: 157 | @docker run --net host -v ${PWD}:/src:z alphahorizonio/unisockets-runner sh -c 'cd /src && unisockets_runner --runBinary true --useTinyGo true --useJSSI true --binaryPath ./out/tinygo/echo_client_wasi.wasm' 158 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unisockets 2 | 3 | A universal Berkeley sockets implementation for both WebAssembly (based on WebRTC) and native platforms with bindings for C, Go and TinyGo. 4 | 5 | ![Yarn CI](https://github.com/alphahorizonio/unisockets/workflows/Yarn%20CI/badge.svg) 6 | ![make CI](https://github.com/alphahorizonio/unisockets/workflows/make%20CI/badge.svg) 7 | ![Mirror](https://github.com/alphahorizonio/unisockets/workflows/Mirror/badge.svg) 8 | [![TypeDoc](https://img.shields.io/badge/TypeScript-Documentation-informational)](https://alphahorizonio.github.io/unisockets/) 9 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/alphahorizonio/unisockets)](https://pkg.go.dev/github.com/alphahorizonio/unisockets) 10 | [![npm](https://img.shields.io/npm/v/@alphahorizonio/unisockets)](https://www.npmjs.com/package/@alphahorizonio/unisockets) 11 | [![Minimal Demo](https://img.shields.io/badge/Minimal%20Demo-unisockets.vercel.app-blueviolet)](https://unisockets.vercel.app/) 12 | [![Lite (webnetes) Demo]()](https://lite.webnetes.dev/) 13 | [![Full (webnetes) Demo]()](https://webnetes.dev/) 14 | [![Part of webnetes](https://img.shields.io/badge/Part%20of-webnetes-black)](https://webnetes.dev/) 15 | 16 | ## Overview 17 | 18 | unisockets implements the [Berkeley sockets API](https://en.wikipedia.org/wiki/Berkeley_sockets). On a native environment like Linux, it falls back to the native Berkeley sockets API; on WASM it uses [WebRTC](https://webrtc.org/) for fast peer-to-peer communication instead of the (non-available) native API. This allows you to "just recompile" an existing socket server/client (such as a web server etc.) and run it natively, in a WebAssembly runtime or in the browser, without the need for a [WebSocket proxy like in emscripten](https://emscripten.org/docs/porting/networking.html) or some other proxy mechanism. You've heard that right, this library allows you to `bind` in the browser! 19 | 20 | ### Components 21 | 22 | [![UML Diagram](https://alphahorizonio.github.io/unisockets/media/diagram.svg)](https://alphahorizonio.github.io/unisockets/media/diagram.svg) 23 | 24 | The system is made up of the following components: 25 | 26 | - **Signaling**: A WebRTC signaling server (with two implementations), client and protocol has been implemented to allow for nodes to discover each other and exchange candidates, but is not involved in any actual connections. When compiling natively, it is not required. 27 | - **Transport**: A minimal wrapper around the WebRTC API. When compiling to WASM, this component manages all actual data transfers and handles incoming/outgoing peer to peer connections. When compiling natively, it is not required. 28 | - **Sockets**: A set of WebAssembly imports that satisfy the basic API of the Berkeley sockets, such as `socket`, `bind`, `listen`, `accept`, `connect`, `send`, `recv` etc. When compiling natively, it falls back to the native implementation. 29 | 30 | These components have no hard dependencies on one another, and can be used independendly. 31 | 32 | Additionally, a [universal C/C++ header](https://github.com/alphahorizonio/unisockets/blob/main/cmd/c_echo_client/unisockets.h) for easy usage and Go/TinyGo bindings (see [![PkgGoDev](https://pkg.go.dev/badge/github.com/alphahorizonio/unisockets/pkg/unisockets)](https://pkg.go.dev/github.com/alphahorizonio/unisockets/pkg/unisockets)) have been created. 33 | 34 | ### Signaling Protocol 35 | 36 | The signaling components use the following protocol: 37 | 38 | [![Sequence Diagram](https://alphahorizonio.github.io/unisockets/media/sequence.svg)](https://alphahorizonio.github.io/unisockets/media/sequence.svg) 39 | 40 | There are two implementations of the signaling server. The TypeScript version is maintained is this repo, the Java version can be found in [junisockets](https://github.com/alphahorizonio/junisockets). 41 | 42 | Public signaling server instances: 43 | 44 | | Implementation | URL | 45 | | -------------- | ------------------------------ | 46 | | TypeScript | `wss://signaler.webnetes.dev` | 47 | | Java | `wss://jsignaler.webnetes.dev` | 48 | 49 | ### Further Resources 50 | 51 | Interested in an implementation of the [Go `net` package](https://golang.org/pkg/net/) based on this package, with TinyGo and WASM support? You might be interested in [tinynet](https://github.com/alphahorizonio/tinynet)! 52 | 53 | You want a Kubernetes-style system for WASM, running in the browser and in node? You might be interested in [webnetes](https://github.com/alphahorizonio/webnetes), which uses unisockets for it's networking layer. 54 | 55 | ## Usage 56 | 57 | Check out the [universal C/C++ header](https://github.com/alphahorizonio/unisockets/blob/main/cmd/c_echo_client/unisockets.h) for the C API docs or [![PkgGoDev](https://pkg.go.dev/badge/github.com/alphahorizonio/unisockets)](https://pkg.go.dev/github.com/alphahorizonio/unisockets) for the Go/TinyGo API. Many examples on how to use it (C, TinyGo & Go clients & servers plus an example WebAssembly runner) can also be found in [the `cmd` folder](https://github.com/alphahorizonio/unisockets/blob/main/cmd). Looking for advice on how to build and run natively or using WASM? Check out the [`Makefile`](https://github.com/alphahorizonio/unisockets/blob/main/Makefile)! 58 | 59 | ## License 60 | 61 | unisockets (c) 2021 Felicitas Pojtinger and contributors 62 | 63 | SPDX-License-Identifier: AGPL-3.0 64 | -------------------------------------------------------------------------------- /cmd/c_echo_client/main.c: -------------------------------------------------------------------------------- 1 | #include "unisockets.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define SERVER_HOST "127.0.0.1" 10 | #define SERVER_PORT 1234 11 | #define RECONNECT_TIMEOUT 2 12 | 13 | #define SENT_MESSAGE_BUFFER_LENGTH 1024 14 | #define RECEIVED_MESSAGE_PREFIX "You've sent: " 15 | #define RECEIVED_MESSAGE_BUFFER_LENGTH \ 16 | SENT_MESSAGE_BUFFER_LENGTH + sizeof(RECEIVED_MESSAGE_PREFIX) 17 | 18 | int main() { 19 | // Variables 20 | int server_sock; 21 | struct sockaddr_in server_host; 22 | 23 | ssize_t sent_message_length; 24 | char read_message[SENT_MESSAGE_BUFFER_LENGTH]; 25 | size_t received_message_length; 26 | char received_message[RECEIVED_MESSAGE_BUFFER_LENGTH]; 27 | 28 | socklen_t server_host_length = sizeof(struct sockaddr_in); 29 | 30 | memset(&server_host, 0, sizeof(server_host)); 31 | 32 | // Logging 33 | char server_address_readable[sizeof(SERVER_HOST) + sizeof(SERVER_PORT) + 1]; 34 | sprintf(server_address_readable, "%s:%d", SERVER_HOST, SERVER_PORT); 35 | 36 | // Create address 37 | server_host.sin_family = AF_INET; 38 | server_host.sin_port = htons(SERVER_PORT); 39 | if (inet_pton(AF_INET, SERVER_HOST, &server_host.sin_addr) == -1) { 40 | perror("[ERROR] Could not parse IP address:"); 41 | 42 | exit(-1); 43 | } 44 | 45 | // Create socket 46 | if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 47 | perror("[ERROR] Could not create socket:"); 48 | 49 | printf("[ERROR] Could not create socket %s\n", server_address_readable); 50 | 51 | exit(-1); 52 | } 53 | 54 | // Connect loop 55 | while (1) { 56 | printf("[INFO] Connecting to server %s\n", server_address_readable); 57 | 58 | // Connect 59 | if ((connect(server_sock, (struct sockaddr *)&server_host, 60 | server_host_length)) == -1) { 61 | perror("[ERROR] Could not connect to server:"); 62 | 63 | printf("[ERROR] Could not connect to server %s, retrying in %ds\n", 64 | server_address_readable, RECONNECT_TIMEOUT); 65 | 66 | sleep(RECONNECT_TIMEOUT); 67 | 68 | continue; 69 | } 70 | 71 | printf("[INFO] Connected to server %s\n", server_address_readable); 72 | 73 | // Read loop 74 | while (1) { 75 | memset(&received_message, 0, RECEIVED_MESSAGE_BUFFER_LENGTH); 76 | memset(&read_message, 0, SENT_MESSAGE_BUFFER_LENGTH); 77 | 78 | printf("[DEBUG] Waiting for user input\n"); 79 | 80 | // Read 81 | fgets(read_message, SENT_MESSAGE_BUFFER_LENGTH, stdin); 82 | 83 | // Send 84 | sent_message_length = 85 | send(server_sock, read_message, strlen(read_message), 0); 86 | if (sent_message_length == -1) { 87 | perror("[ERROR] Could not send to server:"); 88 | 89 | printf("[ERROR] Could not send to server %s, dropping message\n", 90 | server_address_readable); 91 | 92 | break; 93 | } 94 | 95 | printf("[DEBUG] Sent %zd bytes to %s\n", sent_message_length, 96 | server_address_readable); 97 | 98 | printf("[DEBUG] Waiting for server %s to send\n", 99 | server_address_readable); 100 | 101 | // Receive 102 | received_message_length = recv(server_sock, &received_message, 103 | RECEIVED_MESSAGE_BUFFER_LENGTH, 0); 104 | if (received_message_length == -1) { 105 | perror("[ERROR] Could not receive from server:"); 106 | 107 | printf("[ERROR] Could not receive from server %s, dropping message\n", 108 | server_address_readable); 109 | 110 | break; 111 | } 112 | 113 | if (received_message_length == 0) { 114 | break; 115 | } 116 | 117 | printf("[DEBUG] Received %zd bytes from %s\n", received_message_length, 118 | server_address_readable); 119 | 120 | // Print 121 | printf("%s", received_message); 122 | } 123 | 124 | printf("[INFO] Disconnected from server %s\n", server_address_readable); 125 | 126 | // Shutdown 127 | if ((shutdown(server_sock, SHUT_RDWR)) == -1) { 128 | perror("[ERROR] Could not shutdown socket:"); 129 | 130 | printf("[ERROR] Could not shutdown socket %s, stopping\n", 131 | server_address_readable); 132 | 133 | break; 134 | }; 135 | } 136 | 137 | return 0; 138 | } -------------------------------------------------------------------------------- /cmd/c_echo_client/unisockets.h: -------------------------------------------------------------------------------- 1 | #ifndef UNISOCKETS_H 2 | #define UNISOCKETS_H 3 | 4 | #ifdef UNISOCKETS_WITH_CUSTOM_ARPA_INET 5 | #define _Addr long 6 | typedef _Addr ssize_t; 7 | typedef unsigned _Addr size_t; 8 | typedef unsigned socklen_t; 9 | typedef unsigned short sa_family_t; 10 | typedef unsigned char uint8_t; 11 | typedef unsigned short uint16_t; 12 | typedef unsigned int uint32_t; 13 | typedef uint16_t in_port_t; 14 | typedef uint32_t in_addr_t; 15 | 16 | struct sockaddr { 17 | sa_family_t sa_family; 18 | char sa_data[14]; 19 | }; 20 | struct in_addr { 21 | in_addr_t s_addr; 22 | }; 23 | struct sockaddr_in { 24 | sa_family_t sin_family; 25 | in_port_t sin_port; 26 | struct in_addr sin_addr; 27 | uint8_t sin_zero[8]; 28 | }; 29 | 30 | #define PF_INET 2 31 | #define SOCK_STREAM 1 32 | #define SHUT_RDWR 2 33 | 34 | uint16_t htons(uint16_t v) { return (v >> 8) | (v << 8); } 35 | #else 36 | #include 37 | #endif 38 | 39 | typedef struct sockaddr sockaddr; 40 | typedef struct sockaddr_in sockaddr_in; 41 | typedef struct in_addr in_addr; 42 | 43 | #ifndef UNISOCKETS_WITH_INVERSE_ALIAS 44 | int unisockets_socket(int, int, int); 45 | int unisockets_connect(int, const struct sockaddr *, socklen_t); 46 | ssize_t unisockets_send(int, const void *, size_t, int); 47 | ssize_t unisockets_recv(int, void *, size_t, int); 48 | int unisockets_bind(int, const struct sockaddr *, socklen_t); 49 | int unisockets_listen(int, int); 50 | int unisockets_accept(int, struct sockaddr *, socklen_t *); 51 | #endif 52 | 53 | #ifdef UNISOCKETS_WITH_ALIAS 54 | #define socket unisockets_socket 55 | #define connect unisockets_connect 56 | #define send unisockets_send 57 | #define recv unisockets_recv 58 | #define bind unisockets_bind 59 | #define listen unisockets_listen 60 | #define accept unisockets_accept 61 | #endif 62 | 63 | #ifdef UNISOCKETS_WITH_INVERSE_ALIAS 64 | #define unisockets_socket socket 65 | #define unisockets_connect connect 66 | #define unisockets_send send 67 | #define unisockets_recv recv 68 | #define unisockets_bind bind 69 | #define unisockets_listen listen 70 | #define unisockets_accept accept 71 | #endif 72 | 73 | #endif /* UNISOCKETS_H */ -------------------------------------------------------------------------------- /cmd/c_echo_server/main.c: -------------------------------------------------------------------------------- 1 | #include "unisockets.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define LOCAL_HOST "127.0.0.1" 12 | #define LOCAL_PORT 1234 13 | #define BACKLOG 5 14 | 15 | #define RECEIVED_MESSAGE_BUFFER_LENGTH 1024 16 | #define SENT_MESSAGE_PREFIX "You've sent: " 17 | #define SENT_MESSAGE_BUFFER_LENGTH \ 18 | RECEIVED_MESSAGE_BUFFER_LENGTH + sizeof(SENT_MESSAGE_PREFIX) 19 | 20 | int main() { 21 | // Variables 22 | int server_sock; 23 | int client_sock; 24 | 25 | struct sockaddr_in server_address; 26 | struct sockaddr_in client_address; 27 | 28 | socklen_t server_socket_length = sizeof(struct sockaddr_in); 29 | 30 | size_t received_message_length; 31 | char received_message[RECEIVED_MESSAGE_BUFFER_LENGTH]; 32 | ssize_t sent_message_length; 33 | char sent_message[SENT_MESSAGE_BUFFER_LENGTH]; 34 | char client_address_readable[sizeof(client_address.sin_addr) + 35 | client_address.sin_port + 1]; 36 | char client_address_human_readable[INET_ADDRSTRLEN]; 37 | 38 | // Logging 39 | char server_address_readable[sizeof(LOCAL_HOST) + sizeof(LOCAL_PORT) + 1]; 40 | sprintf(server_address_readable, "%s:%d", LOCAL_HOST, LOCAL_PORT); 41 | 42 | memset(&server_address, 0, sizeof(server_address)); 43 | memset(&client_address, 0, sizeof(client_address)); 44 | 45 | // Create address 46 | server_address.sin_family = AF_INET; 47 | server_address.sin_port = htons(LOCAL_PORT); 48 | if (inet_pton(AF_INET, LOCAL_HOST, &server_address.sin_addr) == -1) { 49 | perror("[ERROR] Could not parse IP address:"); 50 | 51 | exit(-1); 52 | } 53 | 54 | // Create socket 55 | if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 56 | perror("[ERROR] Could not create socket:"); 57 | 58 | printf("[ERROR] Could not create socket %s\n", server_address_readable); 59 | 60 | exit(-1); 61 | } 62 | 63 | // Bind 64 | if ((bind(server_sock, (struct sockaddr *)&server_address, 65 | server_socket_length)) == -1) { 66 | perror("[ERROR] Could not bind to socket:"); 67 | 68 | printf("[ERROR] Could not bind to socket %s\n", server_address_readable); 69 | 70 | exit(-1); 71 | } 72 | 73 | // Listen 74 | if ((listen(server_sock, BACKLOG)) == -1) { 75 | perror("[ERROR] Could not listen on socket:"); 76 | 77 | printf("[ERROR] Could not listen on socket %s\n", server_address_readable); 78 | 79 | exit(-1); 80 | } 81 | 82 | printf("[INFO] Listening on %s\n", server_address_readable); 83 | 84 | // Accept loop 85 | while (1) { 86 | printf("[DEBUG] Accepting on %s\n", server_address_readable); 87 | 88 | // Accept 89 | if ((client_sock = accept(server_sock, (struct sockaddr *)&client_address, 90 | &server_socket_length)) == -1) { 91 | perror("[ERROR] Could not accept, continuing:"); 92 | 93 | continue; 94 | } 95 | 96 | if (inet_pton(AF_INET, LOCAL_HOST, &server_address.sin_addr) == -1) { 97 | perror("[ERROR] Could not parse IP address:"); 98 | 99 | continue; 100 | } 101 | 102 | inet_ntop(AF_INET, &client_address.sin_addr, client_address_human_readable, 103 | sizeof(client_address_human_readable)); 104 | 105 | sprintf(client_address_readable, "%s:%d", client_address_human_readable, 106 | client_address.sin_port); 107 | 108 | printf("[INFO] Accepted client %s\n", client_address_readable); 109 | 110 | // Receive loop 111 | received_message_length = 1; 112 | while (received_message_length) { 113 | memset(&received_message, 0, RECEIVED_MESSAGE_BUFFER_LENGTH); 114 | memset(&sent_message, 0, SENT_MESSAGE_BUFFER_LENGTH); 115 | 116 | printf("[DEBUG] Waiting for client %s to send\n", 117 | client_address_readable); 118 | 119 | // Receive 120 | received_message_length = recv(client_sock, &received_message, 121 | RECEIVED_MESSAGE_BUFFER_LENGTH, 0); 122 | if (received_message_length == -1) { 123 | perror("[ERROR] Could not receive from client:"); 124 | 125 | printf("[ERROR] Could not receive from client %s, dropping message\n", 126 | client_address_readable); 127 | 128 | break; 129 | } 130 | 131 | if (received_message_length == 0) { 132 | break; 133 | } 134 | 135 | printf("[DEBUG] Received %zd bytes from %s\n", received_message_length, 136 | client_address_readable); 137 | 138 | // Process 139 | sprintf((char *)&sent_message, "%s%s", SENT_MESSAGE_PREFIX, 140 | received_message); 141 | sent_message[SENT_MESSAGE_BUFFER_LENGTH - 1] = '\0'; 142 | 143 | // Send 144 | sent_message_length = 145 | send(client_sock, sent_message, SENT_MESSAGE_BUFFER_LENGTH, 0); 146 | if (sent_message_length == -1) { 147 | perror("[ERROR] Could not send to client:"); 148 | 149 | printf("[ERROR] Could not send to client %s, dropping message\n", 150 | client_address_readable); 151 | 152 | break; 153 | } 154 | 155 | printf("[DEBUG] Sent %zd bytes to %s\n", sent_message_length, 156 | client_address_readable); 157 | } 158 | 159 | printf("[INFO] Client %s disconnected\n", client_address_readable); 160 | 161 | // Shutdown 162 | if ((shutdown(client_sock, SHUT_RDWR)) == -1) { 163 | perror("[ERROR] Could not shutdown socket:"); 164 | 165 | printf("[ERROR] Could not shutdown socket %s, stopping\n", 166 | client_address_readable); 167 | 168 | break; 169 | }; 170 | } 171 | 172 | return 0; 173 | } -------------------------------------------------------------------------------- /cmd/c_echo_server/unisockets.h: -------------------------------------------------------------------------------- 1 | #ifndef UNISOCKETS_H 2 | #define UNISOCKETS_H 3 | 4 | #ifdef UNISOCKETS_WITH_CUSTOM_ARPA_INET 5 | #define _Addr long 6 | typedef _Addr ssize_t; 7 | typedef unsigned _Addr size_t; 8 | typedef unsigned socklen_t; 9 | typedef unsigned short sa_family_t; 10 | typedef unsigned char uint8_t; 11 | typedef unsigned short uint16_t; 12 | typedef unsigned int uint32_t; 13 | typedef uint16_t in_port_t; 14 | typedef uint32_t in_addr_t; 15 | 16 | struct sockaddr { 17 | sa_family_t sa_family; 18 | char sa_data[14]; 19 | }; 20 | struct in_addr { 21 | in_addr_t s_addr; 22 | }; 23 | struct sockaddr_in { 24 | sa_family_t sin_family; 25 | in_port_t sin_port; 26 | struct in_addr sin_addr; 27 | uint8_t sin_zero[8]; 28 | }; 29 | 30 | #define PF_INET 2 31 | #define SOCK_STREAM 1 32 | #define SHUT_RDWR 2 33 | 34 | uint16_t htons(uint16_t v) { return (v >> 8) | (v << 8); } 35 | #else 36 | #include 37 | #endif 38 | 39 | typedef struct sockaddr sockaddr; 40 | typedef struct sockaddr_in sockaddr_in; 41 | typedef struct in_addr in_addr; 42 | 43 | #ifndef UNISOCKETS_WITH_INVERSE_ALIAS 44 | int unisockets_socket(int, int, int); 45 | int unisockets_connect(int, const struct sockaddr *, socklen_t); 46 | ssize_t unisockets_send(int, const void *, size_t, int); 47 | ssize_t unisockets_recv(int, void *, size_t, int); 48 | int unisockets_bind(int, const struct sockaddr *, socklen_t); 49 | int unisockets_listen(int, int); 50 | int unisockets_accept(int, struct sockaddr *, socklen_t *); 51 | #endif 52 | 53 | #ifdef UNISOCKETS_WITH_ALIAS 54 | #define socket unisockets_socket 55 | #define connect unisockets_connect 56 | #define send unisockets_send 57 | #define recv unisockets_recv 58 | #define bind unisockets_bind 59 | #define listen unisockets_listen 60 | #define accept unisockets_accept 61 | #endif 62 | 63 | #ifdef UNISOCKETS_WITH_INVERSE_ALIAS 64 | #define unisockets_socket socket 65 | #define unisockets_connect connect 66 | #define unisockets_send send 67 | #define unisockets_recv recv 68 | #define unisockets_bind bind 69 | #define unisockets_listen listen 70 | #define unisockets_accept accept 71 | #endif 72 | 73 | #endif /* UNISOCKETS_H */ -------------------------------------------------------------------------------- /cmd/go_echo_client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/alphahorizonio/unisockets/pkg/unisockets" 11 | ) 12 | 13 | var ( 14 | SERVER_HOST = []byte{127, 0, 0, 1} 15 | ) 16 | 17 | const ( 18 | SERVER_PORT = 1234 19 | RECONNECT_TIMEOUT = time.Second 20 | 21 | BUFFER_LENGTH = 1038 22 | ) 23 | 24 | func main() { 25 | // Create address 26 | serverAddress := unisockets.SockaddrIn{ 27 | SinFamily: unisockets.PF_INET, 28 | SinPort: unisockets.Htons(SERVER_PORT), 29 | SinAddr: struct{ SAddr uint32 }{ 30 | SAddr: binary.LittleEndian.Uint32(SERVER_HOST), 31 | }, 32 | } 33 | serverAddressReadable := fmt.Sprintf("%v:%v", SERVER_HOST, SERVER_PORT) 34 | 35 | // Create socket 36 | serverSocket, err := unisockets.Socket(unisockets.PF_INET, unisockets.SOCK_STREAM, 0) 37 | if err != nil { 38 | fmt.Printf("[ERROR] Could not create socket %v: %v\n", serverAddressReadable, err) 39 | 40 | os.Exit(1) 41 | } 42 | 43 | // Create reader 44 | reader := bufio.NewReader(os.Stdin) 45 | 46 | // Connect loop 47 | for { 48 | fmt.Println("[INFO] Connecting to server", serverAddressReadable) 49 | 50 | // Connect 51 | if err := unisockets.Connect(serverSocket, &serverAddress); err != nil { 52 | fmt.Printf("[ERROR] Could not connect to server %v, retrying in %v: %v\n", serverAddressReadable, RECONNECT_TIMEOUT, err) 53 | 54 | time.Sleep(RECONNECT_TIMEOUT) 55 | 56 | continue 57 | } 58 | 59 | fmt.Println("[INFO] Connected to server", serverAddressReadable) 60 | 61 | // Read loop 62 | for { 63 | fmt.Println("[DEBUG] Waiting for user input") 64 | 65 | // Read 66 | readMessage, err := reader.ReadString('\n') 67 | if err != nil { 68 | fmt.Println("[ERROR] Could not read from stdin:", err) 69 | } 70 | 71 | // Send 72 | sentMessage := []byte(readMessage) 73 | 74 | sentMessageLength, err := unisockets.Send(serverSocket, sentMessage, 0) 75 | if err != nil { 76 | fmt.Printf("[ERROR] Could not send to server %v, dropping message: %v\n", serverAddressReadable, err) 77 | 78 | break 79 | } 80 | 81 | fmt.Printf("[DEBUG] Sent %v bytes to %v\n", sentMessageLength, serverAddressReadable) 82 | 83 | fmt.Printf("[DEBUG] Waiting for server %v to send\n", serverAddressReadable) 84 | 85 | // Receive 86 | receivedMessage := make([]byte, BUFFER_LENGTH) 87 | 88 | receivedMessageLength, err := unisockets.Recv(serverSocket, &receivedMessage, BUFFER_LENGTH, 0) 89 | if err != nil { 90 | fmt.Printf("[ERROR] Could not receive from server %v, dropping message: %v\n", serverAddressReadable, err) 91 | 92 | break 93 | } 94 | 95 | if receivedMessageLength == 0 { 96 | break 97 | } 98 | 99 | fmt.Printf("[DEBUG] Received %v bytes from %v\n", receivedMessageLength, serverAddressReadable) 100 | 101 | // Print 102 | fmt.Printf("%v", string(receivedMessage)) 103 | } 104 | 105 | fmt.Println("[INFO] Disconnected from server", serverAddressReadable) 106 | 107 | // Shutdown 108 | if err := unisockets.Shutdown(serverSocket, unisockets.SHUT_RDWR); err != nil { 109 | fmt.Printf("[ERROR] Could not shutdown socket %v, stopping: %v\n", serverAddressReadable, err) 110 | 111 | break 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /cmd/go_echo_server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/alphahorizonio/unisockets/pkg/unisockets" 9 | ) 10 | 11 | var ( 12 | LOCAL_HOST = []byte{127, 0, 0, 1} 13 | ) 14 | 15 | const ( 16 | LOCAL_PORT = 1234 17 | BACKLOG = 1 18 | 19 | BUFFER_LENGTH = 1024 20 | ) 21 | 22 | func main() { 23 | // Create address 24 | serverAddress := unisockets.SockaddrIn{ 25 | SinFamily: unisockets.PF_INET, 26 | SinPort: unisockets.Htons(LOCAL_PORT), 27 | SinAddr: struct{ SAddr uint32 }{ 28 | SAddr: binary.LittleEndian.Uint32(LOCAL_HOST), 29 | }, 30 | } 31 | serverAddressReadable := fmt.Sprintf("%v:%v", LOCAL_HOST, LOCAL_PORT) 32 | 33 | // Create socket 34 | serverSocket, err := unisockets.Socket(unisockets.PF_INET, unisockets.SOCK_STREAM, 0) 35 | if err != nil { 36 | fmt.Printf("[ERROR] Could not create socket %v: %v\n", serverAddressReadable, err) 37 | 38 | os.Exit(1) 39 | } 40 | 41 | // Bind 42 | if err := unisockets.Bind(serverSocket, &serverAddress); err != nil { 43 | fmt.Printf("[ERROR] Could not bind to socket %v: %v\n", serverAddressReadable, err) 44 | 45 | os.Exit(1) 46 | } 47 | 48 | // Listen 49 | if err := unisockets.Listen(serverSocket, BACKLOG); err != nil { 50 | fmt.Printf("[ERROR] Could not listen on socket %v: %v\n", serverAddressReadable, err) 51 | 52 | os.Exit(1) 53 | } 54 | 55 | fmt.Println("[INFO] Listening on", serverAddressReadable) 56 | 57 | // Accept loop 58 | for { 59 | fmt.Println("[DEBUG] Accepting on", serverAddressReadable) 60 | 61 | clientAddress := unisockets.SockaddrIn{} 62 | 63 | // Accept 64 | clientSocket, err := unisockets.Accept(serverSocket, &clientAddress) 65 | if err != nil { 66 | fmt.Println("[ERROR] Could not accept, continuing:", err) 67 | 68 | continue 69 | } 70 | 71 | go func(innerClientSocket int32, innerClientAddress unisockets.SockaddrIn) { 72 | clientHost := make([]byte, 4) // xxx.xxx.xxx.xxx 73 | binary.LittleEndian.PutUint32(clientHost, uint32(innerClientAddress.SinAddr.SAddr)) 74 | 75 | clientAddressReadable := fmt.Sprintf("%v:%v", clientHost, innerClientAddress.SinPort) 76 | 77 | fmt.Println("[INFO] Accepted client", clientAddressReadable) 78 | 79 | // Receive loop 80 | for { 81 | fmt.Printf("[DEBUG] Waiting for client %v to send\n", clientAddressReadable) 82 | 83 | // Receive 84 | receivedMessage := make([]byte, BUFFER_LENGTH) 85 | 86 | receivedMessageLength, err := unisockets.Recv(innerClientSocket, &receivedMessage, BUFFER_LENGTH, 0) 87 | if err != nil { 88 | fmt.Printf("[ERROR] Could not receive from client %v, dropping message: %v\n", clientAddressReadable, err) 89 | 90 | continue 91 | } 92 | 93 | if receivedMessageLength == 0 { 94 | break 95 | } 96 | 97 | fmt.Printf("[DEBUG] Received %v bytes from %v\n", receivedMessageLength, clientAddressReadable) 98 | 99 | // Send 100 | sentMessage := []byte(fmt.Sprintf("You've sent: %v", string(receivedMessage))) 101 | 102 | sentMessageLength, err := unisockets.Send(innerClientSocket, sentMessage, 0) 103 | if err != nil { 104 | fmt.Printf("[ERROR] Could not send to client %v, dropping message: %v\n", clientAddressReadable, err) 105 | 106 | break 107 | } 108 | 109 | fmt.Printf("[DEBUG] Sent %v bytes to %v\n", sentMessageLength, clientAddressReadable) 110 | } 111 | 112 | fmt.Println("[INFO] Disconnected from client", clientAddressReadable) 113 | 114 | // Shutdown 115 | if err := unisockets.Shutdown(innerClientSocket, unisockets.SHUT_RDWR); err != nil { 116 | fmt.Printf("[ERROR] Could not shutdown client socket %v, stopping: %v\n", clientAddressReadable, err) 117 | 118 | return 119 | } 120 | }(clientSocket, clientAddress) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /cmd/unisockets_runner/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { WASI } from "@wasmer/wasi"; 4 | import { lowerI64Imports } from "@wasmer/wasm-transformer"; 5 | import * as Asyncify from "asyncify-wasm"; 6 | import Emittery from "emittery"; 7 | import fs from "fs"; 8 | import { ExtendedRTCConfiguration } from "wrtc"; 9 | import yargs from "yargs"; 10 | import { AliasDoesNotExistError } from "../../pkg/web/signaling/errors/alias-does-not-exist"; 11 | import { SignalingClient } from "../../pkg/web/signaling/services/signaling-client"; 12 | import { SignalingServer } from "../../pkg/web/signaling/services/signaling-server"; 13 | import { Sockets } from "../../pkg/web/sockets/sockets"; 14 | import { Transporter } from "../../pkg/web/transport/transporter"; 15 | import { getLogger } from "../../pkg/web/utils/logger"; 16 | const Go = require("../../vendor/go/wasm_exec.js"); 17 | const TinyGo = require("../../vendor/tinygo/wasm_exec.js"); 18 | 19 | const transporterConfig: ExtendedRTCConfiguration = { 20 | iceServers: [ 21 | { 22 | urls: "stun:stun.l.google.com:19302", 23 | }, 24 | ], 25 | }; 26 | 27 | const { 28 | runSignalingServer, 29 | runBinary, 30 | 31 | signalingServerListenAddress, 32 | 33 | signalingServerConnectAddress, 34 | reconnectTimeout, 35 | subnetPrefix, 36 | 37 | binaryPath, 38 | 39 | useC, 40 | useGo, 41 | useTinyGo, 42 | 43 | useJSSI, 44 | useWASI, 45 | } = yargs(process.argv.slice(2)).options({ 46 | runSignalingServer: { 47 | description: "Run the signaling server", 48 | default: false, 49 | }, 50 | runBinary: { 51 | description: "Run a binary", 52 | default: false, 53 | }, 54 | 55 | signalingServerListenAddress: { 56 | description: 57 | "Signaling server listen address. You may also set the PORT env variable to change the port.", 58 | default: `0.0.0.0:${process.env.PORT || 6999}`, 59 | }, 60 | 61 | signalingServerConnectAddress: { 62 | description: "Signaling server connect address", 63 | default: "wss://signaler.webnetes.dev", 64 | }, 65 | reconnectTimeout: { 66 | description: "Reconnect timeout in milliseconds", 67 | default: 1000, 68 | }, 69 | subnetPrefix: { 70 | description: "Subnet prefix to advertise", 71 | default: "127.0.0", 72 | }, 73 | 74 | binaryPath: { 75 | description: "Path to the binary to run", 76 | default: "main.wasm", 77 | }, 78 | 79 | useC: { 80 | description: "Use the C implementation", 81 | default: false, 82 | }, 83 | useGo: { 84 | description: "Use the Go implementation", 85 | default: false, 86 | }, 87 | useTinyGo: { 88 | description: "Use the TinyGo implementation", 89 | default: false, 90 | }, 91 | 92 | useJSSI: { 93 | description: "Use the JavaScript system interface", 94 | default: false, 95 | }, 96 | useWASI: { 97 | description: "Use the WebAssembly system interface", 98 | default: false, 99 | }, 100 | }).argv; 101 | 102 | const logger = getLogger(); 103 | 104 | if (runBinary) { 105 | // Transporter handlers 106 | const handleTransporterConnectionConnect = async (id: string) => { 107 | logger.verbose("Handling transporter connection connect", { id }); 108 | }; 109 | const handleTransporterConnectionDisconnect = async (id: string) => { 110 | logger.verbose("Handling transporter connection disconnect", { id }); 111 | }; 112 | const handleTransporterChannelOpen = async (id: string) => { 113 | logger.verbose("Handling transporter connection open", { id }); 114 | }; 115 | const handleTransporterChannelClose = async (id: string) => { 116 | logger.verbose("Handling transporter connection close", { id }); 117 | }; 118 | 119 | const ready = new Emittery(); 120 | 121 | const aliases = new Map(); 122 | const transporter = new Transporter( 123 | transporterConfig, 124 | handleTransporterConnectionConnect, 125 | handleTransporterConnectionDisconnect, 126 | handleTransporterChannelOpen, 127 | handleTransporterChannelClose 128 | ); 129 | 130 | // Signaling client handlers 131 | const handleConnect = async () => { 132 | logger.verbose("Handling connect"); 133 | }; 134 | const handleDisconnect = async () => { 135 | logger.verbose("Handling disconnect"); 136 | }; 137 | const handleAcknowledgement = async (id: string, rejected: boolean) => { 138 | logger.debug("Handling acknowledgement", { id, rejected }); 139 | 140 | if (rejected) { 141 | logger.error("Knock rejected", { 142 | id, 143 | }); 144 | } 145 | 146 | await ready.emit("ready", true); 147 | }; 148 | const getOffer = async ( 149 | answererId: string, 150 | handleCandidate: (candidate: string) => Promise 151 | ) => { 152 | const offer = await transporter.getOffer(answererId, handleCandidate); 153 | 154 | logger.verbose("Created offer", { answererId, offer }); 155 | 156 | return offer; 157 | }; 158 | const handleOffer = async ( 159 | offererId: string, 160 | offer: string, 161 | handleCandidate: (candidate: string) => Promise 162 | ) => { 163 | const answer = await transporter.handleOffer( 164 | offererId, 165 | offer, 166 | handleCandidate 167 | ); 168 | 169 | logger.verbose("Created answer for offer", { offererId, offer, answer }); 170 | 171 | return answer; 172 | }; 173 | const handleAnswer = async ( 174 | offererId: string, 175 | answererId: string, 176 | answer: string 177 | ) => { 178 | logger.verbose("Handling answer", { offererId, answererId, answer }); 179 | 180 | await transporter.handleAnswer(answererId, answer); 181 | }; 182 | const handleCandidate = async ( 183 | offererId: string, 184 | answererId: string, 185 | candidate: string 186 | ) => { 187 | logger.verbose("Handling candidate", { offererId, answererId, candidate }); 188 | 189 | await transporter.handleCandidate(offererId, candidate); 190 | }; 191 | const handleGoodbye = async (id: string) => { 192 | logger.verbose("Handling goodbye", { id }); 193 | 194 | await transporter.shutdown(id); 195 | }; 196 | const handleAlias = async (id: string, alias: string, set: boolean) => { 197 | logger.debug("Handling alias", { id }); 198 | 199 | if (set) { 200 | logger.verbose("Setting alias", { id, alias }); 201 | 202 | aliases.set(alias, id); 203 | 204 | logger.debug("New aliases", { 205 | aliases: JSON.stringify(Array.from(aliases)), 206 | }); 207 | } else { 208 | logger.verbose("Removing alias", { id, alias }); 209 | 210 | aliases.delete(alias); 211 | 212 | logger.debug("New aliases", { 213 | aliases: JSON.stringify(Array.from(aliases)), 214 | }); 215 | } 216 | }; 217 | 218 | const signalingClient = new SignalingClient( 219 | signalingServerConnectAddress, 220 | reconnectTimeout, 221 | subnetPrefix, 222 | handleConnect, 223 | handleDisconnect, 224 | handleAcknowledgement, 225 | getOffer, 226 | handleOffer, 227 | handleAnswer, 228 | handleCandidate, 229 | handleGoodbye, 230 | handleAlias 231 | ); 232 | 233 | // Socket handlers 234 | const handleExternalBind = async (alias: string) => { 235 | logger.verbose("Handling external bind", { alias }); 236 | 237 | await signalingClient.bind(alias); 238 | }; 239 | 240 | const handleExternalAccept = async (alias: string) => { 241 | logger.verbose("Handling external accept", { alias }); 242 | 243 | return await signalingClient.accept(alias); 244 | }; 245 | 246 | const handleExternalConnect = async (alias: string) => { 247 | logger.verbose("Handling external connect", { alias }); 248 | 249 | await signalingClient.connect(alias); 250 | }; 251 | 252 | const handleExternalSend = async (alias: string, msg: Uint8Array) => { 253 | logger.verbose("Handling external send", { alias, msg }); 254 | 255 | if (aliases.has(alias)) { 256 | return await transporter.send(aliases.get(alias)!, msg); // .has 257 | } else { 258 | logger.error("Could not find alias", { alias }); 259 | } 260 | }; 261 | 262 | const handleExternalRecv = async (alias: string) => { 263 | if (aliases.has(alias)) { 264 | const msg = await transporter.recv(aliases.get(alias)!); // .has 265 | 266 | logger.verbose("Handling external recv", { alias, msg }); 267 | 268 | return msg; 269 | } else { 270 | throw new AliasDoesNotExistError(); 271 | } 272 | }; 273 | 274 | const sockets = new Sockets( 275 | handleExternalBind, 276 | handleExternalAccept, 277 | handleExternalConnect, 278 | handleExternalSend, 279 | handleExternalRecv 280 | ); 281 | 282 | // WASM runner 283 | (async () => { 284 | await ready.once("ready"); 285 | 286 | if (useC) { 287 | if (useWASI) { 288 | const wasi = new WASI({ 289 | args: [], 290 | env: {}, 291 | }); 292 | const { 293 | memoryId, 294 | imports: socketEnvImports, 295 | } = await sockets.getImports(); 296 | 297 | const module = await WebAssembly.compile( 298 | await lowerI64Imports(new Uint8Array(fs.readFileSync(binaryPath))) 299 | ); 300 | const instance = await Asyncify.instantiate(module, { 301 | ...wasi.getImports(module), 302 | env: socketEnvImports, 303 | }); 304 | 305 | sockets.setMemory(memoryId, instance.exports.memory); 306 | 307 | wasi.start(instance); 308 | } 309 | } else if (useGo) { 310 | if (useJSSI) { 311 | const go = new Go(); 312 | const { 313 | memoryId, 314 | imports: socketEnvImports, 315 | } = await sockets.getImports(); 316 | 317 | const instance = await WebAssembly.instantiate( 318 | await WebAssembly.compile(fs.readFileSync(binaryPath)), 319 | go.importObject 320 | ); 321 | 322 | (global as any).jssiImports = socketEnvImports; 323 | 324 | sockets.setMemory(memoryId, (instance.exports as any).mem); 325 | 326 | go.run(instance); 327 | } 328 | } else if (useTinyGo) { 329 | if (useJSSI) { 330 | const go = new TinyGo(); 331 | const { 332 | memoryId, 333 | imports: socketEnvImports, 334 | } = await sockets.getImports(); 335 | 336 | const instance = await WebAssembly.instantiate( 337 | await WebAssembly.compile(fs.readFileSync(binaryPath)), 338 | go.importObject 339 | ); 340 | 341 | (global as any).jssiImports = socketEnvImports; 342 | 343 | sockets.setMemory(memoryId, (instance.exports as any).memory); 344 | 345 | go.run(instance); 346 | } else if (useWASI) { 347 | const wasi = new WASI({ 348 | args: [], 349 | env: {}, 350 | }); 351 | const { 352 | memoryId, 353 | imports: socketEnvImports, 354 | } = await sockets.getImports(); 355 | const go = new TinyGo(); 356 | 357 | const module = await WebAssembly.compile( 358 | await lowerI64Imports(new Uint8Array(fs.readFileSync(binaryPath))) 359 | ); 360 | const instance = await Asyncify.instantiate(module, { 361 | ...wasi.getImports(module), 362 | env: { 363 | ...go.importObject.env, 364 | ...socketEnvImports, 365 | }, 366 | }); 367 | 368 | sockets.setMemory(memoryId, instance.exports.memory); 369 | 370 | wasi.start(instance); 371 | } 372 | } 373 | })(); 374 | 375 | signalingClient.open(); 376 | } else if (runSignalingServer) { 377 | const signalingServer = new SignalingServer( 378 | signalingServerListenAddress.split(":")[0], 379 | parseInt(signalingServerListenAddress.split(":")[1]) 380 | ); 381 | 382 | signalingServer.open(); 383 | } 384 | -------------------------------------------------------------------------------- /cmd/unisockets_runner_web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | unisockets Demo 7 | 8 | 9 |

Check your console!

10 | 11 | Run the server instead 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /cmd/unisockets_runner_web/main.ts: -------------------------------------------------------------------------------- 1 | (window as any).setImmediate = window.setInterval; // Polyfill 2 | 3 | import { WASI } from "@wasmer/wasi"; 4 | import wasiBindings from "@wasmer/wasi/lib/bindings/browser"; 5 | import { lowerI64Imports } from "@wasmer/wasm-transformer/lib/unoptimized/wasm-transformer.esm.js"; 6 | import { WasmFs } from "@wasmer/wasmfs"; 7 | import * as Asyncify from "asyncify-wasm"; 8 | import Emittery from "emittery"; 9 | import { ExtendedRTCConfiguration } from "wrtc"; 10 | import { AliasDoesNotExistError } from "../../pkg/web/signaling/errors/alias-does-not-exist"; 11 | import { SignalingClient } from "../../pkg/web/signaling/services/signaling-client"; 12 | import { Sockets } from "../../pkg/web/sockets/sockets"; 13 | import { Transporter } from "../../pkg/web/transport/transporter"; 14 | import { getLogger } from "../../pkg/web/utils/logger"; 15 | const Go = require("../../vendor/go/wasm_exec.js"); 16 | const TinyGo = require("../../vendor/tinygo/wasm_exec.js"); 17 | 18 | const transporterConfig: ExtendedRTCConfiguration = { 19 | iceServers: [ 20 | { 21 | urls: "stun:global.stun.twilio.com:3478?transport=udp", 22 | }, 23 | { 24 | username: 25 | "f4b4035eaa76f4a55de5f4351567653ee4ff6fa97b50b6b334fcc1be9c27212d", 26 | urls: "turn:global.turn.twilio.com:3478?transport=udp", 27 | credential: "w1uxM55V9yVoqyVFjt+mxDBV0F87AUCemaYVQGxsPLw=", 28 | }, 29 | { 30 | username: 31 | "f4b4035eaa76f4a55de5f4351567653ee4ff6fa97b50b6b334fcc1be9c27212d", 32 | urls: "turn:global.turn.twilio.com:3478?transport=tcp", 33 | credential: "w1uxM55V9yVoqyVFjt+mxDBV0F87AUCemaYVQGxsPLw=", 34 | }, 35 | { 36 | username: 37 | "f4b4035eaa76f4a55de5f4351567653ee4ff6fa97b50b6b334fcc1be9c27212d", 38 | urls: "turn:global.turn.twilio.com:443?transport=tcp", 39 | credential: "w1uxM55V9yVoqyVFjt+mxDBV0F87AUCemaYVQGxsPLw=", 40 | }, 41 | ], 42 | }; 43 | 44 | const signalingServerConnectAddress = "wss://signaler.webnetes.dev"; 45 | const reconnectTimeout = 1000; 46 | const subnetPrefix = "127.0.0"; 47 | 48 | const urlParams = new URLSearchParams(window.location.search); 49 | const useServerBinary = urlParams.get("server"); 50 | 51 | const binaryPath = useServerBinary 52 | ? "out/go/echo_server.wasm" 53 | : "out/go/echo_client.wasm"; 54 | 55 | const useC = false; 56 | const useGo = true; 57 | const useTinyGo = false; 58 | 59 | const useJSSI = true; 60 | const useWASI = false; 61 | 62 | const logger = getLogger(); 63 | 64 | // Transporter handlers 65 | const handleTransporterConnectionConnect = async (id: string) => { 66 | logger.verbose("Handling transporter connection connect", { id }); 67 | }; 68 | const handleTransporterConnectionDisconnect = async (id: string) => { 69 | logger.verbose("Handling transporter connection disconnect", { id }); 70 | }; 71 | const handleTransporterChannelOpen = async (id: string) => { 72 | logger.verbose("Handling transporter connection open", { id }); 73 | }; 74 | const handleTransporterChannelClose = async (id: string) => { 75 | logger.verbose("Handling transporter connection close", { id }); 76 | }; 77 | 78 | const ready = new Emittery(); 79 | 80 | const aliases = new Map(); 81 | const transporter = new Transporter( 82 | transporterConfig, 83 | handleTransporterConnectionConnect, 84 | handleTransporterConnectionDisconnect, 85 | handleTransporterChannelOpen, 86 | handleTransporterChannelClose 87 | ); 88 | 89 | // Signaling client handlers 90 | const handleConnect = async () => { 91 | logger.verbose("Handling connect"); 92 | }; 93 | const handleDisconnect = async () => { 94 | logger.verbose("Handling disconnect"); 95 | }; 96 | const handleAcknowledgement = async (id: string, rejected: boolean) => { 97 | logger.debug("Handling acknowledgement", { id, rejected }); 98 | 99 | if (rejected) { 100 | logger.error("Knock rejected", { 101 | id, 102 | }); 103 | } 104 | 105 | await ready.emit("ready", true); 106 | }; 107 | const getOffer = async ( 108 | answererId: string, 109 | handleCandidate: (candidate: string) => Promise 110 | ) => { 111 | const offer = await transporter.getOffer(answererId, handleCandidate); 112 | 113 | logger.verbose("Created offer", { answererId, offer }); 114 | 115 | return offer; 116 | }; 117 | const handleOffer = async ( 118 | offererId: string, 119 | offer: string, 120 | handleCandidate: (candidate: string) => Promise 121 | ) => { 122 | const answer = await transporter.handleOffer( 123 | offererId, 124 | offer, 125 | handleCandidate 126 | ); 127 | 128 | logger.verbose("Created answer for offer", { offererId, offer, answer }); 129 | 130 | return answer; 131 | }; 132 | const handleAnswer = async ( 133 | offererId: string, 134 | answererId: string, 135 | answer: string 136 | ) => { 137 | logger.verbose("Handling answer", { offererId, answererId, answer }); 138 | 139 | await transporter.handleAnswer(answererId, answer); 140 | }; 141 | const handleCandidate = async ( 142 | offererId: string, 143 | answererId: string, 144 | candidate: string 145 | ) => { 146 | logger.verbose("Handling candidate", { offererId, answererId, candidate }); 147 | 148 | await transporter.handleCandidate(offererId, candidate); 149 | }; 150 | const handleGoodbye = async (id: string) => { 151 | logger.verbose("Handling goodbye", { id }); 152 | 153 | await transporter.shutdown(id); 154 | }; 155 | const handleAlias = async (id: string, alias: string, set: boolean) => { 156 | logger.debug("Handling alias", { id }); 157 | 158 | if (set) { 159 | logger.verbose("Setting alias", { id, alias }); 160 | 161 | aliases.set(alias, id); 162 | 163 | logger.debug("New aliases", { 164 | aliases: JSON.stringify(Array.from(aliases)), 165 | }); 166 | } else { 167 | logger.verbose("Removing alias", { id, alias }); 168 | 169 | aliases.delete(alias); 170 | 171 | logger.debug("New aliases", { 172 | aliases: JSON.stringify(Array.from(aliases)), 173 | }); 174 | } 175 | }; 176 | 177 | const signalingClient = new SignalingClient( 178 | signalingServerConnectAddress, 179 | reconnectTimeout, 180 | subnetPrefix, 181 | handleConnect, 182 | handleDisconnect, 183 | handleAcknowledgement, 184 | getOffer, 185 | handleOffer, 186 | handleAnswer, 187 | handleCandidate, 188 | handleGoodbye, 189 | handleAlias 190 | ); 191 | 192 | // Socket handlers 193 | const handleExternalBind = async (alias: string) => { 194 | logger.verbose("Handling external bind", { alias }); 195 | 196 | await signalingClient.bind(alias); 197 | }; 198 | 199 | const handleExternalAccept = async (alias: string) => { 200 | logger.verbose("Handling external accept", { alias }); 201 | 202 | return await signalingClient.accept(alias); 203 | }; 204 | 205 | const handleExternalConnect = async (alias: string) => { 206 | logger.verbose("Handling external connect", { alias }); 207 | 208 | await signalingClient.connect(alias); 209 | }; 210 | 211 | const handleExternalSend = async (alias: string, msg: Uint8Array) => { 212 | logger.verbose("Handling external send", { alias, msg }); 213 | 214 | if (aliases.has(alias)) { 215 | return await transporter.send(aliases.get(alias)!, msg); // .has 216 | } else { 217 | logger.error("Could not find alias", { alias }); 218 | } 219 | }; 220 | 221 | const handleExternalRecv = async (alias: string) => { 222 | if (aliases.has(alias)) { 223 | const msg = await transporter.recv(aliases.get(alias)!); // .has 224 | 225 | logger.verbose("Handling external recv", { alias, msg }); 226 | 227 | return msg; 228 | } else { 229 | throw new AliasDoesNotExistError(); 230 | } 231 | }; 232 | 233 | const sockets = new Sockets( 234 | handleExternalBind, 235 | handleExternalAccept, 236 | handleExternalConnect, 237 | handleExternalSend, 238 | handleExternalRecv 239 | ); 240 | 241 | // WASM runner 242 | (async () => { 243 | await ready.once("ready"); 244 | 245 | if (useC) { 246 | if (useWASI) { 247 | const wasmFs = new WasmFs(); 248 | const wasi = new WASI({ 249 | args: [], 250 | env: {}, 251 | bindings: { 252 | ...wasiBindings, 253 | fs: wasmFs.fs, 254 | }, 255 | }); 256 | const { 257 | memoryId, 258 | imports: socketEnvImports, 259 | } = await sockets.getImports(); 260 | 261 | const module = await WebAssembly.compile( 262 | await lowerI64Imports( 263 | new Uint8Array(await (await fetch(binaryPath)).arrayBuffer()) 264 | ) 265 | ); 266 | const instance = await Asyncify.instantiate(module, { 267 | ...wasi.getImports(module), 268 | env: socketEnvImports, 269 | }); 270 | 271 | sockets.setMemory(memoryId, instance.exports.memory); 272 | 273 | wasi.start(instance); 274 | } 275 | } else if (useGo) { 276 | if (useJSSI) { 277 | const go = new Go(); 278 | const { 279 | memoryId, 280 | imports: socketEnvImports, 281 | } = await sockets.getImports(); 282 | 283 | const instance = await WebAssembly.instantiate( 284 | await WebAssembly.compile( 285 | await (await fetch(binaryPath)).arrayBuffer() 286 | ), 287 | go.importObject 288 | ); 289 | 290 | (global as any).fs.read = ( 291 | _: number, 292 | buffer: Uint8Array, 293 | ___: number, 294 | ____: number, 295 | _____: number, 296 | callback: Function 297 | ) => { 298 | new Promise((res) => { 299 | const rawInput = prompt("value for stdin:"); 300 | const input = new TextEncoder().encode(rawInput + "\n"); 301 | 302 | buffer.set(input); 303 | 304 | res(input); 305 | }).then((input) => callback(null, input.length)); 306 | }; 307 | (global as any).jssiImports = socketEnvImports; 308 | 309 | sockets.setMemory(memoryId, (instance.exports as any).mem); 310 | 311 | go.run(instance); 312 | } 313 | } else if (useTinyGo) { 314 | if (useJSSI) { 315 | const go = new TinyGo(); 316 | const { 317 | memoryId, 318 | imports: socketEnvImports, 319 | } = await sockets.getImports(); 320 | 321 | const instance = await WebAssembly.instantiate( 322 | await WebAssembly.compile( 323 | await (await fetch(binaryPath)).arrayBuffer() 324 | ), 325 | go.importObject 326 | ); 327 | 328 | (global as any).fs.read = ( 329 | _: number, 330 | buffer: Uint8Array, 331 | ___: number, 332 | ____: number, 333 | _____: number, 334 | callback: Function 335 | ) => { 336 | new Promise((res) => { 337 | const rawInput = prompt("value for stdin:"); 338 | const input = new TextEncoder().encode(rawInput + "\n"); 339 | 340 | buffer.set(input); 341 | 342 | res(input); 343 | }).then((input) => callback(null, input.length)); 344 | }; 345 | (global as any).jssiImports = socketEnvImports; 346 | 347 | sockets.setMemory(memoryId, (instance.exports as any).memory); 348 | 349 | go.run(instance); 350 | } else if (useWASI) { 351 | const wasmFs = new WasmFs(); 352 | const wasi = new WASI({ 353 | args: [], 354 | env: {}, 355 | bindings: { 356 | ...wasiBindings, 357 | fs: wasmFs.fs, 358 | }, 359 | }); 360 | const { 361 | memoryId, 362 | imports: socketEnvImports, 363 | } = await sockets.getImports(); 364 | const go = new TinyGo(); 365 | 366 | const module = await WebAssembly.compile( 367 | await lowerI64Imports( 368 | new Uint8Array(await (await fetch(binaryPath)).arrayBuffer()) 369 | ) 370 | ); 371 | const instance = await Asyncify.instantiate(module, { 372 | ...wasi.getImports(module), 373 | env: { 374 | ...go.importObject.env, 375 | ...socketEnvImports, 376 | }, 377 | }); 378 | 379 | sockets.setMemory(memoryId, instance.exports.memory); 380 | 381 | wasi.start(instance); 382 | } 383 | } 384 | })(); 385 | 386 | signalingClient.open(); 387 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alphahorizonio/unisockets 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { SignalingClient } from "./pkg/web/signaling/services/signaling-client"; 2 | export { SignalingServer } from "./pkg/web/signaling/services/signaling-server"; 3 | export { Sockets } from "./pkg/web/sockets/sockets"; 4 | export { Transporter } from "./pkg/web/transport/transporter"; 5 | export { ExtendedRTCConfiguration } from "wrtc"; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@alphahorizonio/unisockets", 3 | "version": "0.1.1", 4 | "description": "A universal Berkeley sockets implementation for both WebAssembly (based on WebRTC) and native platforms with bindings for C, Go and TinyGo.", 5 | "source": "index.ts", 6 | "main": "dist/nodeLib/index.js", 7 | "module": "dist/webLib/index.js", 8 | "types": "dist/index.d.ts", 9 | "binSource": "cmd/unisockets_runner/main.ts", 10 | "binMain": "dist/nodeApp/main.js", 11 | "scripts": { 12 | "build": "rm -rf dist && yarn build:lib && yarn build:app:node && yarn build:app:web", 13 | "build:lib": "rollup -c", 14 | "build:app:node": "rollup -c && chmod +x dist/nodeApp/main.js && chmod +x dist/nodeApp/main.js", 15 | "build:app:web": "mkdir -p dist/webApp out && cp -r out dist/webApp && parcel build --target webApp cmd/unisockets_runner_web/index.html", 16 | "build:diagram": "mkdir -p dist/media && tplant -p tsconfig.json -i 'pkg/**/*.ts*' --output 'dist/media/diagram.svg'", 17 | "build:protocol": "mkdir -p dist/media && puml generate --svg protocol.puml -o dist/media/sequence.svg", 18 | "build:docs": "mkdir -p dist/docs && typedoc --tsconfig tsconfig.json --mode file --outDir dist/docs --out dist/docs --media dist/media --exclude '+(node_modules|dist)' .", 19 | "test": "jest", 20 | "test:update": "jest --updateSnapshot", 21 | "dev:tests": "jest ---watchAll", 22 | "dev:lib": "rollup -c -w", 23 | "dev:app:node:signaling-server": "LOG_LEVEL=debug DEBUG='' ts-node-dev --files cmd/unisockets_runner/main.ts --runSignalingServer true", 24 | "dev:app:node:example-server": "LOG_LEVEL=debug DEBUG='' ts-node-dev --files cmd/unisockets_runner/main.ts --runBinary true --useC true --useWASI true --binaryPath out/c/echo_server.wasm", 25 | "dev:app:node:example-client": "LOG_LEVEL=debug DEBUG='' ts-node-dev --files cmd/unisockets_runner/main.ts --runBinary true --useC true --useWASI true --binaryPath out/c/echo_client.wasm", 26 | "dev:app:web": "mkdir -p dist/webApp out && cp -r out dist/webApp && parcel --hmr-port 1235 --target webApp cmd/unisockets_runner_web/index.html", 27 | "start": "yarn start:app:node:signaling-server", 28 | "start:app:node:signaling-server": "./dist/nodeApp/main.js --runSignalingServer true", 29 | "start:app:node:example-server": "./dist/nodeApp/main.js --runBinary true --useC true --useWASI true --binaryPath out/c/echo_server.wasm", 30 | "start:app:node:example-client": "./dist/nodeApp/main.js --runBinary true --useC true --useWASI true --binaryPath out/c/echo_client.wasm", 31 | "start:app:web": "serve dist/webApp", 32 | "start:docs": "serve dist/docs" 33 | }, 34 | "bin": { 35 | "unisockets_runner": "dist/nodeApp/main.js" 36 | }, 37 | "files": [ 38 | "dist/nodeApp", 39 | "dist/nodeLib", 40 | "dist/webLib", 41 | "dist/index.d.ts" 42 | ], 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/alphahorizonio/unisockets.git" 46 | }, 47 | "author": "Felicitas Pojtinger ", 48 | "license": "AGPL-3.0", 49 | "bugs": { 50 | "url": "https://github.com/alphahorizonio/unisockets/issues" 51 | }, 52 | "homepage": "https://github.com/alphahorizonio/unisockets#readme", 53 | "devDependencies": { 54 | "@types/jest": "^26.0.16", 55 | "@types/uuid": "^8.3.0", 56 | "@types/ws": "^7.4.0", 57 | "@types/yargs": "^15.0.9", 58 | "esbuild": "^0.8.20", 59 | "get-port": "^5.1.1", 60 | "jest": "^26.6.3", 61 | "parcel": "^2.0.0-nightly.469", 62 | "rollup": "^2.34.2", 63 | "rollup-plugin-dts": "^2.0.0", 64 | "rollup-plugin-esbuild": "^2.6.0", 65 | "rollup-plugin-hashbang": "^2.2.2", 66 | "tplant": "^2.3.6", 67 | "ts-jest": "^26.4.4", 68 | "ts-node-dev": "^1.0.0", 69 | "typedoc": "^0.19.2", 70 | "typescript": "^4.0.5" 71 | }, 72 | "dependencies": { 73 | "@wasmer/wasi": "^0.12.0", 74 | "@wasmer/wasm-transformer": "^0.12.0", 75 | "@wasmer/wasmfs": "^0.12.0", 76 | "async-mutex": "^0.2.6", 77 | "asyncify-wasm": "^1.1.1", 78 | "emittery": "^0.7.2", 79 | "isomorphic-ws": "^4.0.1", 80 | "serve": "^11.3.2", 81 | "uuid": "^8.3.1", 82 | "winston": "^3.3.3", 83 | "wrtc": "^0.4.6", 84 | "ws": "^7.4.0", 85 | "yargs": "^16.1.0" 86 | }, 87 | "targets": { 88 | "webApp": { 89 | "distDir": "dist/webApp", 90 | "engines": { 91 | "browsers": ">=5%" 92 | } 93 | } 94 | }, 95 | "jest": { 96 | "rootDir": "pkg", 97 | "preset": "ts-jest", 98 | "testEnvironment": "node", 99 | "testMatch": [ 100 | "/**/*.test.ts" 101 | ], 102 | "coverageThreshold": { 103 | "global": { 104 | "branches": 0, 105 | "functions": 0, 106 | "lines": 0, 107 | "statements": 0 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/unisockets/unisockets.go: -------------------------------------------------------------------------------- 1 | package unisockets 2 | 3 | type SockaddrIn struct { 4 | SinFamily uint16 5 | SinPort uint16 6 | SinAddr struct { 7 | SAddr uint32 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /pkg/unisockets/unisockets.h: -------------------------------------------------------------------------------- 1 | #ifndef UNISOCKETS_H 2 | #define UNISOCKETS_H 3 | 4 | #ifdef UNISOCKETS_WITH_CUSTOM_ARPA_INET 5 | #define _Addr long 6 | typedef _Addr ssize_t; 7 | typedef unsigned _Addr size_t; 8 | typedef unsigned socklen_t; 9 | typedef unsigned short sa_family_t; 10 | typedef unsigned char uint8_t; 11 | typedef unsigned short uint16_t; 12 | typedef unsigned int uint32_t; 13 | typedef uint16_t in_port_t; 14 | typedef uint32_t in_addr_t; 15 | 16 | struct sockaddr { 17 | sa_family_t sa_family; 18 | char sa_data[14]; 19 | }; 20 | struct in_addr { 21 | in_addr_t s_addr; 22 | }; 23 | struct sockaddr_in { 24 | sa_family_t sin_family; 25 | in_port_t sin_port; 26 | struct in_addr sin_addr; 27 | uint8_t sin_zero[8]; 28 | }; 29 | 30 | #define PF_INET 2 31 | #define SOCK_STREAM 1 32 | #define SHUT_RDWR 2 33 | 34 | uint16_t htons(uint16_t v) { return (v >> 8) | (v << 8); } 35 | #else 36 | #include 37 | #endif 38 | 39 | typedef struct sockaddr sockaddr; 40 | typedef struct sockaddr_in sockaddr_in; 41 | typedef struct in_addr in_addr; 42 | 43 | #ifndef UNISOCKETS_WITH_INVERSE_ALIAS 44 | int unisockets_socket(int, int, int); 45 | int unisockets_connect(int, const struct sockaddr *, socklen_t); 46 | ssize_t unisockets_send(int, const void *, size_t, int); 47 | ssize_t unisockets_recv(int, void *, size_t, int); 48 | int unisockets_bind(int, const struct sockaddr *, socklen_t); 49 | int unisockets_listen(int, int); 50 | int unisockets_accept(int, struct sockaddr *, socklen_t *); 51 | #endif 52 | 53 | #ifdef UNISOCKETS_WITH_ALIAS 54 | #define socket unisockets_socket 55 | #define connect unisockets_connect 56 | #define send unisockets_send 57 | #define recv unisockets_recv 58 | #define bind unisockets_bind 59 | #define listen unisockets_listen 60 | #define accept unisockets_accept 61 | #endif 62 | 63 | #ifdef UNISOCKETS_WITH_INVERSE_ALIAS 64 | #define unisockets_socket socket 65 | #define unisockets_connect connect 66 | #define unisockets_send send 67 | #define unisockets_recv recv 68 | #define unisockets_bind bind 69 | #define unisockets_listen listen 70 | #define unisockets_accept accept 71 | #endif 72 | 73 | #endif /* UNISOCKETS_H */ -------------------------------------------------------------------------------- /pkg/unisockets/unisockets_native_posix_go.go: -------------------------------------------------------------------------------- 1 | // +build !js,!tinygo 2 | 3 | package unisockets 4 | 5 | /* 6 | #cgo CFLAGS: -DUNISOCKETS_WITH_INVERSE_ALIAS 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "unisockets.h" 13 | */ 14 | import "C" 15 | import ( 16 | "unsafe" 17 | ) 18 | 19 | const ( 20 | PF_INET = uint16(C.PF_INET) 21 | SOCK_STREAM = int32(C.SOCK_STREAM) 22 | SHUT_RDWR = int32(C.SHUT_RDWR) 23 | ) 24 | 25 | func Socket(socketDomain uint16, socketType int32, socketProtocol int32) (int32, error) { 26 | rv, err := C.socket(C.int(socketDomain), C.int(socketType), C.int(socketProtocol)) 27 | 28 | return int32(rv), err 29 | } 30 | 31 | func Bind(socketFd int32, socketAddr *SockaddrIn) error { 32 | addr := C.sockaddr_in{ 33 | sin_family: C.ushort(socketAddr.SinFamily), 34 | sin_port: C.ushort(socketAddr.SinPort), 35 | sin_addr: C.in_addr{ 36 | s_addr: C.uint(socketAddr.SinAddr.SAddr), 37 | }, 38 | } 39 | 40 | _, err := C.bind(C.int(socketFd), (*C.sockaddr)(unsafe.Pointer(&addr)), C.uint(unsafe.Sizeof(addr))) 41 | 42 | return err 43 | } 44 | 45 | func Listen(socketFd int32, socketBacklog int32) error { 46 | _, err := C.listen(C.int(socketFd), C.int(socketBacklog)) 47 | 48 | return err 49 | } 50 | 51 | func Accept(socketFd int32, socketAddr *SockaddrIn) (int32, error) { 52 | addr := C.sockaddr_in{ 53 | sin_family: C.ushort(socketAddr.SinFamily), 54 | sin_port: C.ushort(socketAddr.SinPort), 55 | sin_addr: C.in_addr{ 56 | s_addr: C.uint(socketAddr.SinAddr.SAddr), 57 | }, 58 | } 59 | 60 | addrLen := C.uint(unsafe.Sizeof(socketAddr)) 61 | 62 | rv, err := C.accept(C.int(socketFd), (*C.sockaddr)(unsafe.Pointer(&addr)), &addrLen) 63 | if err != nil { 64 | return int32(rv), err 65 | } 66 | 67 | socketAddr.SinFamily = uint16(addr.sin_family) 68 | socketAddr.SinPort = uint16(addr.sin_port) 69 | socketAddr.SinAddr.SAddr = uint32(addr.sin_addr.s_addr) 70 | 71 | return int32(rv), err 72 | } 73 | 74 | func Recv(socketFd int32, socketReceivedMessage *[]byte, socketBufferLength uint32, socketFlags int32) (int32, error) { 75 | receivedMessage := make([]byte, socketBufferLength) 76 | 77 | rv, err := C.recv(C.int(socketFd), unsafe.Pointer(&receivedMessage[0]), C.ulong(socketBufferLength), C.int(socketFlags)) 78 | if err != nil { 79 | return int32(rv), err 80 | } 81 | 82 | outReceivedMessage := []byte(receivedMessage) 83 | 84 | *socketReceivedMessage = outReceivedMessage 85 | 86 | return int32(rv), err 87 | } 88 | 89 | func Send(socketFd int32, socketMessageToSend []byte, socketFlags int32) (int32, error) { 90 | rv, err := C.send(C.int(socketFd), unsafe.Pointer(&socketMessageToSend[0]), C.ulong(len(socketMessageToSend)), C.int(socketFlags)) 91 | 92 | return int32(rv), err 93 | } 94 | 95 | func Shutdown(socketFd int32, socketFlags int32) error { 96 | _, err := C.shutdown(C.int(socketFd), C.int(socketFlags)) 97 | 98 | return err 99 | } 100 | 101 | func Connect(socketFd int32, socketAddr *SockaddrIn) error { 102 | addr := C.sockaddr_in{ 103 | sin_family: C.ushort(socketAddr.SinFamily), 104 | sin_port: C.ushort(socketAddr.SinPort), 105 | sin_addr: C.in_addr{ 106 | s_addr: C.uint(socketAddr.SinAddr.SAddr), 107 | }, 108 | } 109 | 110 | _, err := C.connect(C.int(socketFd), (*C.sockaddr)(unsafe.Pointer(&addr)), C.uint(unsafe.Sizeof(addr))) 111 | 112 | return err 113 | } 114 | 115 | func Htons(v uint16) uint16 { 116 | return uint16(C.htons(C.ushort(v))) 117 | } 118 | -------------------------------------------------------------------------------- /pkg/unisockets/unisockets_native_posix_tinygo.go: -------------------------------------------------------------------------------- 1 | // +build !js,tinygo,!wasi 2 | 3 | package unisockets 4 | 5 | /* 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "unisockets.h" 11 | */ 12 | import "C" 13 | import "unsafe" 14 | 15 | func socket(a C.int, b C.int, c C.int) C.int { 16 | return C.socket(a, b, c) 17 | } 18 | 19 | func connect(a C.int, b *C.sockaddr, c C.uint) C.int { 20 | return C.connect(a, b, c) 21 | } 22 | 23 | func bind(a C.int, b *C.sockaddr, c C.uint) C.int { 24 | return C.bind(a, b, c) 25 | } 26 | 27 | func listen(a C.int, b C.int) C.int { 28 | return C.listen(a, b) 29 | } 30 | 31 | func accept(a C.int, b *C.sockaddr, c *C.uint) C.int { 32 | return C.accept(a, b, c) 33 | } 34 | 35 | func recv(a C.int, b unsafe.Pointer, c C.ulong, d C.int) C.long { 36 | return C.recv(a, b, c, d) 37 | } 38 | 39 | func send(a C.int, b unsafe.Pointer, c C.ulong, d C.int) C.long { 40 | return C.send(a, b, c, d) 41 | } 42 | 43 | func shutdown(a C.int, b C.int) C.int { 44 | return C.shutdown(a, b) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/unisockets/unisockets_tinygo.go: -------------------------------------------------------------------------------- 1 | // +build !js,tinygo 2 | 3 | package unisockets 4 | 5 | /* 6 | #include 7 | #include 8 | #include 9 | #include 10 | */ 11 | import "C" 12 | import ( 13 | "fmt" 14 | "unsafe" 15 | ) 16 | 17 | const ( 18 | PF_INET = uint16(C.PF_INET) 19 | SOCK_STREAM = int32(C.SOCK_STREAM) 20 | SHUT_RDWR = int32(C.SHUT_RDWR) 21 | ) 22 | 23 | func Socket(socketDomain uint16, socketType int32, socketProtocol int32) (int32, error) { 24 | rv := int32(socket(C.int(socketDomain), C.int(socketType), C.int(socketProtocol))) 25 | if rv == -1 { 26 | return rv, fmt.Errorf("could not create socket, error code %v", rv) 27 | } 28 | 29 | return rv, nil 30 | } 31 | 32 | func Bind(socketFd int32, socketAddr *SockaddrIn) error { 33 | addr := C.sockaddr_in{ 34 | sin_family: C.ushort(socketAddr.SinFamily), 35 | sin_port: uint16(socketAddr.SinPort), 36 | sin_addr: C.in_addr{ 37 | s_addr: uint32(socketAddr.SinAddr.SAddr), 38 | }, 39 | } 40 | 41 | if rv := int32(bind(C.int(socketFd), (*C.sockaddr)(unsafe.Pointer(&addr)), C.uint(unsafe.Sizeof(addr)))); rv == -1 { 42 | return fmt.Errorf("could not bind socket, error code %v", rv) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | func Listen(socketFd int32, socketBacklog int32) error { 49 | if rv := int32(listen(C.int(socketFd), C.int(socketBacklog))); rv == -1 { 50 | return fmt.Errorf("could not listen on socket, error code %v", rv) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func Accept(socketFd int32, socketAddr *SockaddrIn) (int32, error) { 57 | addr := C.sockaddr_in{ 58 | sin_family: C.ushort(socketAddr.SinFamily), 59 | sin_port: uint16(socketAddr.SinPort), 60 | sin_addr: C.in_addr{ 61 | s_addr: uint32(socketAddr.SinAddr.SAddr), 62 | }, 63 | } 64 | 65 | addrLen := C.uint(unsafe.Sizeof(socketAddr)) 66 | 67 | rv := int32(accept(C.int(socketFd), (*C.sockaddr)(unsafe.Pointer(&addr)), &addrLen)) 68 | if rv == -1 { 69 | return rv, fmt.Errorf("could not accept on socket, error code %v", rv) 70 | } 71 | 72 | socketAddr.SinFamily = uint16(addr.sin_family) 73 | socketAddr.SinPort = uint16(addr.sin_port) 74 | socketAddr.SinAddr.SAddr = uint32(addr.sin_addr.s_addr) 75 | 76 | return rv, nil 77 | } 78 | 79 | func Recv(socketFd int32, socketReceivedMessage *[]byte, socketBufferLength uint32, socketFlags int32) (int32, error) { 80 | receivedMessage := make([]byte, socketBufferLength) 81 | 82 | rv := int32(recv(C.int(socketFd), unsafe.Pointer(&receivedMessage[0]), C.ulong(socketBufferLength), C.int(socketFlags))) 83 | if rv == -1 { 84 | return rv, fmt.Errorf("could not receive from socket, error code %v", rv) 85 | } 86 | 87 | outReceivedMessage := []byte(receivedMessage) 88 | 89 | *socketReceivedMessage = outReceivedMessage 90 | 91 | return rv, nil 92 | } 93 | 94 | func Send(socketFd int32, socketMessageToSend []byte, socketFlags int32) (int32, error) { 95 | rv := int32(send(C.int(socketFd), unsafe.Pointer(&socketMessageToSend[0]), C.ulong(len(socketMessageToSend)), C.int(socketFlags))) 96 | if rv == -1 { 97 | return rv, fmt.Errorf("could not send from socket, error code %v", rv) 98 | } 99 | 100 | return rv, nil 101 | } 102 | 103 | func Shutdown(socketFd int32, socketFlags int32) error { 104 | if rv := shutdown(C.int(socketFd), C.int(socketFlags)); rv == -1 { 105 | return fmt.Errorf("could not shut down socket, error code %v", rv) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func Connect(socketFd int32, socketAddr *SockaddrIn) error { 112 | addr := C.sockaddr_in{ 113 | sin_family: C.ushort(socketAddr.SinFamily), 114 | sin_port: uint16(socketAddr.SinPort), 115 | sin_addr: C.in_addr{ 116 | s_addr: uint32(socketAddr.SinAddr.SAddr), 117 | }, 118 | } 119 | 120 | if rv := connect(C.int(socketFd), (*C.sockaddr)(unsafe.Pointer(&addr)), C.uint(unsafe.Sizeof(addr))); rv == -1 { 121 | return fmt.Errorf("could not connect to socket, error code %v", rv) 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func Htons(v uint16) uint16 { 128 | return uint16(C.htons(v)) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/unisockets/unisockets_wasm_jssi.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package unisockets 4 | 5 | import ( 6 | "fmt" 7 | "syscall/js" 8 | "unsafe" 9 | ) 10 | 11 | // TODO: Get this to work with TinyGo WASM/JS 12 | 13 | var ( 14 | jssiImports = js.Global().Get("jssiImports") 15 | ) 16 | 17 | const ( 18 | PF_INET = 2 19 | SOCK_STREAM = 1 20 | SHUT_RDWR = 2 21 | ) 22 | 23 | func Socket(socketDomain int32, socketType int32, socketProtocol int32) (int32, error) { 24 | rvChan := make(chan int32) 25 | 26 | go jssiImports.Call("unisockets_socket", socketDomain, socketType, socketProtocol).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 27 | rvChan <- int32(args[0].Int()) 28 | 29 | return nil 30 | })) 31 | 32 | rv := <-rvChan 33 | if rv == -1 { 34 | return rv, fmt.Errorf("could not create socket, error code %v", rv) 35 | } 36 | 37 | return rv, nil 38 | } 39 | 40 | func Bind(socketFd int32, socketAddr *SockaddrIn) error { 41 | rvChan := make(chan int32) 42 | 43 | go jssiImports.Call("unisockets_bind", socketFd, unsafe.Pointer(socketAddr), uint32(unsafe.Sizeof(socketAddr))).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 44 | rvChan <- int32(args[0].Int()) 45 | 46 | return nil 47 | })) 48 | 49 | rv := <-rvChan 50 | if rv == -1 { 51 | return fmt.Errorf("could not bind socket, error code %v", rv) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func Listen(socketFd int32, socketBacklog int32) error { 58 | rvChan := make(chan int32) 59 | 60 | go jssiImports.Call("unisockets_listen", socketFd, socketBacklog).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 61 | rvChan <- int32(args[0].Int()) 62 | 63 | return nil 64 | })) 65 | 66 | rv := <-rvChan 67 | if rv == -1 { 68 | return fmt.Errorf("could not listen on socket, error code %v", rv) 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func Accept(socketFd int32, socketAddr *SockaddrIn) (int32, error) { 75 | rvChan := make(chan int32) 76 | 77 | socketAddressLength := uint32(unsafe.Sizeof(socketAddr)) 78 | 79 | go jssiImports.Call("unisockets_accept", socketFd, unsafe.Pointer(socketAddr), unsafe.Pointer(&socketAddressLength)).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 80 | rvChan <- int32(args[0].Int()) 81 | 82 | return nil 83 | })) 84 | 85 | rv := <-rvChan 86 | if rv == -1 { 87 | return rv, fmt.Errorf("could not accept on socket, error code %v", rv) 88 | } 89 | 90 | return rv, nil 91 | } 92 | 93 | func Recv(socketFd int32, socketReceivedMessage *[]byte, socketBufferLength uint32, socketFlags int32) (int32, error) { 94 | rvChan := make(chan int32) 95 | 96 | go jssiImports.Call("unisockets_recv", socketFd, unsafe.Pointer(&(*socketReceivedMessage)[0]), socketBufferLength, socketFlags).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 97 | rvChan <- int32(args[0].Int()) 98 | 99 | return nil 100 | })) 101 | 102 | rv := <-rvChan 103 | 104 | if rv == -1 { 105 | return rv, fmt.Errorf("could not receive from socket, error code %v", rv) 106 | } 107 | 108 | return rv, nil 109 | } 110 | 111 | func Send(socketFd int32, socketMessageToSend []byte, socketFlags int32) (int32, error) { 112 | rvChan := make(chan int32) 113 | 114 | go jssiImports.Call("unisockets_send", socketFd, unsafe.Pointer(&socketMessageToSend[0]), uint32(len(socketMessageToSend)), socketFlags).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 115 | rvChan <- int32(args[0].Int()) 116 | 117 | return nil 118 | })) 119 | 120 | rv := <-rvChan 121 | 122 | if rv == -1 { 123 | return rv, fmt.Errorf("could not send from socket, error code %v", rv) 124 | } 125 | 126 | return rv, nil 127 | } 128 | 129 | func Shutdown(socketFd int32, socketFlags int32) error { 130 | // Not necessary on WASM 131 | 132 | return nil 133 | } 134 | 135 | func Connect(socketFd int32, socketAddr *SockaddrIn) error { 136 | rvChan := make(chan int32) 137 | 138 | go jssiImports.Call("unisockets_connect", socketFd, unsafe.Pointer(socketAddr), uint32(unsafe.Sizeof(socketAddr))).Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 139 | rvChan <- int32(args[0].Int()) 140 | 141 | return nil 142 | })) 143 | 144 | rv := <-rvChan 145 | if rv == -1 { 146 | return fmt.Errorf("could not connect to socket, error code %v", rv) 147 | } 148 | 149 | return nil 150 | } 151 | 152 | func Htons(v uint16) uint16 { 153 | return (v >> 8) | (v << 8) 154 | } 155 | -------------------------------------------------------------------------------- /pkg/unisockets/unisockets_wasm_wasi_tinygo.go: -------------------------------------------------------------------------------- 1 | // +build !js,tinygo,wasi 2 | 3 | package unisockets 4 | 5 | /* 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "unisockets.h" 11 | */ 12 | import "C" 13 | import "unsafe" 14 | 15 | func socket(a C.int, b C.int, c C.int) C.int { 16 | return C.unisockets_socket(a, b, c) 17 | } 18 | 19 | func connect(a C.int, b *C.sockaddr, c C.uint) C.int { 20 | return C.unisockets_connect(a, b, c) 21 | } 22 | 23 | func bind(a C.int, b *C.sockaddr, c C.uint) C.int { 24 | return C.unisockets_bind(a, b, c) 25 | } 26 | 27 | func listen(a C.int, b C.int) C.int { 28 | return C.unisockets_listen(a, b) 29 | } 30 | 31 | func accept(a C.int, b *C.sockaddr, c *C.uint) C.int { 32 | return C.unisockets_accept(a, b, c) 33 | } 34 | 35 | func recv(a C.int, b unsafe.Pointer, c C.ulong, d C.int) C.long { 36 | return C.unisockets_recv(a, b, c, d) 37 | } 38 | 39 | func send(a C.int, b unsafe.Pointer, c C.ulong, d C.int) C.long { 40 | return C.unisockets_send(a, b, c, d) 41 | } 42 | 43 | func shutdown(a C.int, b C.int) C.int { 44 | // Not necessary on WASM 45 | 46 | return C.int(0) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/alias-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class AliasDoesNotExistError extends Error { 2 | constructor() { 3 | super("alias does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/bind-rejected.ts: -------------------------------------------------------------------------------- 1 | export class BindRejectedError extends Error { 2 | constructor(idAlias: string) { 3 | super(`bind rejected by server: ${idAlias}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/channel-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class ChannelDoesNotExistError extends Error { 2 | constructor() { 3 | super("channel does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/client-closed.ts: -------------------------------------------------------------------------------- 1 | export class ClientClosedError extends Error { 2 | constructor() { 3 | super("client is closed"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/client-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class ClientDoesNotExistError extends Error { 2 | constructor() { 3 | super("client does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/connection-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class ConnectionDoesNotExistError extends Error { 2 | constructor() { 3 | super("connection does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/connection-rejected.ts: -------------------------------------------------------------------------------- 1 | export class ConnectionRejectedError extends Error { 2 | constructor(idAlias: string) { 3 | super(`connection rejected by server: ${idAlias}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/memory-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class MemoryDoesNotExistError extends Error { 2 | constructor() { 3 | super("memory does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/port-already-allocated-error.ts: -------------------------------------------------------------------------------- 1 | export class PortAlreadyAllocatedError extends Error { 2 | constructor() { 3 | super("port already allocated"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/sdp-invalid.ts: -------------------------------------------------------------------------------- 1 | export class SDPInvalidError extends Error { 2 | constructor() { 3 | super("sdp invalid"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/shutdown-rejected.ts: -------------------------------------------------------------------------------- 1 | export class ShutdownRejectedError extends Error { 2 | constructor(idAlias: string) { 3 | super(`shutdown rejected by server: ${idAlias}`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/socket-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class SocketDoesNotExistError extends Error { 2 | constructor() { 3 | super("socket does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/subnet-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class SubnetDoesNotExistError extends Error { 2 | constructor() { 3 | super("subnet does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/suffix-does-not-exist.ts: -------------------------------------------------------------------------------- 1 | export class SuffixDoesNotExistError extends Error { 2 | constructor() { 3 | super("suffix does not exist"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pkg/web/signaling/errors/unimplemented-operation.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES } from "../operations/operation"; 2 | 3 | export class UnimplementedOperationError extends Error { 4 | constructor(opcode: ESIGNALING_OPCODES) { 5 | super(`unimplemented operation ${opcode}`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /pkg/web/signaling/models/alias.ts: -------------------------------------------------------------------------------- 1 | export class MAlias { 2 | constructor(public id: string, public accepting: boolean) {} 3 | } 4 | -------------------------------------------------------------------------------- /pkg/web/signaling/models/member.ts: -------------------------------------------------------------------------------- 1 | export class MMember { 2 | constructor(public ports: number[]) {} 3 | } 4 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/accept.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IAcceptData { 4 | boundAlias: string; 5 | clientAlias: string; 6 | } 7 | 8 | export class Accept implements ISignalingOperation { 9 | opcode = ESIGNALING_OPCODES.ACCEPT; 10 | 11 | constructor(public data: IAcceptData) {} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/accepting.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IAcceptingData { 4 | id: string; 5 | alias: string; 6 | } 7 | 8 | export class Accepting implements ISignalingOperation { 9 | opcode = ESIGNALING_OPCODES.ACCEPTING; 10 | 11 | constructor(public data: IAcceptingData) {} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/acknowledgement.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IAcknowledgementData { 4 | id: string; 5 | rejected: boolean; 6 | } 7 | 8 | export class Acknowledgement 9 | implements ISignalingOperation { 10 | opcode = ESIGNALING_OPCODES.ACKNOWLEDGED; 11 | 12 | constructor(public data: IAcknowledgementData) {} 13 | } 14 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/alias.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IAliasData { 4 | id: string; 5 | alias: string; 6 | set: boolean; 7 | clientConnectionId?: string; 8 | isConnectionAlias?: boolean; 9 | } 10 | 11 | export class Alias implements ISignalingOperation { 12 | opcode = ESIGNALING_OPCODES.ALIAS; 13 | 14 | constructor(public data: IAliasData) {} 15 | } 16 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/answer.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IAnswerData { 4 | offererId: string; 5 | answererId: string; 6 | answer: string; 7 | } 8 | 9 | export class Answer implements ISignalingOperation { 10 | opcode = ESIGNALING_OPCODES.ANSWER; 11 | 12 | constructor(public data: IAnswerData) {} 13 | } 14 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/bind.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IBindData { 4 | id: string; 5 | alias: string; 6 | } 7 | 8 | export class Bind implements ISignalingOperation { 9 | opcode = ESIGNALING_OPCODES.BIND; 10 | 11 | constructor(public data: IBindData) {} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/candidate.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface ICandidateData { 4 | offererId: string; // Who this candidate describes 5 | answererId: string; // Who this candidate should be delivered to 6 | candidate: string; 7 | } 8 | 9 | export class Candidate implements ISignalingOperation { 10 | opcode = ESIGNALING_OPCODES.CANDIDATE; 11 | 12 | constructor(public data: ICandidateData) {} 13 | } 14 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/connect.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IConnectData { 4 | id: string; 5 | remoteAlias: string; 6 | clientConnectionId: string; 7 | } 8 | 9 | export class Connect implements ISignalingOperation { 10 | opcode = ESIGNALING_OPCODES.CONNECT; 11 | 12 | constructor(public data: IConnectData) {} 13 | } 14 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/goodbye.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IGoodbyeData { 4 | id: string; 5 | } 6 | 7 | export class Goodbye implements ISignalingOperation { 8 | opcode = ESIGNALING_OPCODES.GOODBYE; 9 | 10 | constructor(public data: IGoodbyeData) {} 11 | } 12 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/greeting.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IGreetingData { 4 | offererId: string; 5 | answererId: string; 6 | } 7 | 8 | export class Greeting implements ISignalingOperation { 9 | opcode = ESIGNALING_OPCODES.GREETING; 10 | 11 | constructor(public data: IGreetingData) {} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/knock.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IKnockData { 4 | subnet: string; 5 | } 6 | 7 | export class Knock implements ISignalingOperation { 8 | opcode = ESIGNALING_OPCODES.KNOCK; 9 | 10 | constructor(public data: IKnockData) {} 11 | } 12 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/offer.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IOfferData { 4 | offererId: string; 5 | answererId: string; 6 | offer: string; 7 | } 8 | 9 | export class Offer implements ISignalingOperation { 10 | opcode = ESIGNALING_OPCODES.OFFER; 11 | 12 | constructor(public data: IOfferData) {} 13 | } 14 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/operation.ts: -------------------------------------------------------------------------------- 1 | import { IAcceptData } from "./accept"; 2 | import { IAcceptingData } from "./accepting"; 3 | import { IAcknowledgementData } from "./acknowledgement"; 4 | import { IAliasData } from "./alias"; 5 | import { IAnswerData } from "./answer"; 6 | import { IBindData } from "./bind"; 7 | import { ICandidateData } from "./candidate"; 8 | import { IConnectData } from "./connect"; 9 | import { IGoodbyeData } from "./goodbye"; 10 | import { IGreetingData } from "./greeting"; 11 | import { IKnockData } from "./knock"; 12 | import { IOfferData } from "./offer"; 13 | import { IShutdownData } from "./shutdown"; 14 | 15 | export enum ESIGNALING_OPCODES { 16 | GOODBYE = "goodbye", 17 | KNOCK = "knock", 18 | ACKNOWLEDGED = "acknowledged", 19 | GREETING = "greeting", 20 | OFFER = "offer", 21 | ANSWER = "answer", 22 | CANDIDATE = "candidate", 23 | BIND = "bind", 24 | ACCEPTING = "accepting", 25 | ALIAS = "alias", 26 | SHUTDOWN = "shutdown", 27 | CONNECT = "connect", 28 | ACCEPT = "accept", 29 | } 30 | 31 | export type TSignalingData = 32 | | IGoodbyeData 33 | | IKnockData 34 | | IAcknowledgementData 35 | | IGreetingData 36 | | IOfferData 37 | | IAnswerData 38 | | ICandidateData 39 | | IBindData 40 | | IAcceptingData 41 | | IAliasData 42 | | IShutdownData 43 | | IConnectData 44 | | IAcceptData; 45 | 46 | export interface ISignalingOperation { 47 | opcode: ESIGNALING_OPCODES; 48 | 49 | data: T; 50 | } 51 | -------------------------------------------------------------------------------- /pkg/web/signaling/operations/shutdown.ts: -------------------------------------------------------------------------------- 1 | import { ESIGNALING_OPCODES, ISignalingOperation } from "./operation"; 2 | 3 | export interface IShutdownData { 4 | id: string; 5 | alias: string; 6 | } 7 | 8 | export class Shutdown implements ISignalingOperation { 9 | opcode = ESIGNALING_OPCODES.SHUTDOWN; 10 | 11 | constructor(public data: IShutdownData) {} 12 | } 13 | -------------------------------------------------------------------------------- /pkg/web/signaling/services/signaling-client.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "isomorphic-ws"; 2 | import { UnimplementedOperationError } from "../errors/unimplemented-operation"; 3 | import { IAcknowledgementData } from "../operations/acknowledgement"; 4 | import { IAliasData } from "../operations/alias"; 5 | import { Answer, IAnswerData } from "../operations/answer"; 6 | import { Candidate, ICandidateData } from "../operations/candidate"; 7 | import { IGoodbyeData } from "../operations/goodbye"; 8 | import { IOfferData, Offer } from "../operations/offer"; 9 | import { 10 | ESIGNALING_OPCODES, 11 | ISignalingOperation, 12 | TSignalingData, 13 | } from "../operations/operation"; 14 | import { SignalingService } from "./signaling-service"; 15 | import Emittery from "emittery"; 16 | import { Bind } from "../operations/bind"; 17 | import { BindRejectedError } from "../errors/bind-rejected"; 18 | import { ShutdownRejectedError } from "../errors/shutdown-rejected"; 19 | import { Shutdown } from "../operations/shutdown"; 20 | import { v4 } from "uuid"; 21 | import { ConnectionRejectedError } from "../errors/connection-rejected"; 22 | import { Connect } from "../operations/connect"; 23 | import { Accepting } from "../operations/accepting"; 24 | import { IAcceptData } from "../operations/accept"; 25 | import { IGreetingData } from "../operations/greeting"; 26 | import { Knock } from "../operations/knock"; 27 | 28 | export class SignalingClient extends SignalingService { 29 | private id = ""; 30 | private client?: WebSocket; 31 | private asyncResolver = new Emittery(); 32 | 33 | constructor( 34 | private address: string, 35 | private reconnectDuration: number, 36 | private subnet: string, 37 | private onConnect: () => Promise, 38 | private onDisconnect: () => Promise, 39 | private onAcknowledgement: (id: string, rejected: boolean) => Promise, 40 | private getOffer: ( 41 | answererId: string, 42 | handleCandidate: (candidate: string) => Promise 43 | ) => Promise, 44 | private getAnswer: ( 45 | offererId: string, 46 | offer: string, 47 | handleCandidate: (candidate: string) => Promise 48 | ) => Promise, 49 | private onAnswer: ( 50 | offererId: string, 51 | answererId: string, 52 | answer: string 53 | ) => Promise, 54 | private onCandidate: ( 55 | offererId: string, 56 | answererId: string, 57 | candidate: string 58 | ) => Promise, 59 | private onGoodbye: (id: string) => Promise, 60 | private onAlias: (id: string, alias: string, set: boolean) => Promise 61 | ) { 62 | super(); 63 | } 64 | 65 | async open() { 66 | this.logger.debug("Opening signaling client"); 67 | 68 | this.client = new WebSocket(this.address); 69 | this.client.onmessage = async (operation) => 70 | await this.handleOperation(await this.receive(operation.data)); 71 | this.client.onerror = async (e) => { 72 | this.logger.error("WebSocket error", e); 73 | 74 | this.client?.terminate && this.client?.terminate(); // `terminate` does not seem to be defined in some browsers 75 | }; 76 | this.client.onopen = async () => await this.handleConnect(); 77 | this.client.onclose = async () => await this.handleDisconnect(); 78 | 79 | this.logger.verbose("Server connected", { address: this.address }); 80 | } 81 | 82 | async close() { 83 | this.logger.debug("Closing signaling client"); 84 | 85 | this.client?.terminate && this.client?.terminate(); // `terminate` does not seem to be defined in some browsers 86 | } 87 | 88 | async bind(alias: string) { 89 | this.logger.debug("Binding", { id: this.id, alias }); 90 | 91 | return new Promise(async (res, rej) => { 92 | (async () => { 93 | const set = await this.asyncResolver.once( 94 | this.getAliasKey(this.id, alias) 95 | ); 96 | 97 | set 98 | ? res() 99 | : rej( 100 | new BindRejectedError(this.getAliasKey(this.id, alias)).message 101 | ); 102 | })(); 103 | 104 | await this.send(this.client, new Bind({ id: this.id, alias })); 105 | }); 106 | } 107 | 108 | async accept(alias: string): Promise { 109 | this.logger.debug("Accepting", { id: this.id, alias }); 110 | 111 | return new Promise(async (res) => { 112 | (async () => { 113 | const clientAlias = await this.asyncResolver.once( 114 | this.getAcceptKey(alias) 115 | ); 116 | 117 | res(clientAlias as string); 118 | })(); 119 | 120 | await this.send(this.client, new Accepting({ id: this.id, alias })); 121 | }); 122 | } 123 | 124 | async shutdown(alias: string) { 125 | this.logger.debug("Shutting down", { id: this.id, alias }); 126 | 127 | return new Promise(async (res, rej) => { 128 | (async () => { 129 | const set = await this.asyncResolver.once( 130 | this.getAliasKey(this.id, alias) 131 | ); 132 | 133 | set 134 | ? rej( 135 | new ShutdownRejectedError(this.getAliasKey(this.id, alias)) 136 | .message 137 | ) 138 | : res(); 139 | })(); 140 | 141 | await this.send(this.client, new Shutdown({ id: this.id, alias })); 142 | }); 143 | } 144 | 145 | async connect(remoteAlias: string): Promise { 146 | this.logger.debug("Connecting", { id: this.id, remoteAlias }); 147 | 148 | const clientConnectionId = v4(); 149 | 150 | const clientAlias = await new Promise(async (res, rej) => { 151 | let i = 0; 152 | let alias = ""; 153 | 154 | this.asyncResolver.on( 155 | this.getConnectionKey(clientConnectionId), 156 | (payload) => { 157 | const { set, alias: newAlias, isConnectionAlias } = JSON.parse( 158 | payload as string 159 | ); 160 | 161 | if (set) { 162 | i = i + 1; 163 | 164 | if (isConnectionAlias) { 165 | alias = newAlias; 166 | } 167 | 168 | if (i >= 2) { 169 | res(alias); 170 | } 171 | } else { 172 | rej( 173 | new ConnectionRejectedError( 174 | this.getConnectionKey(clientConnectionId) 175 | ).message 176 | ); 177 | } 178 | } 179 | ); 180 | 181 | await this.send( 182 | this.client, 183 | new Connect({ id: this.id, clientConnectionId, remoteAlias }) 184 | ); 185 | }); 186 | 187 | return clientAlias as string; 188 | } 189 | 190 | private async handleConnect() { 191 | this.logger.silly("Server connected", { address: this.address }); 192 | 193 | await this.send(this.client, new Knock({ subnet: this.subnet })); 194 | 195 | await this.onConnect(); 196 | } 197 | 198 | private async handleDisconnect() { 199 | this.logger.verbose("Server disconnected", { 200 | address: this.address, 201 | reconnectingIn: this.reconnectDuration, 202 | }); 203 | 204 | await this.onDisconnect(); 205 | 206 | await new Promise((res) => setTimeout(res, this.reconnectDuration)); 207 | 208 | await this.open(); 209 | } 210 | 211 | private async sendCandidate(candidate: Candidate) { 212 | this.logger.silly("Sent candidate", candidate); 213 | 214 | await this.send(this.client, candidate); 215 | } 216 | 217 | private async handleOperation( 218 | operation: ISignalingOperation 219 | ) { 220 | this.logger.silly("Handling operation", operation); 221 | 222 | switch (operation.opcode) { 223 | case ESIGNALING_OPCODES.GOODBYE: { 224 | const data = operation.data as IGoodbyeData; 225 | 226 | this.logger.debug("Received goodbye", data); 227 | 228 | await this.onGoodbye(data.id); 229 | 230 | break; 231 | } 232 | 233 | case ESIGNALING_OPCODES.ACKNOWLEDGED: { 234 | const data = operation.data as IAcknowledgementData; 235 | 236 | this.id = data.id; 237 | 238 | this.logger.debug("Received acknowledgement", { id: this.id }); 239 | 240 | await this.onAcknowledgement(this.id, data.rejected); 241 | 242 | break; 243 | } 244 | 245 | case ESIGNALING_OPCODES.GREETING: { 246 | const data = operation.data as IGreetingData; 247 | 248 | const offer = await this.getOffer( 249 | data.answererId, 250 | async (candidate: string) => { 251 | await this.sendCandidate( 252 | new Candidate({ 253 | offererId: data.offererId, 254 | answererId: data.answererId, 255 | candidate, 256 | }) 257 | ); 258 | 259 | this.logger.debug("Sent candidate", data); 260 | } 261 | ); 262 | 263 | await this.send( 264 | this.client, 265 | new Offer({ 266 | offererId: this.id, 267 | answererId: data.answererId, 268 | offer, 269 | }) 270 | ); 271 | 272 | this.logger.debug("Sent offer", { 273 | offererId: this.id, 274 | answererId: data.answererId, 275 | offer, 276 | }); 277 | 278 | break; 279 | } 280 | 281 | case ESIGNALING_OPCODES.OFFER: { 282 | const data = operation.data as IOfferData; 283 | 284 | this.logger.debug("Received offer", data); 285 | 286 | const answer = await this.getAnswer( 287 | data.offererId, 288 | data.offer, 289 | async (candidate: string) => { 290 | await this.sendCandidate( 291 | new Candidate({ 292 | offererId: data.answererId, 293 | answererId: data.offererId, 294 | candidate, 295 | }) 296 | ); 297 | 298 | this.logger.debug("Sent candidate", data); 299 | } 300 | ); 301 | 302 | await this.send( 303 | this.client, 304 | new Answer({ 305 | offererId: data.offererId, 306 | answererId: this.id, 307 | answer, 308 | }) 309 | ); 310 | 311 | this.logger.debug("Sent answer", { 312 | offererId: data.offererId, 313 | answererId: this.id, 314 | answer, 315 | }); 316 | 317 | break; 318 | } 319 | 320 | case ESIGNALING_OPCODES.ANSWER: { 321 | const data = operation.data as IAnswerData; 322 | 323 | this.logger.debug("Received answer", data); 324 | 325 | await this.onAnswer(data.offererId, data.answererId, data.answer); 326 | 327 | break; 328 | } 329 | 330 | case ESIGNALING_OPCODES.CANDIDATE: { 331 | const data = operation.data as ICandidateData; 332 | 333 | this.logger.debug("Received candidate", data); 334 | 335 | await this.onCandidate(data.offererId, data.answererId, data.candidate); 336 | 337 | break; 338 | } 339 | 340 | case ESIGNALING_OPCODES.ALIAS: { 341 | const data = operation.data as IAliasData; 342 | 343 | this.logger.debug("Received alias", data); 344 | 345 | if (data.clientConnectionId) { 346 | await this.notifyConnect( 347 | data.clientConnectionId, 348 | data.set, 349 | data.alias, 350 | data.isConnectionAlias ? true : false 351 | ); 352 | await this.onAlias(data.id, data.alias, data.set); 353 | } else { 354 | await this.notifyBindAndShutdown(data.id, data.alias, data.set); 355 | await this.onAlias(data.id, data.alias, data.set); 356 | } 357 | 358 | break; 359 | } 360 | 361 | case ESIGNALING_OPCODES.ACCEPT: { 362 | const data = operation.data as IAcceptData; 363 | 364 | this.logger.debug("Received accept", data); 365 | 366 | await this.notifyAccept(data.boundAlias, data.clientAlias); 367 | 368 | break; 369 | } 370 | 371 | default: { 372 | throw new UnimplementedOperationError(operation.opcode); 373 | } 374 | } 375 | } 376 | 377 | private async notifyConnect( 378 | clientConnectionId: string, 379 | set: boolean, 380 | alias: string, 381 | isConnectionAlias: boolean 382 | ) { 383 | this.logger.silly("Notifying of connect", { 384 | clientConnectionId, 385 | set, 386 | alias, 387 | isConnectionAlias, 388 | }); 389 | 390 | await this.asyncResolver.emit( 391 | this.getConnectionKey(clientConnectionId), 392 | JSON.stringify({ set, alias, isConnectionAlias }) 393 | ); 394 | } 395 | 396 | private async notifyBindAndShutdown(id: string, alias: string, set: boolean) { 397 | this.logger.silly("Notifying bind and shutdown", { 398 | id, 399 | alias, 400 | set, 401 | }); 402 | 403 | await this.asyncResolver.emit(this.getAliasKey(id, alias), set); 404 | } 405 | 406 | private async notifyAccept(boundAlias: string, clientAlias: string) { 407 | this.logger.silly("Notifying accept", { 408 | boundAlias, 409 | clientAlias, 410 | }); 411 | 412 | await this.asyncResolver.emit(this.getAcceptKey(boundAlias), clientAlias); 413 | } 414 | 415 | private getAliasKey(id: string, alias: string) { 416 | this.logger.silly("Getting alias key", { 417 | id, 418 | alias, 419 | }); 420 | 421 | return `alias id=${id} alias=${alias}`; 422 | } 423 | 424 | private getConnectionKey(clientConnectionId: string) { 425 | this.logger.silly("Getting connection key", { 426 | clientConnectionId, 427 | }); 428 | 429 | return `connection id=${clientConnectionId}`; 430 | } 431 | 432 | private getAcceptKey(boundAlias: string) { 433 | this.logger.silly("Getting accept key", { 434 | boundAlias, 435 | }); 436 | 437 | return `accept alias=${boundAlias}`; 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /pkg/web/signaling/services/signaling-server.test.ts: -------------------------------------------------------------------------------- 1 | import getPort from "get-port"; 2 | import WebSocket from "isomorphic-ws"; 3 | import { IAcknowledgementData } from "../operations/acknowledgement"; 4 | import { Knock } from "../operations/knock"; 5 | import { 6 | ESIGNALING_OPCODES, 7 | ISignalingOperation, 8 | } from "../operations/operation"; 9 | import { SignalingServer } from "./signaling-server"; 10 | 11 | describe("SignalingServer", () => { 12 | const host = "localhost"; 13 | let port: number; 14 | let signalingServer: SignalingServer; 15 | 16 | beforeEach(async () => { 17 | port = await getPort(); 18 | signalingServer = new SignalingServer(host, port); 19 | }); 20 | 21 | describe("lifecycle", () => { 22 | describe("open", () => { 23 | it("should open", async () => { 24 | await signalingServer.open(); 25 | }); 26 | 27 | afterEach(async () => { 28 | await signalingServer.close(); 29 | }); 30 | }); 31 | 32 | describe("close", () => { 33 | beforeEach(async () => { 34 | await signalingServer.open(); 35 | }); 36 | 37 | it("should close", async () => { 38 | await signalingServer.close(); 39 | }); 40 | }); 41 | }); 42 | 43 | describe("operations", () => { 44 | let client: WebSocket; 45 | 46 | beforeEach(async (done) => { 47 | await signalingServer.open(); 48 | 49 | client = new WebSocket(`ws://${host}:${port}`); 50 | 51 | client.once("open", done); 52 | }); 53 | 54 | afterEach(async () => { 55 | await signalingServer.close(); 56 | }); 57 | 58 | describe("KNOCK", () => { 59 | it("should send back non-rejected, valid ACKNOWLEDGEMENT as first message if there is no subnet overflow", async (done) => { 60 | client.onmessage = (msg) => { 61 | const operation = JSON.parse( 62 | msg.data as string 63 | ) as ISignalingOperation; 64 | 65 | expect(operation.opcode).toBe(ESIGNALING_OPCODES.ACKNOWLEDGED); 66 | expect(operation.data.rejected).toBe(false); 67 | 68 | const lastOctet = parseInt(operation.data.id.split(".")[3]); 69 | expect(lastOctet).toBeGreaterThanOrEqual(0); 70 | expect(lastOctet).toBeLessThanOrEqual(255); 71 | 72 | done(); 73 | }; 74 | 75 | client.send( 76 | JSON.stringify( 77 | new Knock({ 78 | subnet: "10.0.0", 79 | }) 80 | ) 81 | ); 82 | }); 83 | 84 | it("should send back non-reject, valid ACKNOWLEDGEMENTs if there is no subnet overflow", async (done) => { 85 | let responses = 0; 86 | 87 | client.onmessage = (msg) => { 88 | const operation = JSON.parse( 89 | msg.data as string 90 | ) as ISignalingOperation; 91 | 92 | if (operation.opcode === ESIGNALING_OPCODES.ACKNOWLEDGED) { 93 | expect(operation.opcode).toBe(ESIGNALING_OPCODES.ACKNOWLEDGED); 94 | expect(operation.data.rejected).toBe(false); 95 | 96 | const lastOctet = parseInt(operation.data.id.split(".")[3]); 97 | expect(lastOctet).toBeGreaterThanOrEqual(0); 98 | expect(lastOctet).toBeLessThanOrEqual(255); 99 | 100 | responses++; 101 | if (responses == 255) { 102 | done(); 103 | } 104 | } 105 | }; 106 | 107 | // Request the maximum amount of IPs (255) 108 | for (let requests = 0; requests <= 255; requests++) { 109 | client.send( 110 | JSON.stringify( 111 | new Knock({ 112 | subnet: "10.0.0", 113 | }) 114 | ) 115 | ); 116 | } 117 | }); 118 | 119 | it("should send back rejected, valid ACKNOWLEDGEMENT if there is a subnet overflow", async (done) => { 120 | let responses = 0; 121 | 122 | client.onmessage = (msg) => { 123 | const operation = JSON.parse( 124 | msg.data as string 125 | ) as ISignalingOperation; 126 | 127 | if (operation.opcode === ESIGNALING_OPCODES.ACKNOWLEDGED) { 128 | if (responses > 255) { 129 | expect(operation.opcode).toBe(ESIGNALING_OPCODES.ACKNOWLEDGED); 130 | expect(operation.data.rejected).toBe(true); 131 | 132 | done(); 133 | } 134 | 135 | responses++; 136 | } 137 | }; 138 | 139 | // Provoke a overflow (255 is the max value) 140 | for (let requests = 0; requests <= 256; requests++) { 141 | client.send( 142 | JSON.stringify( 143 | new Knock({ 144 | subnet: "10.0.0", 145 | }) 146 | ) 147 | ); 148 | } 149 | }); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /pkg/web/signaling/services/signaling-server.ts: -------------------------------------------------------------------------------- 1 | import { Mutex } from "async-mutex"; 2 | import WebSocket, { Server } from "isomorphic-ws"; 3 | import { ClientDoesNotExistError } from "../errors/client-does-not-exist"; 4 | import { PortAlreadyAllocatedError } from "../errors/port-already-allocated-error"; 5 | import { SubnetDoesNotExistError } from "../errors/subnet-does-not-exist"; 6 | import { SuffixDoesNotExistError } from "../errors/suffix-does-not-exist"; 7 | import { UnimplementedOperationError } from "../errors/unimplemented-operation"; 8 | import { MAlias } from "../models/alias"; 9 | import { MMember } from "../models/member"; 10 | import { Accept } from "../operations/accept"; 11 | import { IAcceptingData } from "../operations/accepting"; 12 | import { Acknowledgement } from "../operations/acknowledgement"; 13 | import { Alias } from "../operations/alias"; 14 | import { Answer, IAnswerData } from "../operations/answer"; 15 | import { IBindData } from "../operations/bind"; 16 | import { Candidate, ICandidateData } from "../operations/candidate"; 17 | import { IConnectData } from "../operations/connect"; 18 | import { Goodbye } from "../operations/goodbye"; 19 | import { Greeting } from "../operations/greeting"; 20 | import { IKnockData } from "../operations/knock"; 21 | import { IOfferData, Offer } from "../operations/offer"; 22 | import { 23 | ESIGNALING_OPCODES, 24 | ISignalingOperation, 25 | TSignalingData, 26 | } from "../operations/operation"; 27 | import { IShutdownData } from "../operations/shutdown"; 28 | import { SignalingService } from "./signaling-service"; 29 | 30 | export class SignalingServer extends SignalingService { 31 | private clients = new Map(); 32 | private aliases = new Map(); 33 | protected server?: Server; 34 | 35 | constructor(private host: string, private port: number) { 36 | super(); 37 | } 38 | 39 | async open() { 40 | this.logger.debug("Opening signaling server"); 41 | 42 | const server = new Server({ 43 | host: this.host, 44 | port: this.port, 45 | }); 46 | 47 | await new Promise( 48 | (res) => server.once("listening", () => res()) // We create it above, so this can't be undefined 49 | ); 50 | 51 | server.on("connection", async (client) => { 52 | (client as any).isAlive = true; 53 | 54 | client.on("pong", function () { 55 | (this as any).isAlive = true; 56 | }); 57 | 58 | client.on( 59 | "message", 60 | async (operation) => 61 | await this.handleOperation(await this.receive(operation), client) 62 | ); 63 | }); 64 | 65 | const interval = setInterval( 66 | () => 67 | server.clients.forEach((client) => { 68 | if ((client as any).isAlive === false) { 69 | this.logger.verbose("Client disconnected"); 70 | 71 | return client.terminate(); 72 | } 73 | 74 | (client as any).isAlive = false; 75 | 76 | this.logger.debug("Pinging client"); 77 | 78 | client.ping(() => {}); 79 | }), 80 | 30000 81 | ); 82 | 83 | server.on("close", function close() { 84 | clearInterval(interval); 85 | }); 86 | 87 | this.logger.verbose("Listening", { 88 | host: this.host, 89 | port: this.port, 90 | }); 91 | 92 | this.server = server; 93 | } 94 | 95 | async close() { 96 | this.logger.debug("Shutting down signaling server"); 97 | 98 | await new Promise((res, rej) => 99 | this.server ? this.server.close((e) => (e ? rej(e) : res())) : res() 100 | ); 101 | 102 | this.logger.debug("Closed signaling server"); 103 | } 104 | 105 | private async registerGoodbye(id: string) { 106 | this.logger.silly("Registering goodbye", { id }); 107 | 108 | if (this.clients.has(id)) { 109 | const client = this.clients.get(id)!; // `.has` checks this 110 | 111 | client.on("close", async () => { 112 | this.clients.delete(id); 113 | await this.removeIPAddress(id); 114 | 115 | this.aliases.forEach(async ({ id: clientId }, alias) => { 116 | if (clientId === id) { 117 | this.aliases.delete(alias); 118 | await this.removeIPAddress(alias); 119 | await this.removeTCPAddress(alias); 120 | 121 | this.clients.forEach(async (client) => { 122 | await this.send(client, new Alias({ id, alias, set: false })); 123 | 124 | this.logger.debug("Sent alias", { id, alias }); 125 | }); 126 | } 127 | }); 128 | 129 | this.clients.forEach( 130 | async (client) => await this.send(client, new Goodbye({ id })) 131 | ); 132 | 133 | this.logger.verbose("Client disconnected", { id }); 134 | }); 135 | } else { 136 | throw new ClientDoesNotExistError(); 137 | } 138 | } 139 | 140 | private async handleOperation( 141 | operation: ISignalingOperation, 142 | client: WebSocket 143 | ) { 144 | this.logger.silly("Handling operation", { operation, client }); 145 | 146 | switch (operation.opcode) { 147 | case ESIGNALING_OPCODES.KNOCK: { 148 | const data = operation.data as IKnockData; 149 | 150 | this.logger.debug("Received knock", data); 151 | 152 | await this.handleKnock(data, client); 153 | 154 | break; 155 | } 156 | 157 | case ESIGNALING_OPCODES.OFFER: { 158 | const data = operation.data as IOfferData; 159 | 160 | this.logger.debug("Received offer", data); 161 | 162 | await this.handleOffer(data); 163 | 164 | break; 165 | } 166 | 167 | case ESIGNALING_OPCODES.ANSWER: { 168 | const data = operation.data as IAnswerData; 169 | 170 | this.logger.debug("Received answer", data); 171 | 172 | await this.handleAnswer(data); 173 | 174 | break; 175 | } 176 | 177 | case ESIGNALING_OPCODES.CANDIDATE: { 178 | const data = operation.data as ICandidateData; 179 | 180 | this.logger.debug("Received candidate", data); 181 | 182 | await this.handleCandidate(data); 183 | 184 | break; 185 | } 186 | 187 | case ESIGNALING_OPCODES.BIND: { 188 | const data = operation.data as IBindData; 189 | 190 | this.logger.debug("Received bind", data); 191 | 192 | await this.handleBind(data); 193 | 194 | break; 195 | } 196 | 197 | case ESIGNALING_OPCODES.ACCEPTING: { 198 | const data = operation.data as IAcceptingData; 199 | 200 | this.logger.debug("Received accepting", data); 201 | 202 | await this.handleAccepting(data); 203 | 204 | break; 205 | } 206 | 207 | case ESIGNALING_OPCODES.SHUTDOWN: { 208 | const data = operation.data as IShutdownData; 209 | 210 | this.logger.debug("Received shutdown", data); 211 | 212 | await this.handleShutdown(data); 213 | 214 | break; 215 | } 216 | 217 | case ESIGNALING_OPCODES.CONNECT: { 218 | const data = operation.data as IConnectData; 219 | 220 | this.logger.debug("Received connect", data); 221 | 222 | await this.handleConnect(data); 223 | 224 | break; 225 | } 226 | 227 | default: { 228 | throw new UnimplementedOperationError(operation.opcode); 229 | } 230 | } 231 | } 232 | 233 | private async handleKnock(data: IKnockData, client: WebSocket) { 234 | this.logger.silly("Handling knock", { data, client }); 235 | 236 | const id = await this.createIPAddress(data.subnet); 237 | 238 | if (id !== "-1") { 239 | await this.send(client, new Acknowledgement({ id, rejected: false })); 240 | } else { 241 | await this.send(client, new Acknowledgement({ id, rejected: true })); 242 | 243 | this.logger.debug("Knock rejected", { 244 | id, 245 | reason: "subnet overflow", 246 | }); 247 | 248 | return; 249 | } 250 | 251 | this.clients.forEach(async (existingClient, existingId) => { 252 | if (existingId !== id) { 253 | await this.send( 254 | existingClient, 255 | new Greeting({ 256 | offererId: existingId, 257 | answererId: id, 258 | }) 259 | ); 260 | 261 | this.logger.debug("Sent greeting", { 262 | offererId: existingId, 263 | answererId: id, 264 | }); 265 | } 266 | }); 267 | 268 | this.clients.set(id, client); 269 | await this.registerGoodbye(id); 270 | 271 | this.logger.verbose("Client connected", { id }); 272 | } 273 | 274 | private async handleOffer(data: IOfferData) { 275 | this.logger.silly("Handling offer", { data }); 276 | 277 | const client = this.clients.get(data.answererId); 278 | 279 | await this.send( 280 | client, 281 | new Offer({ 282 | offererId: data.offererId, 283 | answererId: data.answererId, 284 | offer: data.offer, 285 | }) 286 | ); 287 | 288 | this.logger.debug("Sent offer", { 289 | offererId: data.offererId, 290 | answererId: data.answererId, 291 | offer: data.offer, 292 | }); 293 | } 294 | 295 | private async handleAnswer(data: IAnswerData) { 296 | this.logger.silly("Handling answer", { data }); 297 | 298 | const client = this.clients.get(data.offererId); 299 | 300 | await this.send(client, new Answer(data)); 301 | 302 | this.logger.debug("Sent answer", data); 303 | } 304 | 305 | private async handleCandidate(data: ICandidateData) { 306 | this.logger.silly("Handling candidate", { data }); 307 | 308 | const client = this.clients.get(data.answererId); 309 | 310 | await this.send(client, new Candidate(data)); 311 | 312 | this.logger.debug("Sent candidate", data); 313 | } 314 | 315 | private async handleBind(data: IBindData) { 316 | this.logger.silly("Handling bind", { data }); 317 | 318 | if (this.aliases.has(data.alias)) { 319 | this.logger.debug("Rejecting bind, alias already taken", data); 320 | 321 | const client = this.clients.get(data.id); 322 | 323 | await this.send( 324 | client, 325 | new Alias({ id: data.id, alias: data.alias, set: false }) 326 | ); 327 | } else { 328 | this.logger.debug("Accepting bind", data); 329 | 330 | await this.claimTCPAddress(data.alias); 331 | 332 | this.aliases.set(data.alias, new MAlias(data.id, false)); 333 | 334 | this.clients.forEach(async (client, id) => { 335 | await this.send( 336 | client, 337 | new Alias({ id: data.id, alias: data.alias, set: true }) 338 | ); 339 | 340 | this.logger.debug("Sent alias", { id, data }); 341 | }); 342 | } 343 | } 344 | 345 | private async handleAccepting(data: IAcceptingData) { 346 | this.logger.silly("Handling accepting", { data }); 347 | 348 | if ( 349 | !this.aliases.has(data.alias) || 350 | this.aliases.get(data.alias)!.id !== data.id // `.has` checks this 351 | ) { 352 | this.logger.debug("Rejecting accepting, alias does not exist", data); 353 | } else { 354 | this.logger.debug("Accepting accepting", data); 355 | 356 | this.aliases.set(data.alias, new MAlias(data.id, true)); 357 | } 358 | } 359 | 360 | private async handleShutdown(data: IShutdownData) { 361 | this.logger.silly("Handling shutdown", { data }); 362 | 363 | if ( 364 | this.aliases.has(data.alias) && 365 | this.aliases.get(data.alias)!.id === data.id // `.has` checks this 366 | ) { 367 | this.aliases.delete(data.alias); 368 | await this.removeTCPAddress(data.alias); 369 | await this.removeIPAddress(data.alias); 370 | 371 | this.logger.debug("Accepting shutdown", data); 372 | 373 | this.clients.forEach(async (client, id) => { 374 | await this.send( 375 | client, 376 | new Alias({ id: data.id, alias: data.alias, set: false }) 377 | ); 378 | 379 | this.logger.debug("Sent alias", { id, data }); 380 | }); 381 | } else { 382 | this.logger.debug( 383 | "Rejecting shutdown, alias not taken or incorrect client ID", 384 | data 385 | ); 386 | 387 | const client = this.clients.get(data.id); 388 | 389 | await this.send( 390 | client, 391 | new Alias({ id: data.id, alias: data.alias, set: true }) 392 | ); 393 | } 394 | } 395 | 396 | private async handleConnect(data: IConnectData) { 397 | this.logger.silly("Handling connect", { data }); 398 | 399 | const clientAlias = await this.createTCPAddress(data.id); 400 | const client = this.clients.get(data.id); 401 | 402 | if ( 403 | !this.aliases.has(data.remoteAlias) || 404 | !this.aliases.get(data.remoteAlias)!.accepting // `.has` checks this 405 | ) { 406 | this.logger.debug("Rejecting connect, remote alias does not exist", { 407 | data, 408 | }); 409 | 410 | await this.removeTCPAddress(clientAlias); 411 | 412 | await this.send( 413 | client, 414 | new Alias({ 415 | id: data.id, 416 | alias: clientAlias, 417 | set: false, 418 | clientConnectionId: data.clientConnectionId, 419 | }) 420 | ); 421 | } else { 422 | this.logger.debug("Accepting connect", { 423 | data, 424 | }); 425 | 426 | this.aliases.set(clientAlias, new MAlias(data.id, false)); 427 | 428 | const clientAliasMessage = new Alias({ 429 | id: data.id, 430 | alias: clientAlias, 431 | set: true, 432 | clientConnectionId: data.clientConnectionId, 433 | isConnectionAlias: true, 434 | }); 435 | 436 | await this.send(client, clientAliasMessage); 437 | 438 | this.logger.debug("Sent alias for connection to client", { 439 | data, 440 | alias: clientAliasMessage, 441 | }); 442 | 443 | const serverId = this.aliases.get(data.remoteAlias)!; // `.has` checks this 444 | const server = this.clients.get(serverId.id); 445 | 446 | const serverAliasMessage = new Alias({ 447 | id: data.id, 448 | alias: clientAlias, 449 | set: true, 450 | }); 451 | 452 | await this.send(server, serverAliasMessage); 453 | 454 | this.logger.debug("Sent alias for connection to server", { 455 | data, 456 | alias: serverAliasMessage, 457 | }); 458 | 459 | const serverAcceptMessage = new Accept({ 460 | boundAlias: data.remoteAlias, 461 | clientAlias: clientAlias, 462 | }); 463 | 464 | await this.send(server, serverAcceptMessage); 465 | 466 | this.logger.debug("Sent accept to server", { 467 | data, 468 | accept: serverAcceptMessage, 469 | }); 470 | 471 | const serverAliasForClientsMessage = new Alias({ 472 | id: serverId.id, 473 | alias: data.remoteAlias, 474 | set: true, 475 | clientConnectionId: data.clientConnectionId, 476 | }); 477 | 478 | await this.send(client, serverAliasForClientsMessage); 479 | 480 | this.logger.debug("Sent alias for server to client", { 481 | data, 482 | alias: serverAliasForClientsMessage, 483 | }); 484 | } 485 | } 486 | 487 | private subnets = new Map>(); 488 | private subnetsMutex = new Mutex(); 489 | 490 | private async createIPAddress(subnet: string) { 491 | this.logger.silly("Creating IP address", { subnet }); 492 | 493 | const release = await this.subnetsMutex.acquire(); 494 | 495 | try { 496 | if (!this.subnets.has(subnet)) { 497 | this.subnets.set(subnet, new Map()); 498 | } 499 | 500 | const existingMembers = Array.from(this.subnets.get(subnet)!.keys()).sort( 501 | (a, b) => a - b 502 | ); // We ensure above 503 | 504 | // Find the next free suffix 505 | const newSuffix: number = await new Promise((res) => { 506 | existingMembers.forEach((suffix, index) => { 507 | suffix !== index && res(index); 508 | }); 509 | 510 | res(existingMembers.length); 511 | }); 512 | 513 | if (newSuffix > 255) { 514 | return "-1"; 515 | } 516 | 517 | const newMember = new MMember([]); 518 | 519 | this.subnets.get(subnet)!.set(newSuffix, newMember); // We ensure above 520 | 521 | return this.toIPAddress(subnet, newSuffix); 522 | } finally { 523 | release(); 524 | } 525 | } 526 | 527 | private async createTCPAddress(ipAddress: string) { 528 | this.logger.silly("Creating TCP address", { ipAddress }); 529 | 530 | const release = await this.subnetsMutex.acquire(); 531 | 532 | try { 533 | const { subnet, suffix } = this.parseIPAddress(ipAddress); 534 | 535 | if (this.subnets.has(subnet)) { 536 | if (this.subnets.get(subnet)!.has(suffix)) { 537 | const existingPorts = this.subnets 538 | .get(subnet)! 539 | .get(suffix)! 540 | .ports.sort((a, b) => a - b); // We ensure above 541 | 542 | // Find next free port 543 | const newPort: number = await new Promise((res) => { 544 | existingPorts.forEach((port, index) => { 545 | port !== index && res(index); 546 | }); 547 | 548 | res(existingPorts.length); 549 | }); 550 | 551 | this.subnets.get(subnet)!.get(suffix)!.ports.push(newPort); // We ensure above 552 | 553 | return this.toTCPAddress(this.toIPAddress(subnet, suffix), newPort); 554 | } else { 555 | throw new SuffixDoesNotExistError(); 556 | } 557 | } else { 558 | throw new SubnetDoesNotExistError(); 559 | } 560 | } finally { 561 | release(); 562 | } 563 | } 564 | 565 | private async claimTCPAddress(tcpAddress: string) { 566 | this.logger.silly("Claiming TCP address", { tcpAddress }); 567 | 568 | const release = await this.subnetsMutex.acquire(); 569 | 570 | try { 571 | const { ipAddress, port } = this.parseTCPAddress(tcpAddress); 572 | const { subnet, suffix } = this.parseIPAddress(ipAddress); 573 | 574 | if (this.subnets.has(subnet)) { 575 | if (!this.subnets.get(subnet)!.has(suffix)) { 576 | this.subnets.get(subnet)!.set(suffix, new MMember([])); // We ensure above 577 | } 578 | 579 | if ( 580 | this.subnets 581 | .get(subnet)! 582 | .get(suffix)! 583 | .ports.find((p) => p === port) === undefined 584 | ) { 585 | this.subnets.get(subnet)!.get(suffix)!.ports.push(port); // We ensure above 586 | } else { 587 | throw new PortAlreadyAllocatedError(); 588 | } 589 | } else { 590 | throw new SubnetDoesNotExistError(); 591 | } 592 | } finally { 593 | release(); 594 | } 595 | } 596 | 597 | private async removeIPAddress(ipAddress: string) { 598 | this.logger.silly("Removing IP address", { ipAddress }); 599 | 600 | const release = await this.subnetsMutex.acquire(); 601 | 602 | try { 603 | const { subnet, suffix } = this.parseIPAddress(ipAddress); 604 | 605 | if (this.subnets.has(subnet)) { 606 | if (this.subnets.get(subnet)!.has(suffix)) { 607 | this.subnets.get(subnet)!.delete(suffix); // We ensure above 608 | } 609 | } 610 | } finally { 611 | release(); 612 | } 613 | } 614 | 615 | private async removeTCPAddress(tcpAddress: string) { 616 | this.logger.silly("Removing TCP address", { tcpAddress }); 617 | 618 | const release = await this.subnetsMutex.acquire(); 619 | 620 | try { 621 | const { ipAddress, port } = this.parseTCPAddress(tcpAddress); 622 | const { subnet, suffix } = this.parseIPAddress(ipAddress); 623 | 624 | if (this.subnets.has(subnet)) { 625 | if (this.subnets.get(subnet)!.has(suffix)) { 626 | this.subnets.get(subnet)!.get(suffix)!.ports = this.subnets 627 | .get(subnet)! 628 | .get(suffix)! 629 | .ports.filter((p) => p !== port); // We ensure above 630 | } 631 | } 632 | } finally { 633 | release(); 634 | } 635 | } 636 | 637 | private toIPAddress(subnet: string, suffix: number) { 638 | this.logger.silly("Converting to IP address", { subnet, suffix }); 639 | 640 | return `${subnet}.${suffix}`; 641 | } 642 | 643 | private toTCPAddress(ipAddress: string, port: number) { 644 | this.logger.silly("Converting to TCP address", { ipAddress, port }); 645 | 646 | return `${ipAddress}:${port}`; 647 | } 648 | 649 | private parseIPAddress(ipAddress: string) { 650 | this.logger.silly("Parsing IP address", { ipAddress }); 651 | 652 | const parts = ipAddress.split("."); 653 | 654 | return { 655 | subnet: parts.slice(0, 3).join("."), 656 | suffix: parseInt(parts[3]), 657 | }; 658 | } 659 | 660 | private parseTCPAddress(tcpAddress: string) { 661 | this.logger.silly("Parsing TCP address", { tcpAddress }); 662 | 663 | const parts = tcpAddress.split(":"); 664 | 665 | return { 666 | ipAddress: parts[0], 667 | port: parseInt(parts[1]), 668 | }; 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /pkg/web/signaling/services/signaling-service.ts: -------------------------------------------------------------------------------- 1 | import WebSocket, { Data } from "isomorphic-ws"; 2 | import { getLogger } from "../../utils/logger"; 3 | import { ClientClosedError } from "../errors/client-closed"; 4 | import { UnimplementedOperationError } from "../errors/unimplemented-operation"; 5 | import { Accept, IAcceptData } from "../operations/accept"; 6 | import { Accepting, IAcceptingData } from "../operations/accepting"; 7 | import { 8 | Acknowledgement, 9 | IAcknowledgementData, 10 | } from "../operations/acknowledgement"; 11 | import { Alias, IAliasData } from "../operations/alias"; 12 | import { Answer, IAnswerData } from "../operations/answer"; 13 | import { Bind, IBindData } from "../operations/bind"; 14 | import { Candidate, ICandidateData } from "../operations/candidate"; 15 | import { Connect, IConnectData } from "../operations/connect"; 16 | import { Goodbye, IGoodbyeData } from "../operations/goodbye"; 17 | import { Greeting, IGreetingData } from "../operations/greeting"; 18 | import { IKnockData, Knock } from "../operations/knock"; 19 | import { IOfferData, Offer } from "../operations/offer"; 20 | import { 21 | ESIGNALING_OPCODES, 22 | ISignalingOperation, 23 | TSignalingData, 24 | } from "../operations/operation"; 25 | import { IShutdownData, Shutdown } from "../operations/shutdown"; 26 | 27 | export class SignalingService { 28 | protected logger = getLogger(); 29 | 30 | protected async send( 31 | client: WebSocket | undefined, 32 | operation: ISignalingOperation 33 | ) { 34 | this.logger.debug("Sending", { operation }); 35 | 36 | if (client) { 37 | client.send(JSON.stringify(operation)); 38 | } else { 39 | throw new ClientClosedError(); 40 | } 41 | } 42 | 43 | protected async receive( 44 | rawOperation: Data 45 | ): Promise> { 46 | this.logger.debug("Receiving", { rawOperation }); 47 | 48 | const operation = JSON.parse( 49 | rawOperation as string 50 | ) as ISignalingOperation; 51 | 52 | this.logger.debug("Received operation", operation); 53 | 54 | switch (operation.opcode) { 55 | case ESIGNALING_OPCODES.GOODBYE: { 56 | this.logger.silly("Received operation goodbye", operation.data); 57 | 58 | return new Goodbye(operation.data as IGoodbyeData); 59 | } 60 | 61 | case ESIGNALING_OPCODES.KNOCK: { 62 | this.logger.silly("Received operation knock", operation.data); 63 | 64 | return new Knock(operation.data as IKnockData); 65 | } 66 | 67 | case ESIGNALING_OPCODES.ACKNOWLEDGED: { 68 | this.logger.silly("Received operation acknowledged", operation.data); 69 | 70 | return new Acknowledgement(operation.data as IAcknowledgementData); 71 | } 72 | 73 | case ESIGNALING_OPCODES.GREETING: { 74 | this.logger.silly("Received operation greeting", operation.data); 75 | 76 | return new Greeting(operation.data as IGreetingData); 77 | } 78 | 79 | case ESIGNALING_OPCODES.OFFER: { 80 | this.logger.silly("Received operation offer", operation.data); 81 | 82 | return new Offer(operation.data as IOfferData); 83 | } 84 | 85 | case ESIGNALING_OPCODES.ANSWER: { 86 | this.logger.silly("Received operation answer", operation.data); 87 | 88 | return new Answer(operation.data as IAnswerData); 89 | } 90 | 91 | case ESIGNALING_OPCODES.CANDIDATE: { 92 | this.logger.silly("Received operation candidate", operation.data); 93 | 94 | return new Candidate(operation.data as ICandidateData); 95 | } 96 | 97 | case ESIGNALING_OPCODES.BIND: { 98 | this.logger.silly("Received operation bind", operation.data); 99 | 100 | return new Bind(operation.data as IBindData); 101 | } 102 | 103 | case ESIGNALING_OPCODES.ACCEPTING: { 104 | this.logger.silly("Received operation accepting", operation.data); 105 | 106 | return new Accepting(operation.data as IAcceptingData); 107 | } 108 | 109 | case ESIGNALING_OPCODES.ALIAS: { 110 | this.logger.silly("Received operation alias", operation.data); 111 | 112 | return new Alias(operation.data as IAliasData); 113 | } 114 | 115 | case ESIGNALING_OPCODES.SHUTDOWN: { 116 | this.logger.silly("Received operation shutdown", operation.data); 117 | 118 | return new Shutdown(operation.data as IShutdownData); 119 | } 120 | 121 | case ESIGNALING_OPCODES.CONNECT: { 122 | this.logger.silly("Received operation connect", operation.data); 123 | 124 | return new Connect(operation.data as IConnectData); 125 | } 126 | 127 | case ESIGNALING_OPCODES.ACCEPT: { 128 | this.logger.silly("Received operation accept", operation.data); 129 | 130 | return new Accept(operation.data as IAcceptData); 131 | } 132 | 133 | default: { 134 | throw new UnimplementedOperationError(operation.opcode); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/web/sockets/sockets.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import { MemoryDoesNotExistError } from "../signaling/errors/memory-does-not-exist"; 3 | import { SocketDoesNotExistError } from "../signaling/errors/socket-does-not-exist"; 4 | import { getAsBinary } from "../utils/getAsBinary"; 5 | import { htons } from "../utils/htons"; 6 | import { getLogger } from "../utils/logger"; 7 | 8 | const AF_INET = 2; 9 | 10 | // Matches pkg/unisockets/unisockets.h 11 | interface ISocketImports { 12 | unisockets_socket: () => Promise; 13 | unisockets_bind: ( 14 | fd: number, 15 | addressPointer: number, 16 | addressLength: number 17 | ) => Promise; 18 | unisockets_listen: () => Promise; 19 | unisockets_accept: ( 20 | fd: number, 21 | addressPointer: number, 22 | addressLengthPointer: number 23 | ) => Promise; 24 | unisockets_connect: ( 25 | fd: number, 26 | addressPointer: number, 27 | addressLength: number 28 | ) => Promise; 29 | unisockets_send: ( 30 | fd: number, 31 | messagePointer: number, 32 | messagePointerLength: number 33 | ) => Promise; 34 | unisockets_recv: ( 35 | fd: number, 36 | messagePointer: number, 37 | messagePointerLength: number 38 | ) => Promise; 39 | } 40 | 41 | export class Sockets { 42 | private logger = getLogger(); 43 | private binds = new Map(); 44 | private memories = new Map(); 45 | 46 | constructor( 47 | private externalBind: (alias: string) => Promise, 48 | private externalAccept: (alias: string) => Promise, 49 | private externalConnect: (alias: string) => Promise, 50 | private externalSend: (alias: string, msg: Uint8Array) => Promise, 51 | private externalRecv: (alias: string) => Promise 52 | ) {} 53 | 54 | async getImports(): Promise<{ memoryId: string; imports: ISocketImports }> { 55 | this.logger.debug("Getting imports"); 56 | 57 | const memoryId = v4(); 58 | 59 | return { 60 | memoryId, 61 | imports: { 62 | unisockets_socket: async () => { 63 | return await this.socket(); 64 | }, 65 | unisockets_bind: async ( 66 | fd: number, 67 | addressPointer: number, 68 | addressLength: number 69 | ) => { 70 | try { 71 | const memory = await this.accessMemory(memoryId); 72 | 73 | const socketInMemory = memory.slice( 74 | addressPointer, 75 | addressPointer + addressLength 76 | ); 77 | 78 | const addressInMemory = socketInMemory.slice(4, 8); 79 | const portInMemory = socketInMemory.slice(2, 4); 80 | 81 | const address = addressInMemory.join("."); 82 | const port = htons(new Uint16Array(portInMemory.buffer)[0]); 83 | 84 | await this.bind(fd, `${address}:${port}`); 85 | 86 | return 0; 87 | } catch (e) { 88 | this.logger.error("Bind failed", { e }); 89 | 90 | return -1; 91 | } 92 | }, 93 | unisockets_listen: async () => { 94 | return 0; 95 | }, 96 | unisockets_accept: async ( 97 | fd: number, 98 | addressPointer: number, 99 | addressLengthPointer: number 100 | ) => { 101 | try { 102 | const memory = await this.accessMemory(memoryId); 103 | 104 | const { clientFd, clientAlias } = await this.accept(fd); 105 | 106 | const addressLength = new Int32Array( 107 | memory.slice(addressLengthPointer, addressLengthPointer + 4) 108 | )[0]; 109 | 110 | const parts = clientAlias.split(":"); 111 | 112 | const familyInMemory = getAsBinary(AF_INET); 113 | const portInMemory = getAsBinary(parseInt(parts[1])); 114 | const addressInMemory = parts[0] 115 | .split(".") 116 | .map((e) => Uint8Array.from([parseInt(e)])[0]); 117 | 118 | for (let i = 0; i < addressLength; i++) { 119 | const index = addressPointer + i; 120 | 121 | if (i >= 0 && i < 2) { 122 | memory[index] = familyInMemory[i]; 123 | } else if (i >= 2 && i < 4) { 124 | memory[index] = portInMemory[i - 2]; 125 | } else if (i >= 4 && i < 8) { 126 | memory[index] = addressInMemory[i - 4]; 127 | } 128 | } 129 | 130 | return clientFd; 131 | } catch (e) { 132 | this.logger.error("Accept failed", { e }); 133 | 134 | return -1; 135 | } 136 | }, 137 | unisockets_connect: async ( 138 | fd: number, 139 | addressPointer: number, 140 | addressLength: number 141 | ) => { 142 | try { 143 | const memory = await this.accessMemory(memoryId); 144 | 145 | const socketInMemory = memory.slice( 146 | addressPointer, 147 | addressPointer + addressLength 148 | ); 149 | 150 | const addressInMemory = socketInMemory.slice(4, 8); 151 | const portInMemory = socketInMemory.slice(2, 4); 152 | 153 | const address = addressInMemory.join("."); 154 | const port = htons(new Uint16Array(portInMemory.buffer)[0]); 155 | 156 | await this.connect(fd, `${address}:${port}`); 157 | 158 | return 0; 159 | } catch (e) { 160 | this.logger.error("Connect failed", { e }); 161 | 162 | return -1; 163 | } 164 | }, 165 | unisockets_send: async ( 166 | fd: number, 167 | messagePointer: number, 168 | messagePointerLength: number 169 | ) => { 170 | try { 171 | const memory = await this.accessMemory(memoryId); 172 | 173 | const msg = memory.slice( 174 | messagePointer, 175 | messagePointer + messagePointerLength 176 | ); 177 | 178 | await this.send(fd, msg); 179 | 180 | return msg.length; 181 | } catch (e) { 182 | this.logger.error("Send failed", { e }); 183 | 184 | return -1; 185 | } 186 | }, 187 | unisockets_recv: async ( 188 | fd: number, 189 | messagePointer: number, 190 | messagePointerLength: number 191 | ) => { 192 | try { 193 | const memory = await this.accessMemory(memoryId); 194 | 195 | const msg = await this.recv(fd); 196 | 197 | msg.forEach((messagePart, index) => { 198 | // Don't write over the boundary 199 | if (index <= messagePointerLength) { 200 | memory[messagePointer + index] = messagePart; 201 | } 202 | }); 203 | 204 | return msg.length; 205 | } catch (e) { 206 | this.logger.error("Recv failed", { e }); 207 | 208 | return -1; 209 | } 210 | }, 211 | }, 212 | }; 213 | } 214 | 215 | private async socket() { 216 | this.logger.silly("Handling `socket`"); 217 | 218 | const fd = this.binds.size + 1; 219 | 220 | this.binds.set(fd, ""); 221 | 222 | return fd; 223 | } 224 | 225 | private async bind(fd: number, alias: string) { 226 | this.logger.silly("Handling `bind`", { fd, alias }); 227 | 228 | await this.ensureBound(fd); 229 | 230 | await this.externalBind(alias); 231 | 232 | this.binds.set(fd, alias); 233 | } 234 | 235 | private async accept(serverFd: number) { 236 | this.logger.silly("Handling `accept`", { serverFd }); 237 | 238 | await this.ensureBound(serverFd); 239 | 240 | const clientFd = await this.socket(); 241 | const clientAlias = await this.externalAccept(this.binds.get(serverFd)!); // ensureBound 242 | 243 | this.binds.set(clientFd, clientAlias); 244 | 245 | return { 246 | clientFd, 247 | clientAlias, 248 | }; 249 | } 250 | 251 | private async connect(serverFd: number, alias: string) { 252 | this.logger.silly("Handling `connect`", { serverFd, alias }); 253 | 254 | await this.ensureBound(serverFd); 255 | 256 | this.binds.set(serverFd, alias); 257 | 258 | await this.externalConnect(alias); 259 | } 260 | 261 | private async send(fd: number, msg: Uint8Array) { 262 | this.logger.silly("Handling `send`", { fd, msg }); 263 | 264 | await this.ensureBound(fd); 265 | 266 | await this.externalSend(this.binds.get(fd)!, msg); // ensureBound 267 | } 268 | 269 | private async recv(fd: number) { 270 | this.logger.silly("Handling `recv`", { fd }); 271 | 272 | await this.ensureBound(fd); 273 | 274 | return this.externalRecv(this.binds.get(fd)!); // ensureBound 275 | } 276 | 277 | private async ensureBound(fd: number) { 278 | this.logger.silly("Ensuring bound", { fd }); 279 | 280 | if (!this.binds.has(fd)) { 281 | throw new SocketDoesNotExistError(); 282 | } 283 | } 284 | 285 | async setMemory(memoryId: string, memory: Uint8Array) { 286 | this.logger.debug("Setting memory", { memoryId }); 287 | 288 | this.memories.set(memoryId, memory); 289 | } 290 | 291 | private async accessMemory(memoryId: string) { 292 | this.logger.silly("Accessing memory", { memoryId }); 293 | 294 | if (this.memories.has(memoryId)) { 295 | return new Uint8Array(this.memories.get(memoryId)!.buffer); // Checked by .has & we never push undefined 296 | } else { 297 | throw new MemoryDoesNotExistError(); 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /pkg/web/transport/transporter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtendedRTCConfiguration, 3 | RTCIceCandidate, 4 | RTCPeerConnection, 5 | RTCSessionDescription, 6 | } from "wrtc"; 7 | import { ChannelDoesNotExistError } from "../signaling/errors/channel-does-not-exist"; 8 | import { ConnectionDoesNotExistError } from "../signaling/errors/connection-does-not-exist"; 9 | import { SDPInvalidError } from "../signaling/errors/sdp-invalid"; 10 | import { getLogger } from "../utils/logger"; 11 | import Emittery from "emittery"; 12 | 13 | export class Transporter { 14 | private logger = getLogger(); 15 | private connections = new Map(); 16 | private channels = new Map(); 17 | private queuedMessages = new Map(); 18 | private queuedCandidates = new Map(); 19 | private asyncResolver = new Emittery(); 20 | 21 | constructor( 22 | private config: ExtendedRTCConfiguration, 23 | private onConnectionConnect: (id: string) => Promise, 24 | private onConnectionDisconnect: (id: string) => Promise, 25 | private onChannelOpen: (id: string) => Promise, 26 | private onChannelClose: (id: string) => Promise 27 | ) {} 28 | 29 | async close() { 30 | this.logger.debug("Closing transporter"); 31 | 32 | for (let connection of this.connections) { 33 | await this.shutdown(connection[0]); 34 | } 35 | } 36 | 37 | async getOffer( 38 | answererId: string, 39 | handleCandidate: (candidate: string) => Promise 40 | ) { 41 | this.logger.debug("Getting offer", { answererId }); 42 | 43 | const connection = new RTCPeerConnection(this.config); 44 | this.connections.set(answererId, connection); 45 | 46 | connection.onconnectionstatechange = async () => 47 | await this.handleConnectionStatusChange( 48 | connection.connectionState, 49 | answererId 50 | ); 51 | 52 | connection.onicecandidate = async (e) => { 53 | e.candidate && handleCandidate(JSON.stringify(e.candidate)); 54 | }; 55 | 56 | const channel = connection.createDataChannel("channel"); 57 | this.channels.set(answererId, channel); 58 | 59 | channel.onopen = async () => { 60 | this.logger.debug("Channel opened", { id: answererId }); 61 | 62 | await this.asyncResolver.emit(this.getChannelKey(answererId), true); 63 | 64 | await this.onChannelOpen(answererId); 65 | }; 66 | channel.onmessage = async (msg) => { 67 | await this.queueAndEmitMessage(answererId, msg.data); 68 | }; 69 | channel.onclose = async () => { 70 | this.logger.debug("Channel close", { id: answererId }); 71 | 72 | await this.onChannelClose(answererId); 73 | }; 74 | 75 | this.queuedMessages.set(answererId, []); 76 | 77 | this.logger.verbose("Created channel", { 78 | newChannels: JSON.stringify(Array.from(this.channels.keys())), 79 | }); 80 | 81 | const offer = await connection.createOffer(); 82 | await connection.setLocalDescription(offer); 83 | 84 | this.logger.debug("Created offer", { offer: offer.sdp }); 85 | 86 | if (offer.sdp === undefined) { 87 | connection.close(); 88 | 89 | throw new SDPInvalidError(); 90 | } 91 | 92 | this.logger.debug("Created connection", { 93 | newConnections: JSON.stringify(Array.from(this.connections.keys())), 94 | }); 95 | 96 | return offer.sdp; 97 | } 98 | 99 | async handleOffer( 100 | id: string, 101 | offer: string, 102 | handleCandidate: (candidate: string) => Promise 103 | ) { 104 | this.logger.debug("Handling offer", { id, offer }); 105 | 106 | const connection = new RTCPeerConnection(this.config); 107 | this.connections.set(id, connection); 108 | 109 | connection.onconnectionstatechange = async () => 110 | await this.handleConnectionStatusChange(connection.connectionState, id); 111 | 112 | connection.onicecandidate = async (e) => { 113 | e.candidate && handleCandidate(JSON.stringify(e.candidate)); 114 | }; 115 | 116 | await connection.setRemoteDescription( 117 | new RTCSessionDescription({ 118 | type: "offer", 119 | sdp: offer, 120 | }) 121 | ); 122 | 123 | const answer = await connection.createAnswer(); 124 | 125 | this.logger.debug("Created answer", { offer: offer, answer: answer.sdp }); 126 | 127 | await connection.setLocalDescription(answer); 128 | 129 | await this.addQueuedCandidates(id); 130 | 131 | connection.ondatachannel = async ({ channel }) => { 132 | this.channels.set(id, channel); 133 | 134 | channel.onopen = async () => { 135 | this.logger.debug("Channel opened", { id }); 136 | 137 | await this.asyncResolver.emit(this.getChannelKey(id), true); 138 | 139 | await this.onChannelOpen(id); 140 | }; 141 | channel.onmessage = async (msg) => { 142 | await this.queueAndEmitMessage(id, msg.data); 143 | }; 144 | channel.onclose = async () => { 145 | this.logger.debug("Channel close", { id }); 146 | 147 | await this.onChannelClose(id); 148 | }; 149 | 150 | this.queuedMessages.set(id, []); 151 | 152 | this.logger.debug("Created channel", { 153 | newChannels: JSON.stringify(Array.from(this.channels.keys())), 154 | }); 155 | }; 156 | 157 | this.logger.debug("Created connection", { 158 | newConnections: JSON.stringify(Array.from(this.connections.keys())), 159 | }); 160 | 161 | if (answer.sdp === undefined) { 162 | throw new SDPInvalidError(); 163 | } 164 | 165 | return answer.sdp; 166 | } 167 | 168 | async handleAnswer(id: string, answer: string) { 169 | this.logger.debug("Handling answer", { id, answer }); 170 | 171 | if (this.connections.has(id)) { 172 | const connection = this.connections.get(id); 173 | 174 | await connection?.setRemoteDescription( 175 | new RTCSessionDescription({ 176 | type: "answer", 177 | sdp: answer, 178 | }) 179 | ); 180 | 181 | await this.addQueuedCandidates(id); 182 | } else { 183 | throw new ConnectionDoesNotExistError(); 184 | } 185 | } 186 | 187 | async handleCandidate(id: string, candidate: string) { 188 | this.logger.debug("Handling candidate", { id, candidate }); 189 | 190 | if ( 191 | this.connections.has(id) && 192 | this.connections.get(id)!.remoteDescription // We check with `.has` and never push undefined 193 | ) { 194 | const connection = this.connections.get(id); 195 | 196 | await connection?.addIceCandidate( 197 | new RTCIceCandidate(JSON.parse(candidate)) 198 | ); 199 | 200 | this.logger.debug("Added candidate", { id, candidate }); 201 | } else { 202 | this.logger.debug("Queueing candidate", { id, candidate }); 203 | 204 | if (!this.queuedCandidates.has(id)) this.queuedCandidates.set(id, []); 205 | 206 | this.queuedCandidates.get(id)?.push(candidate); 207 | } 208 | } 209 | 210 | async shutdown(id: string) { 211 | this.logger.debug("Shutting down", { id }); 212 | 213 | if (this.connections.has(id)) { 214 | this.logger.debug("Shutting down connection", { id }); 215 | 216 | this.connections.get(id)?.close(); 217 | 218 | this.connections.delete(id); 219 | 220 | this.logger.debug("Deleted connection", { 221 | newConnections: JSON.stringify(Array.from(this.connections.keys())), 222 | }); 223 | } 224 | 225 | if (this.channels.has(id)) { 226 | this.logger.verbose("Shutting down channel", { id }); 227 | 228 | this.channels.get(id)?.close(); 229 | 230 | this.channels.delete(id); 231 | 232 | this.logger.debug("Deleted channel", { 233 | newChannels: JSON.stringify(Array.from(this.connections.keys())), 234 | }); 235 | } 236 | 237 | if (this.queuedCandidates.has(id)) { 238 | this.logger.debug("Removing queued candidates", { id }); 239 | 240 | this.queuedCandidates.delete(id); 241 | 242 | this.logger.debug("Deleted queued candidate", { 243 | newQueuedCandidates: JSON.stringify( 244 | Array.from(this.queuedCandidates.keys()) 245 | ), 246 | }); 247 | } 248 | 249 | if (this.queuedMessages.has(id)) { 250 | this.logger.debug("Removing queued messages", { id }); 251 | 252 | this.queuedMessages.delete(id); 253 | 254 | this.logger.debug("Deleted queued messages", { 255 | newQueuedMessages: JSON.stringify( 256 | Array.from(this.queuedMessages.keys()) 257 | ), 258 | }); 259 | } 260 | } 261 | 262 | async send(id: string, msg: Uint8Array) { 263 | this.logger.debug("Handling send", { id, msg }); 264 | 265 | let channel = this.channels.get(id); 266 | 267 | while ( 268 | !channel || 269 | channel!.readyState !== "open" // Checked by !channel 270 | ) { 271 | await this.asyncResolver.once(this.getChannelKey(id)); 272 | 273 | channel = this.channels.get(id); 274 | } 275 | 276 | channel!.send(msg); // We check above 277 | } 278 | 279 | async recv(id: string) { 280 | this.logger.debug("Handling receive", { id }); 281 | 282 | if ( 283 | this.queuedMessages.has(id) && 284 | this.queuedMessages.get(id)?.length !== 0 // Checked by .has 285 | ) { 286 | return this.queuedMessages.get(id)?.shift()!; // size !== 0 and undefined is ever pushed 287 | } else { 288 | const msg = await this.asyncResolver.once(this.getMessageKey(id)); 289 | 290 | this.queuedMessages.get(id)?.shift(); 291 | 292 | return msg! as Uint8Array; 293 | } 294 | } 295 | 296 | private async handleConnectionStatusChange( 297 | connectionState: string, 298 | id: string 299 | ) { 300 | this.logger.silly("Handling connection status change", { 301 | connectionState, 302 | id, 303 | }); 304 | 305 | if (connectionState === "closed") { 306 | await this.onConnectionDisconnect(id); 307 | 308 | await this.shutdown(id); 309 | } else { 310 | connectionState === "connected" && (await this.onConnectionConnect(id)); 311 | } 312 | } 313 | 314 | private async queueAndEmitMessage(id: string, rawMsg: ArrayBuffer) { 315 | this.logger.silly("Queueing message", { id, rawMsg }); 316 | 317 | const msg = new Uint8Array(rawMsg); 318 | 319 | if (this.channels.has(id)) { 320 | const messages = this.queuedMessages.get(id); 321 | 322 | messages?.push(msg); 323 | 324 | await this.asyncResolver.emit(this.getMessageKey(id), msg); 325 | } else { 326 | throw new ChannelDoesNotExistError(); 327 | } 328 | } 329 | 330 | private async addQueuedCandidates(id: string) { 331 | this.logger.silly("Queueing candidate", { id }); 332 | 333 | this.queuedCandidates.get(id)?.forEach(async (candidate) => { 334 | this.queuedCandidates.set( 335 | id, 336 | this.queuedCandidates.get(id)?.filter((c) => c !== candidate)! // This only runs if it is not undefined 337 | ); 338 | 339 | await this.handleCandidate(id, candidate); 340 | }); 341 | 342 | this.logger.silly("Added queued candidate", { 343 | newQueuedCandidates: JSON.stringify(Array.from(this.queuedCandidates)), 344 | }); 345 | } 346 | 347 | private getMessageKey(id: string) { 348 | this.logger.silly("Getting message key", { id }); 349 | 350 | return `message id=${id}`; 351 | } 352 | 353 | private getChannelKey(id: string) { 354 | this.logger.silly("Getting channel key", { id }); 355 | 356 | return `channel id=${id}`; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /pkg/web/utils/getAsBinary.ts: -------------------------------------------------------------------------------- 1 | export const getAsBinary = (val: number) => { 2 | const valInMemory = new ArrayBuffer(4); 3 | 4 | new Int32Array(valInMemory)[0] = val; 5 | 6 | return Array.from(new Uint8Array(valInMemory)); 7 | }; 8 | -------------------------------------------------------------------------------- /pkg/web/utils/htons.ts: -------------------------------------------------------------------------------- 1 | export const htons = (val: number) => ((val & 0xff) << 8) | ((val >> 8) & 0xff); 2 | -------------------------------------------------------------------------------- /pkg/web/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from "winston"; 2 | import { Console } from "winston/lib/winston/transports"; 3 | 4 | export const getLogger = () => 5 | createLogger({ 6 | transports: new Console(), 7 | level: process.env.LOG_LEVEL, 8 | }); 9 | -------------------------------------------------------------------------------- /protocol.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | title unisockets Signaling Protocol 3 | 4 | actor "External System 1" as e1 5 | participant "Binding Client" as bc 6 | participant "Signaling Server" as ss 7 | participant "Connecting Client" as cc 8 | actor "External System 2" as e2 9 | 10 | e1 ->> bc: Opens client 11 | 12 | bc ->> ss: Knock(subnet: 127.0.0) 13 | ss -->> bc: Acknowledgement(id: 127.0.0.1, rejected: false) 14 | 15 | e1 ->> bc: Binds alias 16 | 17 | bc ->> ss: Bind(id: 127.0.0.1, alias: 127.0.0.1:1234) 18 | ss -->> bc: Alias(id: 127.0.0.1, alias: 127.0.0.1:1234, set: true) 19 | 20 | e1 ->> bc: Accepts clients 21 | 22 | bc ->> ss: Accepting(id: 127.0.0.1, alias: 127.0.0.1:1234) 23 | 24 | e2 ->> cc: Opens client 25 | 26 | cc ->> ss: Knock(subnet: 127.0.0) 27 | ss -->> cc: Acknowledgement(id: 127.0.0.2, rejected: false) 28 | 29 | ss ->> bc: Greeting(offererId: 127.0.0.1, answererId: 127.0.0.2) 30 | bc -->> ss: Offer(offererId: 127.0.0.1, answererId: 127.0.0.2, offer: o1) 31 | 32 | ss ->> cc: Offer(offererId: 127.0.0.1, answererId: 127.0.0.2, offer: o1) 33 | 34 | cc -->> ss: Answer(offererId: 127.0.0.1, answererId: 127.0.0.2, answer: a1) 35 | 36 | ss -->> bc: Answer(offererId: 127.0.0.1, answererId: 127.0.0.2, answer: a1) 37 | 38 | bc ->> ss: Candidate(offererId: 127.0.0.1, answererId: 127.0.0.2, candidate: c1) 39 | bc ->> ss: Candidate(offererId: 127.0.0.1, answererId: 127.0.0.2, candidate: c2) 40 | 41 | ss ->> cc: Candidate(offererId: 127.0.0.1, answererId: 127.0.0.2, candidate: c1) 42 | ss ->> cc: Candidate(offererId: 127.0.0.1, answererId: 127.0.0.2, candidate: c2) 43 | cc ->> ss: Candidate(offererId: 127.0.0.2, answererId: 127.0.0.1, candidate: c3) 44 | cc ->> ss: Candidate(offererId: 127.0.0.2, answererId: 127.0.0.1, candidate: c4) 45 | 46 | ss ->> bc: Candidate(offererId: 127.0.0.2, answererId: 127.0.0.1, candidate: c3) 47 | ss ->> bc: Candidate(offererId: 127.0.0.2, answererId: 127.0.0.1, candidate: c4) 48 | 49 | e2 ->> cc: Requests connection 50 | 51 | cc ->> ss: Connect(id: 127.0.0.2, clientConnectionId: co1, remoteAlias: 127.0.0.1:1234) 52 | 53 | ss -->> cc: Alias(id: 127.0.0.2, alias: 127.0.0.2:0, set: true, clientConnectionId: co1, isConnectionAlias: true) 54 | ss ->> bc: Alias(id: 127.0.0.2, alias: 127.0.0.2:0, set: true) 55 | 56 | ss ->> bc: Accept(boundAlias: 127.0.0.1:1234, clientAlias: 127.0.0.2:0) 57 | bc -->> ss: Accepting(id: 127.0.0.1, alias: 127.0.0.1:1234) 58 | 59 | ss -->> cc: Alias(id: 127.0.0.1, alias: 127.0.0.1:1234, set: true, clientConnectionId: co1) 60 | 61 | note over bc,cc:Handshake complete 62 | 63 | e2 ->> cc: Closes or shuts down client 64 | 65 | ss ->> bc: Goodbye(id: 127.0.0.2) 66 | ss ->> bc: Alias(id: 127.0.0.2, alias: 127.0.0.2:0, set:false) 67 | 68 | note over bc,cc:Removal complete 69 | @enduml -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | import esbuild from "rollup-plugin-esbuild"; 3 | import hashbang from "rollup-plugin-hashbang"; 4 | import { 5 | main, 6 | module, 7 | source, 8 | types, 9 | binSource, 10 | binMain, 11 | } from "./package.json"; 12 | 13 | const bundle = (format) => ({ 14 | input: source, 15 | output: { 16 | file: format == "cjs" ? main : format == "dts" ? types : module, 17 | format: format == "cjs" ? "cjs" : "es", 18 | sourcemap: format != "dts", 19 | }, 20 | plugins: format == "dts" ? [dts()] : [esbuild()], 21 | external: (id) => !/^[./]/.test(id), 22 | }); 23 | 24 | const bundleBin = () => ({ 25 | input: binSource, 26 | output: { 27 | file: binMain, 28 | format: "cjs", 29 | sourcemap: false, 30 | }, 31 | plugins: [esbuild(), hashbang()], 32 | external: (id) => !/^[./]/.test(id), 33 | }); 34 | 35 | export default [bundle("es"), bundle("cjs"), bundle("dts"), bundleBin()]; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "allowJs": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vendor/go/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | // - Webpack 15 | 16 | if (typeof global !== "undefined") { 17 | // global already exists 18 | } else if (typeof window !== "undefined") { 19 | window.global = window; 20 | } else if (typeof self !== "undefined") { 21 | self.global = self; 22 | } else { 23 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 24 | } 25 | 26 | if (!global.require && typeof require !== "undefined") { 27 | global.require = require; 28 | } 29 | 30 | if (!global.fs && global.require) { 31 | const fs = require("fs"); 32 | if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) { 33 | global.fs = fs; 34 | } 35 | } 36 | 37 | const enosys = () => { 38 | const err = new Error("not implemented"); 39 | err.code = "ENOSYS"; 40 | return err; 41 | }; 42 | 43 | if (!global.fs) { 44 | let outputBuf = ""; 45 | global.fs = { 46 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 47 | writeSync(fd, buf) { 48 | outputBuf += decoder.decode(buf); 49 | const nl = outputBuf.lastIndexOf("\n"); 50 | if (nl != -1) { 51 | console.log(outputBuf.substr(0, nl)); 52 | outputBuf = outputBuf.substr(nl + 1); 53 | } 54 | return buf.length; 55 | }, 56 | write(fd, buf, offset, length, position, callback) { 57 | if (offset !== 0 || length !== buf.length || position !== null) { 58 | callback(enosys()); 59 | return; 60 | } 61 | const n = this.writeSync(fd, buf); 62 | callback(null, n); 63 | }, 64 | chmod(path, mode, callback) { callback(enosys()); }, 65 | chown(path, uid, gid, callback) { callback(enosys()); }, 66 | close(fd, callback) { callback(enosys()); }, 67 | fchmod(fd, mode, callback) { callback(enosys()); }, 68 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 69 | fstat(fd, callback) { callback(enosys()); }, 70 | fsync(fd, callback) { callback(null); }, 71 | ftruncate(fd, length, callback) { callback(enosys()); }, 72 | lchown(path, uid, gid, callback) { callback(enosys()); }, 73 | link(path, link, callback) { callback(enosys()); }, 74 | lstat(path, callback) { callback(enosys()); }, 75 | mkdir(path, perm, callback) { callback(enosys()); }, 76 | open(path, flags, mode, callback) { callback(enosys()); }, 77 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 78 | readdir(path, callback) { callback(enosys()); }, 79 | readlink(path, callback) { callback(enosys()); }, 80 | rename(from, to, callback) { callback(enosys()); }, 81 | rmdir(path, callback) { callback(enosys()); }, 82 | stat(path, callback) { callback(enosys()); }, 83 | symlink(path, link, callback) { callback(enosys()); }, 84 | truncate(path, length, callback) { callback(enosys()); }, 85 | unlink(path, callback) { callback(enosys()); }, 86 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 87 | }; 88 | } 89 | 90 | if (!global.process) { 91 | global.process = { 92 | getuid() { return -1; }, 93 | getgid() { return -1; }, 94 | geteuid() { return -1; }, 95 | getegid() { return -1; }, 96 | getgroups() { throw enosys(); }, 97 | pid: -1, 98 | ppid: -1, 99 | umask() { throw enosys(); }, 100 | cwd() { throw enosys(); }, 101 | chdir() { throw enosys(); }, 102 | } 103 | } 104 | 105 | if (!global.crypto && global.require) { 106 | const nodeCrypto = require("crypto"); 107 | global.crypto = { 108 | getRandomValues(b) { 109 | nodeCrypto.randomFillSync(b); 110 | }, 111 | }; 112 | } 113 | if (!global.crypto) { 114 | throw new Error("global.crypto is not available, polyfill required (getRandomValues only)"); 115 | } 116 | 117 | if (!global.performance) { 118 | global.performance = { 119 | now() { 120 | const [sec, nsec] = process.hrtime(); 121 | return sec * 1000 + nsec / 1000000; 122 | }, 123 | }; 124 | } 125 | 126 | if (!global.TextEncoder && global.require) { 127 | global.TextEncoder = require("util").TextEncoder; 128 | } 129 | if (!global.TextEncoder) { 130 | throw new Error("global.TextEncoder is not available, polyfill required"); 131 | } 132 | 133 | if (!global.TextDecoder && global.require) { 134 | global.TextDecoder = require("util").TextDecoder; 135 | } 136 | if (!global.TextDecoder) { 137 | throw new Error("global.TextDecoder is not available, polyfill required"); 138 | } 139 | 140 | // End of polyfills for common API. 141 | 142 | const encoder = new TextEncoder("utf-8"); 143 | const decoder = new TextDecoder("utf-8"); 144 | 145 | global.Go = class { 146 | constructor() { 147 | this.argv = ["js"]; 148 | this.env = {}; 149 | this.exit = (code) => { 150 | if (code !== 0) { 151 | console.warn("exit code:", code); 152 | } 153 | }; 154 | this._exitPromise = new Promise((resolve) => { 155 | this._resolveExitPromise = resolve; 156 | }); 157 | this._pendingEvent = null; 158 | this._scheduledTimeouts = new Map(); 159 | this._nextCallbackTimeoutID = 1; 160 | 161 | const setInt64 = (addr, v) => { 162 | this.mem.setUint32(addr + 0, v, true); 163 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 164 | } 165 | 166 | const getInt64 = (addr) => { 167 | const low = this.mem.getUint32(addr + 0, true); 168 | const high = this.mem.getInt32(addr + 4, true); 169 | return low + high * 4294967296; 170 | } 171 | 172 | const loadValue = (addr) => { 173 | const f = this.mem.getFloat64(addr, true); 174 | if (f === 0) { 175 | return undefined; 176 | } 177 | if (!isNaN(f)) { 178 | return f; 179 | } 180 | 181 | const id = this.mem.getUint32(addr, true); 182 | return this._values[id]; 183 | } 184 | 185 | const storeValue = (addr, v) => { 186 | const nanHead = 0x7FF80000; 187 | 188 | if (typeof v === "number" && v !== 0) { 189 | if (isNaN(v)) { 190 | this.mem.setUint32(addr + 4, nanHead, true); 191 | this.mem.setUint32(addr, 0, true); 192 | return; 193 | } 194 | this.mem.setFloat64(addr, v, true); 195 | return; 196 | } 197 | 198 | if (v === undefined) { 199 | this.mem.setFloat64(addr, 0, true); 200 | return; 201 | } 202 | 203 | let id = this._ids.get(v); 204 | if (id === undefined) { 205 | id = this._idPool.pop(); 206 | if (id === undefined) { 207 | id = this._values.length; 208 | } 209 | this._values[id] = v; 210 | this._goRefCounts[id] = 0; 211 | this._ids.set(v, id); 212 | } 213 | this._goRefCounts[id]++; 214 | let typeFlag = 0; 215 | switch (typeof v) { 216 | case "object": 217 | if (v !== null) { 218 | typeFlag = 1; 219 | } 220 | break; 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | sp >>>= 0; 268 | const code = this.mem.getInt32(sp + 8, true); 269 | this.exited = true; 270 | delete this._inst; 271 | delete this._values; 272 | delete this._goRefCounts; 273 | delete this._ids; 274 | delete this._idPool; 275 | this.exit(code); 276 | }, 277 | 278 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 279 | "runtime.wasmWrite": (sp) => { 280 | sp >>>= 0; 281 | const fd = getInt64(sp + 8); 282 | const p = getInt64(sp + 16); 283 | const n = this.mem.getInt32(sp + 24, true); 284 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 285 | }, 286 | 287 | // func resetMemoryDataView() 288 | "runtime.resetMemoryDataView": (sp) => { 289 | sp >>>= 0; 290 | this.mem = new DataView(this._inst.exports.mem.buffer); 291 | }, 292 | 293 | // func nanotime1() int64 294 | "runtime.nanotime1": (sp) => { 295 | sp >>>= 0; 296 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 297 | }, 298 | 299 | // func walltime1() (sec int64, nsec int32) 300 | "runtime.walltime1": (sp) => { 301 | sp >>>= 0; 302 | const msec = (new Date).getTime(); 303 | setInt64(sp + 8, msec / 1000); 304 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 305 | }, 306 | 307 | // func scheduleTimeoutEvent(delay int64) int32 308 | "runtime.scheduleTimeoutEvent": (sp) => { 309 | sp >>>= 0; 310 | const id = this._nextCallbackTimeoutID; 311 | this._nextCallbackTimeoutID++; 312 | this._scheduledTimeouts.set(id, setTimeout( 313 | () => { 314 | this._resume(); 315 | while (this._scheduledTimeouts.has(id)) { 316 | // for some reason Go failed to register the timeout event, log and try again 317 | // (temporary workaround for https://github.com/golang/go/issues/28975) 318 | console.warn("scheduleTimeoutEvent: missed timeout event"); 319 | this._resume(); 320 | } 321 | }, 322 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 323 | )); 324 | this.mem.setInt32(sp + 16, id, true); 325 | }, 326 | 327 | // func clearTimeoutEvent(id int32) 328 | "runtime.clearTimeoutEvent": (sp) => { 329 | sp >>>= 0; 330 | const id = this.mem.getInt32(sp + 8, true); 331 | clearTimeout(this._scheduledTimeouts.get(id)); 332 | this._scheduledTimeouts.delete(id); 333 | }, 334 | 335 | // func getRandomData(r []byte) 336 | "runtime.getRandomData": (sp) => { 337 | sp >>>= 0; 338 | crypto.getRandomValues(loadSlice(sp + 8)); 339 | }, 340 | 341 | // func finalizeRef(v ref) 342 | "syscall/js.finalizeRef": (sp) => { 343 | sp >>>= 0; 344 | const id = this.mem.getUint32(sp + 8, true); 345 | this._goRefCounts[id]--; 346 | if (this._goRefCounts[id] === 0) { 347 | const v = this._values[id]; 348 | this._values[id] = null; 349 | this._ids.delete(v); 350 | this._idPool.push(id); 351 | } 352 | }, 353 | 354 | // func stringVal(value string) ref 355 | "syscall/js.stringVal": (sp) => { 356 | sp >>>= 0; 357 | storeValue(sp + 24, loadString(sp + 8)); 358 | }, 359 | 360 | // func valueGet(v ref, p string) ref 361 | "syscall/js.valueGet": (sp) => { 362 | sp >>>= 0; 363 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 364 | sp = this._inst.exports.getsp() >>> 0; // see comment above 365 | storeValue(sp + 32, result); 366 | }, 367 | 368 | // func valueSet(v ref, p string, x ref) 369 | "syscall/js.valueSet": (sp) => { 370 | sp >>>= 0; 371 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 372 | }, 373 | 374 | // func valueDelete(v ref, p string) 375 | "syscall/js.valueDelete": (sp) => { 376 | sp >>>= 0; 377 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 378 | }, 379 | 380 | // func valueIndex(v ref, i int) ref 381 | "syscall/js.valueIndex": (sp) => { 382 | sp >>>= 0; 383 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 384 | }, 385 | 386 | // valueSetIndex(v ref, i int, x ref) 387 | "syscall/js.valueSetIndex": (sp) => { 388 | sp >>>= 0; 389 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 390 | }, 391 | 392 | // func valueCall(v ref, m string, args []ref) (ref, bool) 393 | "syscall/js.valueCall": (sp) => { 394 | sp >>>= 0; 395 | try { 396 | const v = loadValue(sp + 8); 397 | const m = Reflect.get(v, loadString(sp + 16)); 398 | const args = loadSliceOfValues(sp + 32); 399 | const result = Reflect.apply(m, v, args); 400 | sp = this._inst.exports.getsp() >>> 0; // see comment above 401 | storeValue(sp + 56, result); 402 | this.mem.setUint8(sp + 64, 1); 403 | } catch (err) { 404 | storeValue(sp + 56, err); 405 | this.mem.setUint8(sp + 64, 0); 406 | } 407 | }, 408 | 409 | // func valueInvoke(v ref, args []ref) (ref, bool) 410 | "syscall/js.valueInvoke": (sp) => { 411 | sp >>>= 0; 412 | try { 413 | const v = loadValue(sp + 8); 414 | const args = loadSliceOfValues(sp + 16); 415 | const result = Reflect.apply(v, undefined, args); 416 | sp = this._inst.exports.getsp() >>> 0; // see comment above 417 | storeValue(sp + 40, result); 418 | this.mem.setUint8(sp + 48, 1); 419 | } catch (err) { 420 | storeValue(sp + 40, err); 421 | this.mem.setUint8(sp + 48, 0); 422 | } 423 | }, 424 | 425 | // func valueNew(v ref, args []ref) (ref, bool) 426 | "syscall/js.valueNew": (sp) => { 427 | sp >>>= 0; 428 | try { 429 | const v = loadValue(sp + 8); 430 | const args = loadSliceOfValues(sp + 16); 431 | const result = Reflect.construct(v, args); 432 | sp = this._inst.exports.getsp() >>> 0; // see comment above 433 | storeValue(sp + 40, result); 434 | this.mem.setUint8(sp + 48, 1); 435 | } catch (err) { 436 | storeValue(sp + 40, err); 437 | this.mem.setUint8(sp + 48, 0); 438 | } 439 | }, 440 | 441 | // func valueLength(v ref) int 442 | "syscall/js.valueLength": (sp) => { 443 | sp >>>= 0; 444 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 445 | }, 446 | 447 | // valuePrepareString(v ref) (ref, int) 448 | "syscall/js.valuePrepareString": (sp) => { 449 | sp >>>= 0; 450 | const str = encoder.encode(String(loadValue(sp + 8))); 451 | storeValue(sp + 16, str); 452 | setInt64(sp + 24, str.length); 453 | }, 454 | 455 | // valueLoadString(v ref, b []byte) 456 | "syscall/js.valueLoadString": (sp) => { 457 | sp >>>= 0; 458 | const str = loadValue(sp + 8); 459 | loadSlice(sp + 16).set(str); 460 | }, 461 | 462 | // func valueInstanceOf(v ref, t ref) bool 463 | "syscall/js.valueInstanceOf": (sp) => { 464 | sp >>>= 0; 465 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 466 | }, 467 | 468 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 469 | "syscall/js.copyBytesToGo": (sp) => { 470 | sp >>>= 0; 471 | const dst = loadSlice(sp + 8); 472 | const src = loadValue(sp + 32); 473 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 474 | this.mem.setUint8(sp + 48, 0); 475 | return; 476 | } 477 | const toCopy = src.subarray(0, dst.length); 478 | dst.set(toCopy); 479 | setInt64(sp + 40, toCopy.length); 480 | this.mem.setUint8(sp + 48, 1); 481 | }, 482 | 483 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 484 | "syscall/js.copyBytesToJS": (sp) => { 485 | sp >>>= 0; 486 | const dst = loadValue(sp + 8); 487 | const src = loadSlice(sp + 16); 488 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 489 | this.mem.setUint8(sp + 48, 0); 490 | return; 491 | } 492 | const toCopy = src.subarray(0, dst.length); 493 | dst.set(toCopy); 494 | setInt64(sp + 40, toCopy.length); 495 | this.mem.setUint8(sp + 48, 1); 496 | }, 497 | 498 | "debug": (value) => { 499 | console.log(value); 500 | }, 501 | } 502 | }; 503 | } 504 | 505 | async run(instance) { 506 | if (!(instance instanceof WebAssembly.Instance)) { 507 | throw new Error("Go.run: WebAssembly.Instance expected"); 508 | } 509 | this._inst = instance; 510 | this.mem = new DataView(this._inst.exports.mem.buffer); 511 | this._values = [ // JS values that Go currently has references to, indexed by reference id 512 | NaN, 513 | 0, 514 | null, 515 | true, 516 | false, 517 | global, 518 | this, 519 | ]; 520 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 521 | this._ids = new Map([ // mapping from JS values to reference ids 522 | [0, 1], 523 | [null, 2], 524 | [true, 3], 525 | [false, 4], 526 | [global, 5], 527 | [this, 6], 528 | ]); 529 | this._idPool = []; // unused ids that have been garbage collected 530 | this.exited = false; // whether the Go program has exited 531 | 532 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 533 | let offset = 4096; 534 | 535 | const strPtr = (str) => { 536 | const ptr = offset; 537 | const bytes = encoder.encode(str + "\0"); 538 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 539 | offset += bytes.length; 540 | if (offset % 8 !== 0) { 541 | offset += 8 - (offset % 8); 542 | } 543 | return ptr; 544 | }; 545 | 546 | const argc = this.argv.length; 547 | 548 | const argvPtrs = []; 549 | this.argv.forEach((arg) => { 550 | argvPtrs.push(strPtr(arg)); 551 | }); 552 | argvPtrs.push(0); 553 | 554 | const keys = Object.keys(this.env).sort(); 555 | keys.forEach((key) => { 556 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 557 | }); 558 | argvPtrs.push(0); 559 | 560 | const argv = offset; 561 | argvPtrs.forEach((ptr) => { 562 | this.mem.setUint32(offset, ptr, true); 563 | this.mem.setUint32(offset + 4, 0, true); 564 | offset += 8; 565 | }); 566 | 567 | this._inst.exports.run(argc, argv); 568 | if (this.exited) { 569 | this._resolveExitPromise(); 570 | } 571 | await this._exitPromise; 572 | } 573 | 574 | _resume() { 575 | if (this.exited) { 576 | throw new Error("Go program has already exited"); 577 | } 578 | this._inst.exports.resume(); 579 | if (this.exited) { 580 | this._resolveExitPromise(); 581 | } 582 | } 583 | 584 | _makeFuncWrapper(id) { 585 | const go = this; 586 | return function () { 587 | const event = { id: id, this: this, args: arguments }; 588 | go._pendingEvent = event; 589 | go._resume(); 590 | return event.result; 591 | }; 592 | } 593 | } 594 | 595 | if ( 596 | typeof module !== "undefined" && 597 | global.require && 598 | global.require.main === module && 599 | global.process && 600 | global.process.versions && 601 | !global.process.versions.electron 602 | ) { 603 | if (process.argv.length < 3) { 604 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 605 | process.exit(1); 606 | } 607 | 608 | const go = new Go(); 609 | go.argv = process.argv.slice(2); 610 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 611 | go.exit = process.exit; 612 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 613 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 614 | if (code === 0 && !go.exited) { 615 | // deadlock, make Go print error and stack traces 616 | go._pendingEvent = { id: 0 }; 617 | go._resume(); 618 | } 619 | }); 620 | return go.run(result.instance); 621 | }).catch((err) => { 622 | console.error(err); 623 | process.exit(1); 624 | }); 625 | } 626 | })(); 627 | module.exports = Go 628 | -------------------------------------------------------------------------------- /vendor/tinygo/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // This file has been modified for use by the TinyGo compiler. 6 | 7 | (() => { 8 | // Map multiple JavaScript environments to a single common API, 9 | // preferring web standards over Node.js API. 10 | // 11 | // Environments considered: 12 | // - Browsers 13 | // - Node.js 14 | // - Electron 15 | // - Parcel 16 | 17 | if (typeof global !== "undefined") { 18 | // global already exists 19 | } else if (typeof window !== "undefined") { 20 | window.global = window; 21 | } else if (typeof self !== "undefined") { 22 | self.global = self; 23 | } else { 24 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 25 | } 26 | 27 | if (!global.require && typeof require !== "undefined") { 28 | global.require = require; 29 | } 30 | 31 | if (!global.fs && global.require) { 32 | global.fs = require("fs"); 33 | } 34 | 35 | const enosys = () => { 36 | const err = new Error("not implemented"); 37 | err.code = "ENOSYS"; 38 | return err; 39 | }; 40 | 41 | if (!global.fs) { 42 | let outputBuf = ""; 43 | global.fs = { 44 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 45 | writeSync(fd, buf) { 46 | outputBuf += decoder.decode(buf); 47 | const nl = outputBuf.lastIndexOf("\n"); 48 | if (nl != -1) { 49 | console.log(outputBuf.substr(0, nl)); 50 | outputBuf = outputBuf.substr(nl + 1); 51 | } 52 | return buf.length; 53 | }, 54 | write(fd, buf, offset, length, position, callback) { 55 | if (offset !== 0 || length !== buf.length || position !== null) { 56 | callback(enosys()); 57 | return; 58 | } 59 | const n = this.writeSync(fd, buf); 60 | callback(null, n); 61 | }, 62 | chmod(path, mode, callback) { callback(enosys()); }, 63 | chown(path, uid, gid, callback) { callback(enosys()); }, 64 | close(fd, callback) { callback(enosys()); }, 65 | fchmod(fd, mode, callback) { callback(enosys()); }, 66 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 67 | fstat(fd, callback) { callback(enosys()); }, 68 | fsync(fd, callback) { callback(null); }, 69 | ftruncate(fd, length, callback) { callback(enosys()); }, 70 | lchown(path, uid, gid, callback) { callback(enosys()); }, 71 | link(path, link, callback) { callback(enosys()); }, 72 | lstat(path, callback) { callback(enosys()); }, 73 | mkdir(path, perm, callback) { callback(enosys()); }, 74 | open(path, flags, mode, callback) { callback(enosys()); }, 75 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 76 | readdir(path, callback) { callback(enosys()); }, 77 | readlink(path, callback) { callback(enosys()); }, 78 | rename(from, to, callback) { callback(enosys()); }, 79 | rmdir(path, callback) { callback(enosys()); }, 80 | stat(path, callback) { callback(enosys()); }, 81 | symlink(path, link, callback) { callback(enosys()); }, 82 | truncate(path, length, callback) { callback(enosys()); }, 83 | unlink(path, callback) { callback(enosys()); }, 84 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 85 | }; 86 | } 87 | 88 | if (!global.process) { 89 | global.process = { 90 | getuid() { return -1; }, 91 | getgid() { return -1; }, 92 | geteuid() { return -1; }, 93 | getegid() { return -1; }, 94 | getgroups() { throw enosys(); }, 95 | pid: -1, 96 | ppid: -1, 97 | umask() { throw enosys(); }, 98 | cwd() { throw enosys(); }, 99 | chdir() { throw enosys(); }, 100 | } 101 | } 102 | 103 | if (!global.crypto) { 104 | const nodeCrypto = require("crypto"); 105 | global.crypto = { 106 | getRandomValues(b) { 107 | nodeCrypto.randomFillSync(b); 108 | }, 109 | }; 110 | } 111 | 112 | if (!global.performance) { 113 | global.performance = { 114 | now() { 115 | const [sec, nsec] = process.hrtime(); 116 | return sec * 1000 + nsec / 1000000; 117 | }, 118 | }; 119 | } 120 | 121 | if (!global.TextEncoder) { 122 | global.TextEncoder = require("util").TextEncoder; 123 | } 124 | 125 | if (!global.TextDecoder) { 126 | global.TextDecoder = require("util").TextDecoder; 127 | } 128 | 129 | // End of polyfills for common API. 130 | 131 | const encoder = new TextEncoder("utf-8"); 132 | const decoder = new TextDecoder("utf-8"); 133 | var logLine = []; 134 | 135 | global.Go = class { 136 | constructor() { 137 | this._callbackTimeouts = new Map(); 138 | this._nextCallbackTimeoutID = 1; 139 | 140 | const mem = () => { 141 | // The buffer may change when requesting more memory. 142 | return new DataView(this._inst.exports.memory.buffer); 143 | } 144 | 145 | const setInt64 = (addr, v) => { 146 | mem().setUint32(addr + 0, v, true); 147 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 148 | } 149 | 150 | const getInt64 = (addr) => { 151 | const low = mem().getUint32(addr + 0, true); 152 | const high = mem().getInt32(addr + 4, true); 153 | return low + high * 4294967296; 154 | } 155 | 156 | const loadValue = (addr) => { 157 | const f = mem().getFloat64(addr, true); 158 | if (f === 0) { 159 | return undefined; 160 | } 161 | if (!isNaN(f)) { 162 | return f; 163 | } 164 | 165 | const id = mem().getUint32(addr, true); 166 | return this._values[id]; 167 | } 168 | 169 | const storeValue = (addr, v) => { 170 | const nanHead = 0x7FF80000; 171 | 172 | if (typeof v === "number") { 173 | if (isNaN(v)) { 174 | mem().setUint32(addr + 4, nanHead, true); 175 | mem().setUint32(addr, 0, true); 176 | return; 177 | } 178 | if (v === 0) { 179 | mem().setUint32(addr + 4, nanHead, true); 180 | mem().setUint32(addr, 1, true); 181 | return; 182 | } 183 | mem().setFloat64(addr, v, true); 184 | return; 185 | } 186 | 187 | switch (v) { 188 | case undefined: 189 | mem().setFloat64(addr, 0, true); 190 | return; 191 | case null: 192 | mem().setUint32(addr + 4, nanHead, true); 193 | mem().setUint32(addr, 2, true); 194 | return; 195 | case true: 196 | mem().setUint32(addr + 4, nanHead, true); 197 | mem().setUint32(addr, 3, true); 198 | return; 199 | case false: 200 | mem().setUint32(addr + 4, nanHead, true); 201 | mem().setUint32(addr, 4, true); 202 | return; 203 | } 204 | 205 | let id = this._ids.get(v); 206 | if (id === undefined) { 207 | id = this._idPool.pop(); 208 | if (id === undefined) { 209 | id = this._values.length; 210 | } 211 | this._values[id] = v; 212 | this._goRefCounts[id] = 0; 213 | this._ids.set(v, id); 214 | } 215 | this._goRefCounts[id]++; 216 | let typeFlag = 1; 217 | switch (typeof v) { 218 | case "string": 219 | typeFlag = 2; 220 | break; 221 | case "symbol": 222 | typeFlag = 3; 223 | break; 224 | case "function": 225 | typeFlag = 4; 226 | break; 227 | } 228 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 229 | mem().setUint32(addr, id, true); 230 | } 231 | 232 | const loadSlice = (array, len, cap) => { 233 | return new Uint8Array(this._inst.exports.memory.buffer, array, len); 234 | } 235 | 236 | const loadSliceOfValues = (array, len, cap) => { 237 | const a = new Array(len); 238 | for (let i = 0; i < len; i++) { 239 | a[i] = loadValue(array + i * 8); 240 | } 241 | return a; 242 | } 243 | 244 | const loadString = (ptr, len) => { 245 | return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); 246 | } 247 | 248 | const timeOrigin = Date.now() - performance.now(); 249 | this.importObject = { 250 | wasi_unstable: { 251 | // https://github.com/bytecodealliance/wasmtime/blob/master/docs/WASI-api.md#__wasi_fd_write 252 | fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { 253 | let nwritten = 0; 254 | if (fd == 1) { 255 | for (let iovs_i=0; iovs_i { 283 | return timeOrigin + performance.now(); 284 | }, 285 | 286 | // func sleepTicks(timeout float64) 287 | "runtime.sleepTicks": (timeout) => { 288 | // Do not sleep, only reactivate scheduler after the given timeout. 289 | setTimeout(this._inst.exports.go_scheduler, timeout); 290 | }, 291 | 292 | // func Exit(code int) 293 | "syscall.Exit": (code) => { 294 | if (global.process) { 295 | // Node.js 296 | process.exit(code); 297 | } else { 298 | // Can't exit in a browser. 299 | throw 'trying to exit with code ' + code; 300 | } 301 | }, 302 | 303 | // func finalizeRef(v ref) 304 | "syscall/js.finalizeRef": (sp) => { 305 | // Note: TinyGo does not support finalizers so this should never be 306 | // called. 307 | console.error('syscall/js.finalizeRef not implemented'); 308 | }, 309 | 310 | // func stringVal(value string) ref 311 | "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { 312 | const s = loadString(value_ptr, value_len); 313 | storeValue(ret_ptr, s); 314 | }, 315 | 316 | // func valueGet(v ref, p string) ref 317 | "syscall/js.valueGet": (retval, v_addr, p_ptr, p_len) => { 318 | let prop = loadString(p_ptr, p_len); 319 | let value = loadValue(v_addr); 320 | let result = Reflect.get(value, prop); 321 | storeValue(retval, result); 322 | }, 323 | 324 | // func valueSet(v ref, p string, x ref) 325 | "syscall/js.valueSet": (v_addr, p_ptr, p_len, x_addr) => { 326 | const v = loadValue(v_addr); 327 | const p = loadString(p_ptr, p_len); 328 | const x = loadValue(x_addr); 329 | Reflect.set(v, p, x); 330 | }, 331 | 332 | // func valueDelete(v ref, p string) 333 | "syscall/js.valueDelete": (v_addr, p_ptr, p_len) => { 334 | const v = loadValue(v_addr); 335 | const p = loadString(p_ptr, p_len); 336 | Reflect.deleteProperty(v, p); 337 | }, 338 | 339 | // func valueIndex(v ref, i int) ref 340 | "syscall/js.valueIndex": (ret_addr, v_addr, i) => { 341 | storeValue(ret_addr, Reflect.get(loadValue(v_addr), i)); 342 | }, 343 | 344 | // valueSetIndex(v ref, i int, x ref) 345 | "syscall/js.valueSetIndex": (v_addr, i, x_addr) => { 346 | Reflect.set(loadValue(v_addr), i, loadValue(x_addr)); 347 | }, 348 | 349 | // func valueCall(v ref, m string, args []ref) (ref, bool) 350 | "syscall/js.valueCall": (ret_addr, v_addr, m_ptr, m_len, args_ptr, args_len, args_cap) => { 351 | const v = loadValue(v_addr); 352 | const name = loadString(m_ptr, m_len); 353 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 354 | try { 355 | const m = Reflect.get(v, name); 356 | storeValue(ret_addr, Reflect.apply(m, v, args)); 357 | mem().setUint8(ret_addr + 8, 1); 358 | } catch (err) { 359 | storeValue(ret_addr, err); 360 | mem().setUint8(ret_addr + 8, 0); 361 | } 362 | }, 363 | 364 | // func valueInvoke(v ref, args []ref) (ref, bool) 365 | "syscall/js.valueInvoke": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { 366 | try { 367 | const v = loadValue(v_addr); 368 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 369 | storeValue(ret_addr, Reflect.apply(v, undefined, args)); 370 | mem().setUint8(ret_addr + 8, 1); 371 | } catch (err) { 372 | storeValue(ret_addr, err); 373 | mem().setUint8(ret_addr + 8, 0); 374 | } 375 | }, 376 | 377 | // func valueNew(v ref, args []ref) (ref, bool) 378 | "syscall/js.valueNew": (ret_addr, v_addr, args_ptr, args_len, args_cap) => { 379 | const v = loadValue(v_addr); 380 | const args = loadSliceOfValues(args_ptr, args_len, args_cap); 381 | try { 382 | storeValue(ret_addr, Reflect.construct(v, args)); 383 | mem().setUint8(ret_addr + 8, 1); 384 | } catch (err) { 385 | storeValue(ret_addr, err); 386 | mem().setUint8(ret_addr+ 8, 0); 387 | } 388 | }, 389 | 390 | // func valueLength(v ref) int 391 | "syscall/js.valueLength": (v_addr) => { 392 | return loadValue(v_addr).length; 393 | }, 394 | 395 | // valuePrepareString(v ref) (ref, int) 396 | "syscall/js.valuePrepareString": (ret_addr, v_addr) => { 397 | const s = String(loadValue(v_addr)); 398 | const str = encoder.encode(s); 399 | storeValue(ret_addr, str); 400 | setInt64(ret_addr + 8, str.length); 401 | }, 402 | 403 | // valueLoadString(v ref, b []byte) 404 | "syscall/js.valueLoadString": (v_addr, slice_ptr, slice_len, slice_cap) => { 405 | const str = loadValue(v_addr); 406 | loadSlice(slice_ptr, slice_len, slice_cap).set(str); 407 | }, 408 | 409 | // func valueInstanceOf(v ref, t ref) bool 410 | "syscall/js.valueInstanceOf": (v_addr, t_addr) => { 411 | return loadValue(v_attr) instanceof loadValue(t_addr); 412 | }, 413 | 414 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 415 | "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, source_addr) => { 416 | let num_bytes_copied_addr = ret_addr; 417 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 418 | 419 | const dst = loadSlice(dest_addr, dest_len); 420 | const src = loadValue(source_addr); 421 | if (!(src instanceof Uint8Array)) { 422 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 423 | return; 424 | } 425 | const toCopy = src.subarray(0, dst.length); 426 | dst.set(toCopy); 427 | setInt64(num_bytes_copied_addr, toCopy.length); 428 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 429 | }, 430 | 431 | // copyBytesToJS(dst ref, src []byte) (int, bool) 432 | // Originally copied from upstream Go project, then modified: 433 | // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 434 | "syscall/js.copyBytesToJS": (ret_addr, dest_addr, source_addr, source_len, source_cap) => { 435 | let num_bytes_copied_addr = ret_addr; 436 | let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable 437 | 438 | const dst = loadValue(dest_addr); 439 | const src = loadSlice(source_addr, source_len); 440 | if (!(dst instanceof Uint8Array)) { 441 | mem().setUint8(returned_status_addr, 0); // Return "not ok" status 442 | return; 443 | } 444 | const toCopy = src.subarray(0, dst.length); 445 | dst.set(toCopy); 446 | setInt64(num_bytes_copied_addr, toCopy.length); 447 | mem().setUint8(returned_status_addr, 1); // Return "ok" status 448 | }, 449 | } 450 | }; 451 | } 452 | 453 | async run(instance) { 454 | this._inst = instance; 455 | this._values = [ // JS values that Go currently has references to, indexed by reference id 456 | NaN, 457 | 0, 458 | null, 459 | true, 460 | false, 461 | global, 462 | this, 463 | ]; 464 | this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 465 | this._ids = new Map(); // mapping from JS values to reference ids 466 | this._idPool = []; // unused ids that have been garbage collected 467 | this.exited = false; // whether the Go program has exited 468 | 469 | const mem = new DataView(this._inst.exports.memory.buffer) 470 | 471 | while (true) { 472 | const callbackPromise = new Promise((resolve) => { 473 | this._resolveCallbackPromise = () => { 474 | if (this.exited) { 475 | throw new Error("bad callback: Go program has already exited"); 476 | } 477 | setTimeout(resolve, 0); // make sure it is asynchronous 478 | }; 479 | }); 480 | this._inst.exports._start(); 481 | if (this.exited) { 482 | break; 483 | } 484 | await callbackPromise; 485 | } 486 | } 487 | 488 | _resume() { 489 | if (this.exited) { 490 | throw new Error("Go program has already exited"); 491 | } 492 | this._inst.exports.resume(); 493 | if (this.exited) { 494 | this._resolveExitPromise(); 495 | } 496 | } 497 | 498 | _makeFuncWrapper(id) { 499 | const go = this; 500 | return function () { 501 | const event = { id: id, this: this, args: arguments }; 502 | go._pendingEvent = event; 503 | go._resume(); 504 | return event.result; 505 | }; 506 | } 507 | } 508 | 509 | if ( 510 | global.require && 511 | global.require.main === module && 512 | global.process && 513 | global.process.versions && 514 | !global.process.versions.electron 515 | ) { 516 | if (process.argv.length != 3) { 517 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 518 | process.exit(1); 519 | } 520 | 521 | const go = new Go(); 522 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 523 | return go.run(result.instance); 524 | }).catch((err) => { 525 | console.error(err); 526 | process.exit(1); 527 | }); 528 | } 529 | })(); 530 | 531 | module.exports = Go --------------------------------------------------------------------------------