├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── interop │ └── main.go └── simple │ └── main.go ├── api.go ├── cmd └── server │ └── main.go ├── conn.go ├── conn_data.go ├── conn_media.go ├── emitter └── emitter.go ├── enums └── enums.go ├── go.mod ├── go.sum ├── interop ├── js │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ └── package.json └── py │ └── requirements.txt ├── logger.go ├── mediastream.go ├── models └── message.go ├── negotiator.go ├── options.go ├── peer.go ├── peer_test.go ├── queue.go ├── server ├── auth.go ├── auth_test.go ├── broken_connections.go ├── client.go ├── enum.go ├── handlers_heartbeat.go ├── handlers_registry.go ├── handlers_transmission.go ├── http.go ├── http_test.go ├── message_expire.go ├── message_handler.go ├── message_queue.go ├── realm.go ├── realm_test.go ├── server.go ├── server_test.go ├── util.go └── wss.go ├── socket.go ├── socket_test.go └── util ├── util.go └── util_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | /tmp 4 | /data 5 | /build 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /tmp 3 | /data 4 | /build 5 | __debug_bin 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang as base 2 | ARG ARCH=amd64 3 | ARG ARM= 4 | ADD ./ /build 5 | WORKDIR /build 6 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} GOARM=${ARM} go build -o ./peer-server ./cmd/server/main.go 7 | 8 | FROM scratch 9 | COPY --from=base /build/peer-server /peer-server 10 | ENTRYPOINT [ "/peer-server" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | IMAGENAME ?= opny/peer-server 3 | BUILDPATH ?= ./build 4 | 5 | server/run: 6 | go run cmd/server/main.go 7 | 8 | kill-server: 9 | kill -9 $(lsof -t -i tcp:9000) 10 | 11 | peerjs/interop/js: 12 | cd interop/js && npm run serve 13 | 14 | peerjs/server/run: 15 | docker stop peerjs-server || true 16 | docker run --rm --name peerjs-server -p 9000:9000 -d peerjs/peerjs-server --port 9000 --path / 17 | docker logs -f peerjs-server 18 | 19 | docker/run: 20 | docker run --name peer-server -it --rm -p 9000:9000 $(IMAGENAME) 21 | 22 | build: build/amd64 build/arm64 build/arm 23 | 24 | build/amd64: 25 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BUILDPATH}/amd64 . 26 | 27 | build/arm64: 28 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ${BUILDPATH}/arm64 . 29 | 30 | build/arm: 31 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o ${BUILDPATH}/arm7 . 32 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o ${BUILDPATH}/arm6 . 33 | 34 | docker/build: build docker/build/amd64 docker/build/arm64 docker/build/arm 35 | 36 | docker/build/manifest: 37 | 38 | docker manifest push --purge ${IMAGENAME} || true 39 | 40 | docker manifest create \ 41 | ${IMAGENAME} \ 42 | --amend ${IMAGENAME}-amd64 \ 43 | --amend ${IMAGENAME}-arm64 \ 44 | --amend ${IMAGENAME}-arm6 \ 45 | --amend ${IMAGENAME}-arm7 46 | 47 | docker manifest annotate ${IMAGENAME} ${IMAGENAME}-amd64 --arch amd64 --os linux 48 | docker manifest annotate ${IMAGENAME} ${IMAGENAME}-arm64 --arch arm64 --os linux 49 | docker manifest annotate ${IMAGENAME} ${IMAGENAME}-arm6 --arch arm --variant v6 --os linux 50 | docker manifest annotate ${IMAGENAME} ${IMAGENAME}-arm7 --arch arm --variant v7 --os linux 51 | 52 | docker manifest push ${IMAGENAME} 53 | 54 | docker/build/amd64: 55 | docker build . -t ${IMAGENAME}-amd64 --build-arg ARCH=amd64 56 | 57 | docker/build/arm64: 58 | docker build . -t ${IMAGENAME}-arm64 --build-arg ARCH=arm64 59 | 60 | docker/build/arm: 61 | docker build . -t ${IMAGENAME}-arm6 --build-arg ARCH=arm --build-arg ARM=7 62 | docker build . -t ${IMAGENAME}-arm7 --build-arg ARCH=arm --build-arg ARM=6 63 | 64 | docker/push: docker/build docker/push/amd64 docker/push/arm64 docker/push/arm docker/build/manifest 65 | docker manifest push ${IMAGENAME} 66 | 67 | docker/push/amd64: 68 | docker push ${IMAGENAME}-amd64 69 | 70 | docker/push/arm64: 71 | docker push ${IMAGENAME}-arm64 72 | 73 | docker/push/arm: 74 | docker push ${IMAGENAME}-arm6 75 | docker push ${IMAGENAME}-arm7 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang PeerJS 2 | 3 | A Golang port of [PeerJS](https://github.com/peers/peerjs) 4 | 5 | ## Implementation 6 | 7 | - [x] Datachannel 8 | - [x] Mediachannel 9 | - [x] Test coverage > 80% 10 | - [x] Signalling server 11 | - [ ] Interoperability tests 12 | 13 | ## Docs 14 | 15 | [![Go Reference](https://pkg.go.dev/badge/github.com/muka/peerjs-go.svg)](https://pkg.go.dev/github.com/muka/peerjs-go) 16 | 17 | ⚠️ _Note_: While the Javascript [PeerJS documentation](https://peerjs.com/docs/) often applies to this library, there are differences, namely: 18 | 19 | - All methods and properties are in PascalCase. 20 | - Enum values are represented as seperate constants. 21 | - All peer event callback functions should take a generic interface{} parameter, and then cast the interface{} to the appropriate peerjs-go type. 22 | - Blocked peer event callback functions will block other peerjs-go events from firing. 23 | - Refer to the [go package docs](https://pkg.go.dev/github.com/muka/peerjs-go) whenever unsure. 24 | 25 | ### Unsupported features 26 | 27 | - Payload de/encoding based on [js-binarypack](https://github.com/peers/js-binarypack) is not supported. 28 | - Message chunking (should be already done in recent browser versions) 29 | 30 | ## Usage example 31 | 32 | See [\_examples folder](./_examples) 33 | 34 | ```golang 35 | 36 | package main 37 | 38 | import ( 39 | "log" 40 | "time" 41 | 42 | peer "github.com/muka/peerjs-go" 43 | ) 44 | 45 | func main() { 46 | peer1, _ := peer.NewPeer("peer1", peer.NewOptions()) 47 | defer peer1.Close() 48 | 49 | peer2, _ := peer.NewPeer("peer2", peer.NewOptions()) 50 | defer peer2.Close() 51 | 52 | peer2.On("connection", func(data interface{}) { 53 | conn2 := data.(*peer.DataConnection) 54 | conn2.On("data", func(data interface{}) { 55 | // Will print 'hi!' 56 | log.Printf("Received: %#v: %s\n", data, data) 57 | }) 58 | }) 59 | 60 | conn1, _ := peer1.Connect("peer2", nil) 61 | conn1.On("open", func(data interface{}) { 62 | for { 63 | conn1.Send([]byte("hi!"), false) 64 | <-time.After(time.Millisecond * 1000) 65 | } 66 | }) 67 | 68 | select {} 69 | } 70 | ``` 71 | 72 | ## Peer server 73 | 74 | This library includes a GO based peer server in the [/server folder](./server/) 75 | 76 | ### Documentation: 77 | 78 | [![Go Reference](https://pkg.go.dev/badge/github.com/muka/peerjs-go/server.svg)](https://pkg.go.dev/github.com/muka/peerjs-go/server) 79 | 80 | ### Example usage 81 | 82 | ```golang 83 | package main 84 | 85 | import ( 86 | "log" 87 | 88 | peerjsServer "github.com/muka/peerjs-go/server" 89 | ) 90 | 91 | func main() { 92 | serverOptions := peerjsServer.NewOptions() 93 | // These are the default values NewOptions() creates: 94 | serverOptions.Port = 9000 95 | serverOptions.Host = "0.0.0.0" 96 | serverOptions.LogLevel = "info" 97 | serverOptions.ExpireTimeout = 5000 98 | serverOptions.AliveTimeout = 60000 99 | serverOptions.Key = "peerjs" 100 | serverOptions.Path = "/" 101 | serverOptions.ConcurrentLimit = 5000 102 | serverOptions.AllowDiscovery = false 103 | serverOptions.CleanupOutMsgs = 1000 104 | 105 | server := peerjsServer.New(serverOptions) 106 | defer server.Stop() 107 | 108 | if err := server.Start(); err != nil { 109 | log.Printf("Error starting peerjs server: %s", err) 110 | } 111 | 112 | select{} 113 | } 114 | ``` 115 | 116 | ### Docker 117 | 118 | A docker image for the GO based peer server is available at [opny/peer-server](https://hub.docker.com/r/opny/peer-server) built for Raspberry Pi and PCs 119 | 120 | ### Standalone 121 | 122 | To build a standalone GO based Peerjs server executable, run `go build ./cmd/server/main.go` in the repository folder. To set the server options, create a `peer.yaml` configuration file in the same folder as the executable with the following options: 123 | 124 | **Available Server Options:** 125 | 126 | - **Host** String 127 | - **Port** Int 128 | - **LogLevel** String 129 | - **ExpireTimeout** Int64 130 | - **AliveTimeout** Int64 131 | - **Key** String 132 | - **Path** String 133 | - **ConcurrentLimit** Int 134 | - **AllowDiscovery** Bool 135 | - **CleanupOutMsgs** Int 136 | -------------------------------------------------------------------------------- /_examples/interop/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/muka/peerjs-go" 8 | ) 9 | 10 | func fail(err error) { 11 | if err != nil { 12 | panic(err) 13 | } 14 | } 15 | 16 | func main() { 17 | 18 | // ensure to run your own peerjs-server 19 | // docker run --rm --name peerjs-server -p 9000:9000 -d peerjs/peerjs-server 20 | 21 | opts := peer.NewOptions() 22 | opts.Debug = 3 23 | opts.Path = "/myapp" 24 | opts.Host = "127.0.0.1" 25 | opts.Port = 9000 26 | opts.Secure = false 27 | 28 | peer1, err := peer.NewPeer("peer1", opts) 29 | fail(err) 30 | defer peer1.Close() 31 | 32 | peer1.On("connection", func(data interface{}) { 33 | conn1 := data.(*peer.DataConnection) 34 | conn1.On("data", func(data interface{}) { 35 | // Will print 'hi!' 36 | log.Printf("Received: %v\n", data) 37 | }) 38 | }) 39 | 40 | connOpts := peer.NewConnectionOptions() 41 | connOpts.Serialization = peer.SerializationTypeNone 42 | conn1, err := peer1.Connect("peerjs", connOpts) 43 | fail(err) 44 | conn1.On("open", func(data interface{}) { 45 | for { 46 | conn1.Send([]byte("hi!"), false) 47 | <-time.After(time.Millisecond * 1000) 48 | } 49 | }) 50 | 51 | select {} 52 | 53 | } 54 | -------------------------------------------------------------------------------- /_examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/muka/peerjs-go" 8 | ) 9 | 10 | func fail(err error) { 11 | if err != nil { 12 | panic(err) 13 | } 14 | } 15 | 16 | func main() { 17 | 18 | // ensure to run your own peerjs-server 19 | // docker run --rm --name peerjs-server -p 9000:9000 -d peerjs/peerjs-server 20 | 21 | opts := peer.NewOptions() 22 | opts.Debug = 3 23 | opts.Path = "/myapp" 24 | opts.Host = "127.0.0.1" 25 | opts.Port = 9000 26 | opts.Secure = false 27 | 28 | peer1, err := peer.NewPeer("peer1", opts) 29 | fail(err) 30 | defer peer1.Close() 31 | 32 | peer2, err := peer.NewPeer("peer2", opts) 33 | fail(err) 34 | defer peer2.Close() 35 | 36 | peer2.On("connection", func(data interface{}) { 37 | conn2 := data.(*peer.DataConnection) 38 | conn2.On("data", func(data interface{}) { 39 | // Will print 'hi!' 40 | log.Printf("Received: %v\n", data) 41 | }) 42 | }) 43 | 44 | conn1, err := peer1.Connect("peer2", nil) 45 | fail(err) 46 | conn1.On("open", func(data interface{}) { 47 | for { 48 | conn1.Send([]byte("hi!"), false) 49 | <-time.After(time.Millisecond * 1000) 50 | } 51 | }) 52 | 53 | select {} 54 | 55 | } 56 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "math/rand" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | //NewAPI initiate a new API client 14 | func NewAPI(opts Options) API { 15 | return API{ 16 | opts: opts, 17 | log: createLogger("api", opts.Debug), 18 | } 19 | } 20 | 21 | //API wrap calls to API server 22 | type API struct { 23 | opts Options 24 | log *logrus.Entry 25 | } 26 | 27 | func (a *API) buildURL(method string) string { 28 | proto := "http" 29 | if a.opts.Secure { 30 | proto = "https" 31 | } 32 | 33 | path := a.opts.Path 34 | if path == "/" { 35 | path = "" 36 | } 37 | 38 | return fmt.Sprintf( 39 | "%s://%s:%d%s/%s/%s?ts=%d%d", 40 | proto, 41 | a.opts.Host, 42 | a.opts.Port, 43 | path, 44 | a.opts.Key, 45 | method, 46 | time.Now().UnixNano(), 47 | rand.Int(), 48 | ) 49 | } 50 | 51 | func (a *API) req(method string) ([]byte, error) { 52 | uri := a.buildURL(method) 53 | resp, err := http.Get(uri) 54 | if err != nil { 55 | return []byte{}, err 56 | } 57 | if resp.StatusCode >= 400 { 58 | return []byte{}, fmt.Errorf("Request %s failed: %s", uri, resp.Status) 59 | } 60 | defer resp.Body.Close() 61 | body, err := ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | return []byte{}, err 64 | } 65 | 66 | return body, nil 67 | } 68 | 69 | //RetrieveID retrieve a ID 70 | func (a *API) RetrieveID() ([]byte, error) { 71 | return a.req("id") 72 | } 73 | 74 | //ListAllPeers return the list of available peers 75 | func (a *API) ListAllPeers() ([]byte, error) { 76 | return a.req("peers") 77 | } 78 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/muka/peerjs-go/server" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | func fail(err error, msg string) { 14 | if err == nil { 15 | return 16 | } 17 | if err != nil { 18 | fmt.Printf("%s: %s \n", msg, err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | func main() { 24 | 25 | viper.AutomaticEnv() 26 | viper.AutomaticEnv() 27 | viper.SetEnvPrefix("peer") 28 | viper.SetConfigName("peer") 29 | viper.SetConfigType("yaml") 30 | viper.AddConfigPath(".") 31 | 32 | if err := viper.ReadInConfig(); err != nil { 33 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 34 | // Config file not found; ignore error if desired 35 | } else { 36 | fail(err, "Failed to read config file") 37 | } 38 | } 39 | 40 | opts := server.NewOptions() 41 | if viper.IsSet("Host") { 42 | opts.Host = viper.GetString("Host") 43 | } 44 | if viper.IsSet("Port") { 45 | opts.Port = viper.GetInt("Port") 46 | } 47 | if viper.IsSet("LogLevel") { 48 | opts.LogLevel = viper.GetString("LogLevel") 49 | } 50 | if viper.IsSet("ExpireTimeout") { 51 | opts.ExpireTimeout = viper.GetInt64("ExpireTimeout") 52 | } 53 | if viper.IsSet("AliveTimeout") { 54 | opts.AliveTimeout = viper.GetInt64("AliveTimeout") 55 | } 56 | if viper.IsSet("Key") { 57 | opts.Key = viper.GetString("Key") 58 | } 59 | if viper.IsSet("Path") { 60 | opts.Path = viper.GetString("Path") 61 | } 62 | if viper.IsSet("ConcurrentLimit") { 63 | opts.ConcurrentLimit = viper.GetInt("ConcurrentLimit") 64 | } 65 | if viper.IsSet("AllowDiscovery") { 66 | opts.AllowDiscovery = viper.GetBool("AllowDiscovery") 67 | } 68 | if viper.IsSet("CleanupOutMsgs") { 69 | opts.CleanupOutMsgs = viper.GetInt("CleanupOutMsgs") 70 | } 71 | 72 | s := server.New(opts) 73 | defer s.Stop() 74 | err := s.Start() 75 | fail(err, "Failed to start server") 76 | 77 | quitChannel := make(chan os.Signal, 1) 78 | signal.Notify(quitChannel, syscall.SIGINT, syscall.SIGTERM) 79 | <-quitChannel 80 | } 81 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/muka/peerjs-go/emitter" 5 | "github.com/muka/peerjs-go/models" 6 | "github.com/pion/webrtc/v3" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | //Connection shared interface 11 | type Connection interface { 12 | GetType() string 13 | GetID() string 14 | GetPeerID() string 15 | GetProvider() *Peer 16 | GetMetadata() interface{} 17 | GetPeerConnection() *webrtc.PeerConnection 18 | SetPeerConnection(pc *webrtc.PeerConnection) 19 | Close() error 20 | HandleMessage(*models.Message) error 21 | Emit(string, interface{}) 22 | GetOptions() ConnectionOptions 23 | } 24 | 25 | func newBaseConnection(connType string, peer *Peer, opts ConnectionOptions) BaseConnection { 26 | return BaseConnection{ 27 | Emitter: emitter.NewEmitter(), 28 | Type: connType, 29 | Provider: peer, 30 | log: createLogger(connType, opts.Debug), 31 | opts: opts, 32 | negotiator: nil, 33 | } 34 | } 35 | 36 | // BaseConnection shared base connection 37 | type BaseConnection struct { 38 | emitter.Emitter 39 | // id connection ID 40 | id string 41 | // peerID peer ID of the connection 42 | peerID string 43 | //Provider is the peer instance 44 | Provider *Peer 45 | // DataChannel A reference to the RTCDataChannel object associated with the connection. 46 | DataChannel *webrtc.DataChannel 47 | // The optional label passed in or assigned by PeerJS when the connection was initiated. 48 | Label string 49 | // Metadata Any type of metadata associated with the connection, passed in by whoever initiated the connection. 50 | Metadata interface{} 51 | // Open This is true if the connection is open and ready for read/write. 52 | Open bool 53 | // PeerConnection A reference to the RTCPeerConnection object associated with the connection. 54 | PeerConnection *webrtc.PeerConnection 55 | // Reliable Whether the underlying data channels are reliable; defined when the connection was initiated. 56 | Reliable bool 57 | // Serialization The serialization format of the data sent over the connection. Can be binary (default), binary-utf8, json, or none. 58 | Serialization string 59 | // Type defines the type for connections 60 | Type string 61 | // BufferSize The number of messages queued to be sent once the browser buffer is no longer full. 62 | BufferSize int 63 | opts ConnectionOptions 64 | log *logrus.Entry 65 | negotiator *Negotiator 66 | } 67 | 68 | // GetOptions return the connection configuration 69 | func (c *BaseConnection) GetOptions() ConnectionOptions { 70 | return c.opts 71 | } 72 | 73 | // GetMetadata return the connection metadata 74 | func (c *BaseConnection) GetMetadata() interface{} { 75 | return c.Metadata 76 | } 77 | 78 | // GetPeerConnection return the underlying WebRTC PeerConnection 79 | func (c *BaseConnection) GetPeerConnection() *webrtc.PeerConnection { 80 | return c.PeerConnection 81 | } 82 | 83 | // SetPeerConnection set the underlying WebRTC PeerConnection 84 | func (c *BaseConnection) SetPeerConnection(pc *webrtc.PeerConnection) { 85 | c.PeerConnection = pc 86 | c.log.Debugf("%v", c.PeerConnection) 87 | } 88 | 89 | // GetID return the connection ID 90 | func (c *BaseConnection) GetID() string { 91 | return c.id 92 | } 93 | 94 | // GetPeerID return the connection peer ID 95 | func (c *BaseConnection) GetPeerID() string { 96 | return c.peerID 97 | } 98 | 99 | // Close closes the data connection 100 | func (c *BaseConnection) Close() error { 101 | panic("Not implemented!") 102 | } 103 | 104 | // HandleMessage handles incoming messages 105 | func (c *BaseConnection) HandleMessage(msg *models.Message) error { 106 | panic("Not implemented!") 107 | } 108 | 109 | // GetType return the connection type 110 | func (c *BaseConnection) GetType() string { 111 | return c.Type 112 | } 113 | 114 | // GetProvider return the peer provider 115 | func (c *BaseConnection) GetProvider() *Peer { 116 | return c.Provider 117 | } 118 | -------------------------------------------------------------------------------- /conn_data.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | "github.com/muka/peerjs-go/enums" 8 | "github.com/muka/peerjs-go/models" 9 | "github.com/muka/peerjs-go/util" 10 | "github.com/pion/webrtc/v3" 11 | ) 12 | 13 | const ( 14 | //DataChannelIDPrefix used as prefix for random ID 15 | DataChannelIDPrefix = "dc_" 16 | //MaxBufferedAmount max amount to buffer 17 | MaxBufferedAmount = 8 * 1024 * 1024 18 | ) 19 | 20 | // NewDataConnection create new DataConnection 21 | func NewDataConnection(peerID string, peer *Peer, opts ConnectionOptions) (*DataConnection, error) { 22 | 23 | d := &DataConnection{ 24 | BaseConnection: newBaseConnection(enums.ConnectionTypeData, peer, opts), 25 | buffer: bytes.NewBuffer([]byte{}), 26 | // encodingQueue: NewEncodingQueue(), 27 | } 28 | 29 | d.peerID = peerID 30 | 31 | d.id = opts.ConnectionID 32 | if d.id == "" { 33 | d.id = DataChannelIDPrefix + util.RandomToken() 34 | } 35 | 36 | d.Label = opts.Label 37 | if d.Label == "" { 38 | d.Label = d.id 39 | } 40 | 41 | d.Serialization = opts.Serialization 42 | if d.Serialization == "" { 43 | d.Serialization = enums.SerializationTypeRaw 44 | } 45 | 46 | d.Reliable = opts.Reliable 47 | 48 | // d.encodingQueue.On("done", d.onQueueDone) 49 | // d.encodingQueue.On("error", d.onQueueErr) 50 | 51 | d.negotiator = NewNegotiator(d, opts) 52 | err := d.negotiator.StartConnection(opts) 53 | 54 | return d, err 55 | } 56 | 57 | type chunkedData struct { 58 | Data []byte 59 | Count int 60 | Total int 61 | } 62 | 63 | // DataConnection track a connection with a remote Peer 64 | type DataConnection struct { 65 | BaseConnection 66 | buffer *bytes.Buffer 67 | bufferSize int 68 | buffering bool 69 | // chunkedData map[int]chunkedData 70 | // encodingQueue *EncodingQueue 71 | } 72 | 73 | // parse: (data: string) => any = JSON.parse; 74 | 75 | // func (d *DataConnection) onQueueDone(data interface{}) { 76 | // buf := data.([]byte) 77 | // d.bufferedSend(buf) 78 | // } 79 | 80 | // func (d *DataConnection) onQueueErr(data interface{}) { 81 | // err := data.(error) 82 | // d.log.Errorf(`DC#%s: Error occured in encoding from blob to arraybuffer, close DC: %s`, d.GetID(), err) 83 | // d.Close() 84 | // } 85 | 86 | //Initialize called by the Negotiator when the DataChannel is ready 87 | func (d *DataConnection) Initialize(dc *webrtc.DataChannel) { 88 | d.DataChannel = dc 89 | d.configureDataChannel() 90 | } 91 | 92 | func (d *DataConnection) configureDataChannel() { 93 | // TODO 94 | // d.DataChannel.binaryType = "arraybuffer"; 95 | 96 | d.DataChannel.OnOpen(func() { 97 | //TODO 98 | d.log.Debugf(`DC#%s dc connection success`, d.GetID()) 99 | d.Open = true 100 | d.Emit(enums.ConnectionEventTypeOpen, nil) 101 | }) 102 | 103 | d.DataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 104 | d.log.Debugf(`DC#%s dc onmessage: %v`, d.GetID(), msg.Data) 105 | d.handleDataMessage(msg) 106 | }) 107 | 108 | d.DataChannel.OnClose(func() { 109 | d.log.Debugf(`DC#%s dc closed for %s`, d.GetID(), d.peerID) 110 | d.Close() 111 | }) 112 | 113 | } 114 | 115 | // Handles a DataChannel message. 116 | func (d *DataConnection) handleDataMessage(msg webrtc.DataChannelMessage) { 117 | 118 | // isBinarySerialization := d.Serialization == SerializationTypeBinary || 119 | // d.Serialization == SerializationTypeBinaryUTF8 120 | 121 | if msg.IsString { 122 | d.Emit(enums.ConnectionEventTypeData, string(msg.Data)) 123 | } else { 124 | d.Emit(enums.ConnectionEventTypeData, msg.Data) 125 | } 126 | 127 | // if (isBinarySerialization) { 128 | // if (datatype == Blob) { 129 | // // Datatype should never be blob 130 | // util.blobToArrayBuffer(data as Blob, (ab) => { 131 | // const unpackedData = util.unpack(ab); 132 | // d.emit(ConnectionEventType.Data, unpackedData); 133 | // }); 134 | // return; 135 | // } else if (datatype === ArrayBuffer) { 136 | // deserializedData = util.unpack(data as ArrayBuffer); 137 | // } else if (datatype === String) { 138 | // // String fallback for binary data for browsers that don't support binary yet 139 | // const ab = util.binaryStringToArrayBuffer(data as string); 140 | // deserializedData = util.unpack(ab); 141 | // } 142 | // } else if (d.serialization === SerializationType.JSON) { 143 | // deserializedData = d.parse(data as string); 144 | // } 145 | // // Check if we've chunked--if so, piece things back together. 146 | // // We're guaranteed that this isn't 0. 147 | // if deserializedData.__peerData { 148 | // d.handleChunk(deserializedData) 149 | // return 150 | // } 151 | // d.Emit(ConnectionEventTypeData, deserializedData) 152 | } 153 | 154 | // func (d *DataConnection) handleChunk(raw []byte) { 155 | // // const id = data.__peerData; 156 | // // const chunkInfo = d._chunkedData[id] || { 157 | // // data: [], 158 | // // count: 0, 159 | // // total: data.total 160 | // // }; 161 | 162 | // // chunkInfo.data[data.n] = data.data; 163 | // // chunkInfo.count++; 164 | // // d._chunkedData[id] = chunkInfo; 165 | 166 | // // if (chunkInfo.total === chunkInfo.count) { 167 | // // // Clean up before making the recursive call to `_handleDataMessage`. 168 | // // delete d._chunkedData[id]; 169 | 170 | // // // We've received all the chunks--time to construct the complete data. 171 | // // const data = new Blob(chunkInfo.data); 172 | // // d._handleDataMessage({ data }); 173 | // // } 174 | // } 175 | 176 | /** 177 | * Exposed functionality for users. 178 | */ 179 | 180 | //Close allows user to close connection 181 | func (d *DataConnection) Close() error { 182 | 183 | d.buffer = nil 184 | d.bufferSize = 0 185 | // d.chunkedData = map[int]chunkedData{} 186 | 187 | if d.negotiator != nil { 188 | d.negotiator.Cleanup() 189 | d.negotiator = nil 190 | } 191 | 192 | if d.Provider != nil { 193 | d.Provider.RemoveConnection(d) 194 | d.Provider = nil 195 | } 196 | 197 | if d.DataChannel != nil { 198 | d.DataChannel.OnOpen(func() {}) 199 | d.DataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {}) 200 | d.DataChannel.OnClose(func() {}) 201 | d.DataChannel = nil 202 | } 203 | 204 | // if d.encodingQueue != nil { 205 | // d.encodingQueue.Destroy() 206 | // d.encodingQueue = nil 207 | // } 208 | 209 | if !d.Open { 210 | return nil 211 | } 212 | 213 | d.Open = false 214 | 215 | d.Emit(enums.ConnectionEventTypeClose, nil) 216 | return nil 217 | } 218 | 219 | // Send allows user to send data. 220 | func (d *DataConnection) Send(data []byte, chunked bool) error { 221 | if !d.Open { 222 | err := errors.New("Connection is not open. You should listen for the `open` event before sending messages") 223 | d.Emit( 224 | enums.ConnectionEventTypeError, 225 | err, 226 | ) 227 | return err 228 | } 229 | 230 | err := d.DataChannel.Send(data) 231 | if err != nil { 232 | d.log.Warnf("Send failed: %s", err) 233 | return err 234 | } 235 | 236 | return nil 237 | 238 | // if d.Serialization == SerializationTypeJSON { 239 | // // JSON data must be marshalled before send! 240 | // d.log.Debug("Send JSON") 241 | // d.bufferedSend(raw) 242 | // } else if d.Serialization == SerializationTypeBinary || d.Serialization == SerializationTypeBinaryUTF8 { 243 | 244 | // panic(errors.New("binarypack encoding is not supported")) 245 | 246 | // // NOTE we pack with MessagePack not with binarypack. Understant if this is good enough 247 | // // blob, err := msgpack.Marshal(data) 248 | // // if err != nil { 249 | // // return fmt.Errorf("Failed to pack message: %s", err) 250 | // // } 251 | 252 | // // if !chunked && len(blob) > ChunkedMTU { 253 | // // d.log.Debug("Chunk payload") 254 | // // d.sendChunks(blob) 255 | // // return nil 256 | // // } 257 | 258 | // // d.log.Debugf("Send encoded payload %v", raw) 259 | // // d.bufferedSend(blob) 260 | 261 | // } else { 262 | // d.log.Debug("Send raw payload") 263 | // d.bufferedSend(raw) 264 | // } 265 | 266 | } 267 | 268 | // func (d *DataConnection) bufferedSend(msg []byte) { 269 | // if d.buffering || !d.trySend(msg) { 270 | // d.buffer.Write(msg) 271 | // d.bufferSize = d.buffer.Len() 272 | // } 273 | // } 274 | 275 | // // Returns true if the send succeeds. 276 | // func (d *DataConnection) trySend(msg []byte) bool { 277 | // if !d.Open { 278 | // return false 279 | // } 280 | 281 | // if d.DataChannel.BufferedAmount() > MaxBufferedAmount { 282 | // d.buffering = true 283 | // <-time.After(time.Millisecond * 50) 284 | // d.buffering = false 285 | // d.tryBuffer() 286 | // return false 287 | // } 288 | 289 | // err := d.DataChannel.Send(msg) 290 | // if err != nil { 291 | // d.log.Errorf(`DC#%s Error sending %s`, d.GetID(), err) 292 | // d.buffering = true 293 | // // d.Close() 294 | // return false 295 | // } 296 | 297 | // return true 298 | // } 299 | 300 | // Try to send the first message in the buffer. 301 | // func (d *DataConnection) tryBuffer() { 302 | // if !d.Open { 303 | // return 304 | // } 305 | 306 | // if d.buffer.Len() == 0 { 307 | // return 308 | // } 309 | 310 | // // TODO here buffer is a slice not a continuous array 311 | // // check or reimplement this part! 312 | // msg := d.buffer.Bytes() 313 | // if d.trySend(msg) { 314 | // d.buffer.Reset() 315 | // d.bufferSize = d.buffer.Len() 316 | // d.tryBuffer() 317 | // } 318 | // } 319 | 320 | // func (d *DataConnection) sendChunks(raw []byte) { 321 | // panic("sendChunks: binarypack not implemented, please use SerializationTypeRaw") 322 | // // // this method requires a [binarypack] encoding to work 323 | // // chunks := util.Chunk(raw) 324 | // // d.log.Debugf(`DC#%s Try to send %d chunks...`, d.GetID(), len(chunks)) 325 | // // for _, chunk := range chunks { 326 | // // d.Send(chunk, true) 327 | // // } 328 | // } 329 | 330 | // HandleMessage handles incoming messages 331 | func (d *DataConnection) HandleMessage(message *models.Message) error { 332 | payload := message.Payload 333 | 334 | switch message.Type { 335 | case enums.ServerMessageTypeAnswer: 336 | d.negotiator.handleSDP(message.Type, *payload.SDP) 337 | break 338 | case enums.ServerMessageTypeCandidate: 339 | err := d.negotiator.HandleCandidate(payload.Candidate) 340 | if err != nil { 341 | d.log.Errorf("Failed to handle candidate for peer=%s: %s", d.peerID, err) 342 | } 343 | break 344 | default: 345 | d.log.Warnf( 346 | "Unrecognized message type: %s from peer: %s", 347 | message.Type, 348 | d.peerID, 349 | ) 350 | break 351 | } 352 | 353 | return nil 354 | } 355 | -------------------------------------------------------------------------------- /conn_media.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/muka/peerjs-go/enums" 7 | "github.com/muka/peerjs-go/models" 8 | "github.com/muka/peerjs-go/util" 9 | "github.com/pion/webrtc/v3" 10 | ) 11 | 12 | //MediaChannelIDPrefix the media channel connection id prefix 13 | const MediaChannelIDPrefix = "mc_" 14 | 15 | //NewMediaConnection create new MediaConnection 16 | func NewMediaConnection(id string, peer *Peer, opts ConnectionOptions) (*MediaConnection, error) { 17 | 18 | m := &MediaConnection{ 19 | BaseConnection: newBaseConnection(enums.ConnectionTypeMedia, peer, opts), 20 | } 21 | 22 | m.peerID = id 23 | 24 | m.id = opts.ConnectionID 25 | if m.id == "" { 26 | m.id = fmt.Sprintf("%s%s", MediaChannelIDPrefix, util.RandomToken()) 27 | } 28 | 29 | m.localStream = opts.Stream 30 | 31 | m.negotiator = NewNegotiator(m, opts) 32 | var err error 33 | if m.localStream != nil { 34 | opts.Originator = true 35 | err = m.negotiator.StartConnection(opts) 36 | } 37 | 38 | return m, err 39 | } 40 | 41 | // MediaConnection track a connection with a remote Peer 42 | type MediaConnection struct { 43 | BaseConnection 44 | Open bool 45 | remoteStream *MediaStream 46 | localStream *MediaStream 47 | } 48 | 49 | // GetLocalStream returns the local stream 50 | func (m *MediaConnection) GetLocalStream() *MediaStream { 51 | return m.localStream 52 | } 53 | 54 | // GetRemoteStream returns the remote stream 55 | func (m *MediaConnection) GetRemoteStream() *MediaStream { 56 | return m.remoteStream 57 | } 58 | 59 | // AddStream adds a stream to the MediaConnection 60 | func (m *MediaConnection) AddStream(tr *webrtc.TrackRemote) { 61 | m.log.Debugf("Receiving stream: %v", tr) 62 | m.remoteStream = NewMediaStreamWithTrack([]MediaStreamTrack{tr}) 63 | m.Emit(enums.ConnectionEventTypeStream, tr) 64 | } 65 | 66 | func (m *MediaConnection) HandleMessage(message *models.Message) error { 67 | mtype := message.GetType() 68 | payload := message.GetPayload() 69 | switch message.GetType() { 70 | case enums.ServerMessageTypeAnswer: 71 | // Forward to negotiator 72 | m.negotiator.handleSDP(message.GetType(), *payload.SDP) 73 | m.Open = true 74 | break 75 | case enums.ServerMessageTypeCandidate: 76 | m.negotiator.HandleCandidate(payload.Candidate) 77 | break 78 | default: 79 | m.log.Warnf("Unrecognized message type:%s from peer:%s", mtype, m.peerID) 80 | break 81 | } 82 | return nil 83 | } 84 | 85 | //Answer open the media connection with the remote peer 86 | func (m *MediaConnection) Answer(tl webrtc.TrackLocal, options *AnswerOption) { 87 | 88 | if m.localStream != nil { 89 | m.log.Warn("Local stream already exists on this MediaConnection. Are you answering a call twice?") 90 | return 91 | } 92 | 93 | stream := NewMediaStreamWithTrack([]MediaStreamTrack{tl}) 94 | m.localStream = stream 95 | 96 | if options != nil && options.SDPTransform != nil { 97 | m.BaseConnection.opts.SDPTransform = options.SDPTransform 98 | } 99 | 100 | connOpts := m.GetOptions() 101 | connOpts.Stream = stream 102 | m.negotiator.StartConnection(connOpts) 103 | // Retrieve lost messages stored because PeerConnection not set up. 104 | messages := m.GetProvider().GetMessages(m.GetID()) 105 | 106 | for _, message := range messages { 107 | m.HandleMessage(&message) 108 | } 109 | 110 | m.Open = true 111 | } 112 | 113 | //Close allows user to close connection 114 | func (m *MediaConnection) Close() error { 115 | if m.negotiator != nil { 116 | m.negotiator.Cleanup() 117 | m.negotiator = nil 118 | } 119 | 120 | m.localStream = nil 121 | m.remoteStream = nil 122 | 123 | if m.GetProvider() != nil { 124 | m.GetProvider().RemoveConnection(m) 125 | m.BaseConnection.Provider = nil 126 | } 127 | 128 | if m.BaseConnection.opts.Stream != nil { 129 | m.BaseConnection.opts.Stream = nil 130 | } 131 | 132 | if !m.Open { 133 | return nil 134 | } 135 | 136 | m.Open = false 137 | 138 | m.Emit(enums.ConnectionEventTypeClose, nil) 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /emitter/emitter.go: -------------------------------------------------------------------------------- 1 | package emitter 2 | 3 | import ( 4 | "github.com/chuckpreslar/emission" 5 | ) 6 | 7 | //EventHandler wrap an event callback 8 | type EventHandler func(interface{}) 9 | 10 | // NewEmitter initializes an Emitter 11 | func NewEmitter() Emitter { 12 | return Emitter{ 13 | emitter: emission.NewEmitter(), 14 | } 15 | } 16 | 17 | // Emitter exposes an EventEmitter-like interface 18 | type Emitter struct { 19 | emitter *emission.Emitter 20 | } 21 | 22 | //Emit emits an event with contextual data 23 | func (p *Emitter) Emit(event string, data interface{}) { 24 | // log.Printf("EMIT %s %++v", event, data) 25 | p.emitter.Emit(event, data) 26 | } 27 | 28 | //On register a function. Note that the pointer to the function need to be 29 | //the same to be removed with Off 30 | func (p *Emitter) On(event string, handler EventHandler) { 31 | p.emitter.On(event, handler) 32 | } 33 | 34 | //Off remove a listener function, pointer of the function passed must match with the one 35 | func (p *Emitter) Off(event string, handler EventHandler) { 36 | p.emitter.Off(event, handler) 37 | } 38 | -------------------------------------------------------------------------------- /enums/enums.go: -------------------------------------------------------------------------------- 1 | package enums 2 | 3 | const ( 4 | 5 | //ConnectionEventTypeOpen enum for connection open 6 | ConnectionEventTypeOpen string = "open" 7 | //ConnectionEventTypeStream enum for connection stream 8 | ConnectionEventTypeStream = "stream" 9 | //ConnectionEventTypeData enum for connection data 10 | ConnectionEventTypeData = "data" 11 | //ConnectionEventTypeClose enum for connection close 12 | ConnectionEventTypeClose = "close" 13 | //ConnectionEventTypeError enum for connection error 14 | ConnectionEventTypeError = "error" 15 | //ConnectionEventTypeIceStateChanged enum for ICE state changes 16 | ConnectionEventTypeIceStateChanged = "iceStateChanged" 17 | //ConnectionTypeData enum for data connection type 18 | ConnectionTypeData = "data" 19 | //ConnectionTypeMedia enum for media connection type 20 | ConnectionTypeMedia = "media" 21 | 22 | //PeerEventTypeOpen enum for peer open 23 | PeerEventTypeOpen = "open" 24 | //PeerEventTypeClose enum for peer close 25 | PeerEventTypeClose = "close" 26 | //PeerEventTypeConnection enum for peer connection 27 | PeerEventTypeConnection = "connection" 28 | //PeerEventTypeCall enum for peer call 29 | PeerEventTypeCall = "call" 30 | //PeerEventTypeDisconnected enum for peer disconnected 31 | PeerEventTypeDisconnected = "disconnected" 32 | //PeerEventTypeError enum for peer error 33 | PeerEventTypeError = "error" 34 | 35 | //PeerErrorTypeBrowserIncompatible enum for peer error browser-incompatible 36 | PeerErrorTypeBrowserIncompatible = "browser-incompatible" 37 | //PeerErrorTypeDisconnected enum for peer error disconnected 38 | PeerErrorTypeDisconnected = "disconnected" 39 | //PeerErrorTypeInvalidID enum for peer error invalid-id 40 | PeerErrorTypeInvalidID = "invalid-id" 41 | //PeerErrorTypeInvalidKey enum for peer error invalid-key 42 | PeerErrorTypeInvalidKey = "invalid-key" 43 | //PeerErrorTypeNetwork enum for peer error network 44 | PeerErrorTypeNetwork = "network" 45 | //PeerErrorTypePeerUnavailable enum for peer error peer-unavailable 46 | PeerErrorTypePeerUnavailable = "peer-unavailable" 47 | //PeerErrorTypeSslUnavailable enum for peer error ssl-unavailable 48 | PeerErrorTypeSslUnavailable = "ssl-unavailable" 49 | //PeerErrorTypeServerError enum for peer error server-error 50 | PeerErrorTypeServerError = "server-error" 51 | //PeerErrorTypeSocketError enum for peer error socket-error 52 | PeerErrorTypeSocketError = "socket-error" 53 | //PeerErrorTypeSocketClosed enum for peer error socket-closed 54 | PeerErrorTypeSocketClosed = "socket-closed" 55 | //PeerErrorTypeUnavailableID enum for peer error unavailable-id 56 | PeerErrorTypeUnavailableID = "unavailable-id" 57 | //PeerErrorTypeWebRTC enum for peer error webrtc 58 | PeerErrorTypeWebRTC = "webrtc" 59 | 60 | //SerializationTypeBinary enum for binary serialization 61 | SerializationTypeBinary = "binary" 62 | //SerializationTypeBinaryUTF8 enum for UTF8 binary serialization 63 | SerializationTypeBinaryUTF8 = "binary-utf8" 64 | //SerializationTypeJSON enum for JSON serialization 65 | SerializationTypeJSON = "json" 66 | //SerializationTypeRaw Payload is sent as-is 67 | SerializationTypeRaw = "raw" 68 | 69 | //SocketEventTypeMessage enum for socket message 70 | SocketEventTypeMessage = "message" 71 | //SocketEventTypeDisconnected enum for socket disconnected 72 | SocketEventTypeDisconnected = "disconnected" 73 | //SocketEventTypeError enum for socket error 74 | SocketEventTypeError = "error" 75 | //SocketEventTypeClose enum for socket close 76 | SocketEventTypeClose = "close" 77 | 78 | //ServerMessageTypeHeartbeat enum for server HEARTBEAT 79 | ServerMessageTypeHeartbeat = "HEARTBEAT" 80 | //ServerMessageTypeCandidate enum for server CANDIDATE 81 | ServerMessageTypeCandidate = "CANDIDATE" 82 | //ServerMessageTypeOffer enum for server OFFER 83 | ServerMessageTypeOffer = "OFFER" 84 | //ServerMessageTypeAnswer enum for server ANSWER 85 | ServerMessageTypeAnswer = "ANSWER" 86 | //ServerMessageTypeOpen enum for server OPEN 87 | ServerMessageTypeOpen = "OPEN" 88 | //ServerMessageTypeError enum for server ERROR 89 | ServerMessageTypeError = "ERROR" 90 | //ServerMessageTypeIDTaken enum for server 91 | ServerMessageTypeIDTaken = "ID-TAKEN" // The selected ID is taken. 92 | //ServerMessageTypeInvalidKey enum for INVALID-KEY 93 | ServerMessageTypeInvalidKey = "INVALID-KEY" 94 | //ServerMessageTypeLeave enum for server LEAVE 95 | ServerMessageTypeLeave = "LEAVE" 96 | //ServerMessageTypeExpire enum for server EXPIRE 97 | ServerMessageTypeExpire = "EXPIRE" 98 | ) 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/muka/peerjs-go 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 7 | github.com/gorilla/mux v1.8.0 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/pion/webrtc/v3 v3.1.47 10 | github.com/rs/cors v1.8.2 11 | github.com/sirupsen/logrus v1.9.0 12 | github.com/spf13/viper v1.13.0 13 | github.com/stretchr/testify v1.8.1 14 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /interop/js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /interop/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 41 | 42 | -------------------------------------------------------------------------------- /interop/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peerjs-interop", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "serve": "parcel ./", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "http-server": "^0.12.3", 14 | "parcel": "^1.12.4", 15 | "peerjs": "^1.3.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /interop/py/requirements.txt: -------------------------------------------------------------------------------- 1 | peerjs 2 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | func createLogger(source string, debugLevel int8) *logrus.Entry { 8 | 9 | log := logrus.New() 10 | 11 | // 0 Prints no logs. 12 | // 1 Prints only errors. 13 | // 2 Prints errors and warnings. 14 | // 3 Prints all logs. 15 | switch debugLevel { 16 | case 0: 17 | log.SetLevel(logrus.PanicLevel) 18 | break 19 | case 1: 20 | log.SetLevel(logrus.ErrorLevel) 21 | break 22 | case 2: 23 | log.SetLevel(logrus.WarnLevel) 24 | break 25 | default: 26 | log.SetLevel(logrus.DebugLevel) 27 | break 28 | } 29 | 30 | //TODO configure logger format 31 | // log.SetFormatter(&logrus.JSONFormatter{}) 32 | log.SetFormatter(&logrus.TextFormatter{}) 33 | 34 | // log to stderr by default 35 | // log.SetOutput(os.Stderr) 36 | // log.SetOutput(os.Stdout) 37 | 38 | return log.WithFields(logrus.Fields{ 39 | "module": "peer", 40 | "source": source, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /mediastream.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/pion/webrtc/v3" 5 | ) 6 | 7 | //MediaStreamTrack interaface that wraps together TrackLocal and TrackRemote 8 | type MediaStreamTrack interface { 9 | // ID is the unique identifier for this Track. This should be unique for the 10 | // stream, but doesn't have to globally unique. A common example would be 'audio' or 'video' 11 | // and StreamID would be 'desktop' or 'webcam' 12 | ID() string 13 | 14 | // StreamID is the group this track belongs too. This must be unique 15 | StreamID() string 16 | 17 | // Kind controls if this TrackLocal is audio or video 18 | Kind() webrtc.RTPCodecType 19 | } 20 | 21 | // NewMediaStreamWithTrack create a mediastream with tracks 22 | func NewMediaStreamWithTrack(tracks []MediaStreamTrack) *MediaStream { 23 | m := new(MediaStream) 24 | m.tracks = tracks 25 | return m 26 | } 27 | 28 | // MediaStream A stream of media content. A stream consists of several tracks 29 | // such as video or audio tracks. Each track is specified as an instance 30 | // of MediaStreamTrack. 31 | type MediaStream struct { 32 | tracks []MediaStreamTrack 33 | } 34 | 35 | // GetTracks returns a list of tracks 36 | func (m *MediaStream) GetTracks() []MediaStreamTrack { 37 | return m.tracks 38 | } 39 | 40 | // AddTrack add a track 41 | func (m *MediaStream) AddTrack(t MediaStreamTrack) { 42 | m.tracks = append(m.tracks, t) 43 | } 44 | 45 | // RemoveTrack remove a track 46 | func (m *MediaStream) RemoveTrack(t MediaStreamTrack) { 47 | tracks := []MediaStreamTrack{} 48 | for i, t1 := range m.tracks { 49 | if t1 == t { 50 | m.tracks[i] = nil 51 | continue 52 | } 53 | tracks = append(tracks, t1) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /models/message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/pion/webrtc/v3" 4 | 5 | // Payload wraps a message payload 6 | type Payload struct { 7 | Type string `json:"type"` 8 | ConnectionID string `json:"connectionId"` 9 | Metadata interface{} `json:"metadata,omitempty"` 10 | Label string `json:"label,omitempty"` 11 | Serialization string `json:"serialization,omitempty"` 12 | Reliable bool `json:"reliable,omitempty"` 13 | Candidate *webrtc.ICECandidateInit `json:"candidate,omitempty"` 14 | SDP *webrtc.SessionDescription `json:"sdp,omitempty"` 15 | Browser string `json:"browser,omitempty"` 16 | Msg string `json:"msg,omitempty"` 17 | } 18 | 19 | //IMessage message interface 20 | type IMessage interface { 21 | GetType() string 22 | GetSrc() string 23 | GetDst() string 24 | GetPayload() Payload 25 | } 26 | 27 | // Message the IMessage implementation 28 | type Message struct { 29 | Type string `json:"type"` 30 | Payload Payload `json:"payload"` 31 | Src string `json:"src"` 32 | Dst string `json:"dst,omitempty"` 33 | } 34 | 35 | // GetPayload returns the message payload 36 | func (m Message) GetPayload() Payload { 37 | return m.Payload 38 | } 39 | 40 | // GetSrc returns the message src 41 | func (m Message) GetSrc() string { 42 | return m.Src 43 | } 44 | 45 | // GetDst returns the message dst 46 | func (m Message) GetDst() string { 47 | return m.Dst 48 | } 49 | 50 | // GetType returns the message payload 51 | func (m Message) GetType() string { 52 | return m.Type 53 | } 54 | -------------------------------------------------------------------------------- /negotiator.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | 8 | "github.com/muka/peerjs-go/enums" 9 | "github.com/muka/peerjs-go/models" 10 | "github.com/pion/webrtc/v3" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // DefaultBrowser is the browser name 15 | const DefaultBrowser = "peerjs-go" 16 | 17 | func newWebrtcAPI(mediaEngine *webrtc.MediaEngine) *webrtc.API { 18 | if mediaEngine == nil { 19 | mediaEngine = new(webrtc.MediaEngine) 20 | mediaEngine.RegisterDefaultCodecs() 21 | } 22 | return webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine)) 23 | } 24 | 25 | // NewNegotiator initiate a new negotiator 26 | func NewNegotiator(conn Connection, opts ConnectionOptions) *Negotiator { 27 | return &Negotiator{ 28 | connection: conn, 29 | log: createLogger("negotiator", opts.Debug), 30 | webrtc: newWebrtcAPI(opts.MediaEngine), 31 | } 32 | } 33 | 34 | // Negotiator manages all negotiations between Peers 35 | type Negotiator struct { 36 | connection Connection 37 | log *logrus.Entry 38 | webrtc *webrtc.API 39 | } 40 | 41 | // StartConnection Returns a PeerConnection object set up correctly (for data, media). */ 42 | func (n *Negotiator) StartConnection(opts ConnectionOptions) error { 43 | 44 | connectionReadyForIce := false 45 | defer func() { 46 | connectionReadyForIce = true 47 | }() 48 | 49 | peerConnection, err := n.startPeerConnection(&connectionReadyForIce) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // Set the connection's PC. 55 | n.connection.SetPeerConnection(peerConnection) 56 | 57 | if n.connection.GetType() == enums.ConnectionTypeMedia && opts.Stream != nil { 58 | for _, track := range opts.Stream.GetTracks() { 59 | rtpSender, err := peerConnection.AddTrack(track.(webrtc.TrackLocal)) 60 | if err != nil { 61 | n.log.Warn("Error adding track to connection:", err) 62 | } else { 63 | go n.listenForRTCPPackets(rtpSender) 64 | } 65 | } 66 | } 67 | 68 | // What do we need to do now? 69 | if opts.Originator { 70 | if n.connection.GetType() == enums.ConnectionTypeData { 71 | 72 | dataConnection := n.connection.(*DataConnection) 73 | 74 | config := &webrtc.DataChannelInit{ 75 | Ordered: &opts.Reliable, 76 | } 77 | 78 | dataChannel, err := peerConnection.CreateDataChannel(dataConnection.Label, config) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | dataConnection.Initialize(dataChannel) 84 | } 85 | 86 | n.makeOffer() 87 | } else { 88 | // OFFER 89 | err = n.handleSDP(enums.ServerMessageTypeOffer, opts.SDP) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (n *Negotiator) listenForRTCPPackets(rtpSender *webrtc.RTPSender) { 99 | // Read incoming RTCP packets 100 | // Before these packets are returned they are processed by interceptors. 101 | // For things like NACK this needs to be called. 102 | rtcpBuf := make([]byte, 1500) 103 | for { 104 | if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { 105 | return 106 | } 107 | } 108 | } 109 | 110 | // Start a PC 111 | func (n *Negotiator) startPeerConnection(connectionReadyForIce *bool) (*webrtc.PeerConnection, error) { 112 | 113 | n.log.Debug("Creating RTCPeerConnection") 114 | 115 | // peerConnection = webrtc.PeerConnection(this.connection.provider.options.config); 116 | c := n.connection.GetProvider().GetOptions().Configuration 117 | peerConnection, err := n.webrtc.NewPeerConnection(c) 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | n.setupListeners(peerConnection, connectionReadyForIce) 123 | 124 | return peerConnection, nil 125 | } 126 | 127 | // Set up various WebRTC listeners 128 | func (n *Negotiator) setupListeners(peerConnection *webrtc.PeerConnection, connectionReadyForIce *bool) { 129 | 130 | peerID := n.connection.GetPeerID() 131 | connectionID := n.connection.GetID() 132 | provider := n.connection.GetProvider() 133 | 134 | n.log.Debug("Listening for ICE candidates.") 135 | peerConnection.OnICECandidate(func(evt *webrtc.ICECandidate) { 136 | 137 | for !*connectionReadyForIce { 138 | runtime.Gosched() 139 | } 140 | 141 | peerID := n.connection.GetPeerID() 142 | connectionID := n.connection.GetID() 143 | connectionType := n.connection.GetType() 144 | provider := n.connection.GetProvider() 145 | 146 | if evt == nil { 147 | n.log.Debugf("ICECandidate gathering completed for peer=%s conn=%s", peerID, connectionID) 148 | return 149 | } 150 | 151 | candidate := evt.ToJSON() 152 | 153 | if candidate.Candidate == "" { 154 | return 155 | } 156 | 157 | n.log.Debugf("Received ICE candidates for %s: %s", peerID, candidate.Candidate) 158 | 159 | msg := models.Message{ 160 | Type: enums.ServerMessageTypeCandidate, 161 | Payload: models.Payload{ 162 | Candidate: &candidate, 163 | Type: connectionType, 164 | ConnectionID: connectionID, 165 | }, 166 | Dst: peerID, 167 | } 168 | 169 | res, err := json.Marshal(msg) 170 | if err != nil { 171 | n.log.Errorf("OnICECandidate: Failed to serialize message: %s", err) 172 | } 173 | 174 | err = provider.GetSocket().Send(res) 175 | if err != nil { 176 | n.log.Errorf("OnICECandidate: Failed to send message: %s", err) 177 | } 178 | 179 | }) 180 | 181 | peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) { 182 | switch peerConnection.ICEConnectionState() { 183 | case webrtc.ICEConnectionStateFailed: 184 | n.log.Debugf("iceConnectionState is failed, closing connections to %s", peerID) 185 | n.connection.Emit( 186 | enums.ConnectionEventTypeError, 187 | fmt.Errorf("Negotiation of connection to %s failed", peerID), 188 | ) 189 | n.connection.Close() 190 | break 191 | case webrtc.ICEConnectionStateClosed: 192 | n.log.Debugf("iceConnectionState is closed, closing connections to %s", peerID) 193 | n.connection.Emit(enums.ConnectionEventTypeError, fmt.Errorf("Connection to %s closed", peerID)) 194 | n.connection.Close() 195 | break 196 | case webrtc.ICEConnectionStateDisconnected: 197 | n.log.Debugf("iceConnectionState changed to disconnected on the connection with %s", peerID) 198 | break 199 | case webrtc.ICEConnectionStateCompleted: 200 | peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { 201 | // noop 202 | }) 203 | break 204 | } 205 | 206 | n.connection.Emit(enums.ConnectionEventTypeIceStateChanged, peerConnection.ICEConnectionState()) 207 | }) 208 | 209 | // DATACONNECTION. 210 | n.log.Debug("Listening for data channel") 211 | 212 | // Fired between offer and answer, so options should already be saved in the options hash. 213 | peerConnection.OnDataChannel(func(dataChannel *webrtc.DataChannel) { 214 | n.log.Debug("Received data channel") 215 | conn, ok := provider.GetConnection(peerID, connectionID) 216 | if ok { 217 | connection := conn.(*DataConnection) 218 | connection.Initialize(dataChannel) 219 | } 220 | }) 221 | 222 | // MEDIACONNECTION 223 | n.log.Debug("Listening for remote stream") 224 | 225 | peerConnection.OnTrack(func(tr *webrtc.TrackRemote, r *webrtc.RTPReceiver) { 226 | n.log.Debug("Received remote stream") 227 | 228 | connection, ok := provider.GetConnection(peerID, connectionID) 229 | if ok { 230 | if connection.GetType() == enums.ConnectionTypeMedia { 231 | mediaConnection := connection.(*MediaConnection) 232 | n.log.Debugf("add stream %s to media connection %s", tr.ID(), mediaConnection.GetID()) 233 | mediaConnection.AddStream(tr) 234 | } 235 | } 236 | }) 237 | } 238 | 239 | // Cleanup clean up the negotiatior internal state 240 | func (n *Negotiator) Cleanup() { 241 | n.log.Debugf("Cleaning up PeerConnection to %s", n.connection.GetPeerID()) 242 | 243 | peerConnection := n.connection.GetPeerConnection() 244 | if peerConnection == nil { 245 | return 246 | } 247 | 248 | n.connection.SetPeerConnection(nil) 249 | 250 | //unsubscribe from all PeerConnection's events 251 | peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {}) 252 | peerConnection.OnICEConnectionStateChange(func(is webrtc.ICEConnectionState) {}) 253 | peerConnection.OnDataChannel(func(dc *webrtc.DataChannel) {}) 254 | peerConnection.OnTrack(func(tr *webrtc.TrackRemote, r *webrtc.RTPReceiver) {}) 255 | 256 | peerConnectionNotClosed := peerConnection.ConnectionState() != webrtc.PeerConnectionStateClosed 257 | dataChannelNotClosed := false 258 | 259 | if n.connection.GetType() == enums.ConnectionTypeData { 260 | dataConnection := n.connection.(*DataConnection) 261 | dataChannel := dataConnection.DataChannel 262 | 263 | if dataChannel != nil { 264 | dataChannelNotClosed = dataChannel.ReadyState() != webrtc.DataChannelStateClosed 265 | } 266 | } 267 | 268 | if peerConnectionNotClosed || dataChannelNotClosed { 269 | peerConnection.Close() 270 | } 271 | 272 | } 273 | 274 | func (n *Negotiator) makeOffer() error { 275 | 276 | peerConnection := n.connection.GetPeerConnection() 277 | provider := n.connection.GetProvider() 278 | 279 | // TODO check offer message 280 | offer, err := peerConnection.CreateOffer(&webrtc.OfferOptions{ 281 | OfferAnswerOptions: webrtc.OfferAnswerOptions{ 282 | // VoiceActivityDetection: true, 283 | }, 284 | ICERestart: false, 285 | }) 286 | if err != nil { 287 | err1 := fmt.Errorf("makeOffer: Failed to create offer: %s", err) 288 | n.log.Warn(err1) 289 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 290 | return err 291 | } 292 | n.log.Debug("Created offer") 293 | 294 | connOpts := n.connection.GetOptions() 295 | 296 | if connOpts.SDPTransform != nil { 297 | offer.SDP = connOpts.SDPTransform(offer.SDP) 298 | } 299 | 300 | err = peerConnection.SetLocalDescription(offer) 301 | if err != nil { 302 | err1 := fmt.Errorf("makeOffer: Failed to set local description: %s", err) 303 | n.log.Warn(err1) 304 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 305 | return err 306 | } 307 | 308 | n.log.Debugf("Set localDescription: %s for:%s", offer.SDP, n.connection.GetPeerID()) 309 | 310 | payload := models.Payload{ 311 | Type: n.connection.GetType(), 312 | ConnectionID: n.connection.GetID(), 313 | Metadata: n.connection.GetMetadata(), 314 | SDP: &offer, 315 | Browser: DefaultBrowser, 316 | } 317 | 318 | if n.connection.GetType() == enums.ConnectionTypeData { 319 | dataConnection := n.connection.(*DataConnection) 320 | payload.Label = dataConnection.Label 321 | payload.Reliable = dataConnection.Reliable 322 | payload.Serialization = dataConnection.Serialization 323 | } 324 | 325 | msg := models.Message{ 326 | Type: enums.ServerMessageTypeOffer, 327 | Dst: n.connection.GetPeerID(), 328 | Payload: payload, 329 | } 330 | 331 | raw, err := json.Marshal(msg) 332 | if err != nil { 333 | err1 := fmt.Errorf("makeOffer: Failed to marshal socket message: %s", err) 334 | n.log.Warn(err1) 335 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 336 | return err 337 | } 338 | 339 | err = provider.GetSocket().Send(raw) 340 | if err != nil { 341 | err1 := fmt.Errorf("makeOffer: Failed to send message: %s", err) 342 | n.log.Warn(err1) 343 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 344 | return err 345 | } 346 | 347 | return nil 348 | } 349 | 350 | func (n *Negotiator) makeAnswer() error { 351 | 352 | peerConnection := n.connection.GetPeerConnection() 353 | provider := n.connection.GetProvider() 354 | 355 | answer, err := peerConnection.CreateAnswer(&webrtc.AnswerOptions{ 356 | OfferAnswerOptions: webrtc.OfferAnswerOptions{ 357 | // VoiceActivityDetection: true, 358 | }, 359 | }) 360 | if err != nil { 361 | err1 := fmt.Errorf("makeAnswer: Failed to create answer: %s", err) 362 | n.log.Warn(err1) 363 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 364 | return err 365 | } 366 | 367 | n.log.Debug("Created answer.") 368 | 369 | connOpts := n.connection.GetOptions() 370 | if connOpts.SDPTransform != nil { 371 | answer.SDP = connOpts.SDPTransform(answer.SDP) 372 | } 373 | 374 | err = peerConnection.SetLocalDescription(answer) 375 | if err != nil { 376 | err1 := fmt.Errorf("makeAnswer: Failed to set local description: %s", err) 377 | n.log.Warn(err1) 378 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 379 | return err 380 | } 381 | 382 | n.log.Debugf(`Set localDescription: %s for %s`, answer.SDP, n.connection.GetPeerID()) 383 | 384 | msg := models.Message{ 385 | Type: enums.ServerMessageTypeAnswer, 386 | Dst: n.connection.GetPeerID(), 387 | Payload: models.Payload{ 388 | Type: n.connection.GetType(), 389 | ConnectionID: n.connection.GetID(), 390 | SDP: &answer, 391 | Browser: DefaultBrowser, 392 | }, 393 | } 394 | 395 | raw, err := json.Marshal(msg) 396 | if err != nil { 397 | err1 := fmt.Errorf("makeAnswer: Failed to marshal sockt message: %s", err) 398 | n.log.Warn(err1) 399 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 400 | return err 401 | } 402 | 403 | err = provider.GetSocket().Send(raw) 404 | if err != nil { 405 | err1 := fmt.Errorf("makeAnswer: Failed to send message: %s", err) 406 | n.log.Warn(err1) 407 | provider.EmitError(enums.PeerErrorTypeWebRTC, err1) 408 | return err 409 | } 410 | 411 | return nil 412 | } 413 | 414 | // Handle an SDP. 415 | func (n *Negotiator) handleSDP(sdpType string, sdp webrtc.SessionDescription) error { 416 | 417 | peerConnection := n.connection.GetPeerConnection() 418 | provider := n.connection.GetProvider() 419 | 420 | n.log.Debugf("Setting remote description %v", sdp) 421 | 422 | err := peerConnection.SetRemoteDescription(sdp) 423 | if err != nil { 424 | provider.EmitError(enums.PeerErrorTypeWebRTC, err) 425 | n.log.Warnf("handleSDP: Failed to setRemoteDescription %s", err) 426 | return err 427 | } 428 | 429 | n.log.Debugf(`Set remoteDescription:%s for:%s`, sdpType, n.connection.GetPeerID()) 430 | 431 | // sdpType == OFFER 432 | if sdpType == enums.ServerMessageTypeOffer { 433 | err := n.makeAnswer() 434 | if err != nil { 435 | return err 436 | } 437 | } 438 | 439 | return nil 440 | } 441 | 442 | // HandleCandidate handles a candidate 443 | func (n *Negotiator) HandleCandidate(iceInit *webrtc.ICECandidateInit) error { 444 | 445 | n.log.Debugf(`HandleCandidate: %v`, iceInit) 446 | 447 | // candidate := ice.ToJSON().Candidate 448 | // sdpMLineIndex := ice.ToJSON().SDPMLineIndex 449 | // sdpMid := ice.ToJSON().SDPMid 450 | 451 | peerConnection := n.connection.GetPeerConnection() 452 | provider := n.connection.GetProvider() 453 | 454 | err := peerConnection.AddICECandidate(*iceInit) 455 | if err != nil { 456 | provider.EmitError(enums.PeerErrorTypeWebRTC, err) 457 | n.log.Errorf("handleCandidate: %s", err) 458 | return err 459 | } 460 | 461 | n.log.Debugf(`Added ICE candidate for:%s`, n.connection.GetPeerID()) 462 | 463 | return nil 464 | } 465 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "github.com/muka/peerjs-go/enums" 5 | "github.com/muka/peerjs-go/models" 6 | "github.com/muka/peerjs-go/util" 7 | "github.com/pion/webrtc/v3" 8 | ) 9 | 10 | // NewOptions return Peer options with defaults 11 | func NewOptions() Options { 12 | return Options{ 13 | Host: "0.peerjs.com", 14 | Port: 443, 15 | PingInterval: 1000, 16 | Path: "/", 17 | Secure: true, 18 | Token: util.RandomToken(), 19 | Key: DefaultKey, 20 | Configuration: webrtc.Configuration{ 21 | ICEServers: []webrtc.ICEServer{ 22 | { 23 | URLs: []string{"stun:stun.l.google.com:19302"}, 24 | }, 25 | { 26 | URLs: []string{"turn:eu-0.turn.peerjs.com:3478", "turn:us-0.turn.peerjs.com:3478"}, 27 | Username: "peerjs", 28 | Credential: "peerjsp", 29 | CredentialType: webrtc.ICECredentialTypePassword, 30 | }, 31 | }, 32 | SDPSemantics: webrtc.SDPSemanticsUnifiedPlan, 33 | }, 34 | Debug: 0, 35 | } 36 | } 37 | 38 | // Options store Peer options 39 | type Options struct { 40 | // Key API key for the cloud PeerServer. This is not used for servers other than 0.peerjs.com. 41 | Key string 42 | // Server host. Defaults to 0.peerjs.com. Also accepts '/' to signify relative hostname. 43 | Host string 44 | //Port Server port. Defaults to 443. 45 | Port int 46 | //PingInterval Ping interval in ms. Defaults to 5000. 47 | PingInterval int 48 | //Path The path where your self-hosted PeerServer is running. Defaults to '/'. 49 | Path string 50 | //Secure true if you're using SSL. 51 | Secure bool 52 | //Configuration hash passed to RTCPeerConnection. This hash contains any custom ICE/TURN server configuration. Defaults to { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }], 'sdpSemantics': 'unified-plan' } 53 | Configuration webrtc.Configuration 54 | // Debug 55 | // Prints log messages depending on the debug level passed in. Defaults to 0. 56 | // 0 Prints no logs. 57 | // 1 Prints only errors. 58 | // 2 Prints errors and warnings. 59 | // 3 Prints all logs. 60 | Debug int8 61 | //Token a string to group peers 62 | Token string 63 | } 64 | 65 | // NewConnectionOptions return a ConnectionOptions with defaults 66 | func NewConnectionOptions() *ConnectionOptions { 67 | return &ConnectionOptions{ 68 | Serialization: enums.SerializationTypeRaw, 69 | Debug: -1, 70 | } 71 | } 72 | 73 | // ConnectionOptions wrap optios for Peer Connect() 74 | type ConnectionOptions struct { 75 | //ConnectionID 76 | ConnectionID string 77 | //Payload 78 | Payload models.Payload 79 | //Label A unique label by which you want to identify this data connection. If left unspecified, a label will be generated at random. 80 | Label string 81 | // Metadata associated with the connection, passed in by whoever initiated the connection. 82 | Metadata interface{} 83 | // Serialization. "raw" is the default. PeerJS supports other options, like encodings for JSON objects, but those aren't supported by this library. 84 | Serialization string 85 | // Reliable whether the underlying data channels should be reliable (e.g. for large file transfers) or not (e.g. for gaming or streaming). Defaults to false. 86 | Reliable bool 87 | // Stream contains the reference to a media stream 88 | Stream *MediaStream 89 | // Originator indicate if the originator 90 | Originator bool 91 | // SDP contains SDP information 92 | SDP webrtc.SessionDescription 93 | // Debug level for debug taken. See Options 94 | Debug int8 95 | // SDPTransform transformation function for SDP message 96 | SDPTransform func(string) string 97 | // MediaEngine override the default pion webrtc MediaEngine used in negotiating media channels. This allows you to specify your own supported media formats and parameters. 98 | MediaEngine *webrtc.MediaEngine 99 | } 100 | 101 | // AnswerOption wraps answer options 102 | type AnswerOption struct { 103 | // SDPTransform transformation function for SDP message 104 | SDPTransform func(string) string 105 | } 106 | -------------------------------------------------------------------------------- /peer.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/muka/peerjs-go/emitter" 9 | "github.com/muka/peerjs-go/enums" 10 | "github.com/muka/peerjs-go/models" 11 | "github.com/pion/webrtc/v3" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // DefaultKey is the default API key 16 | var DefaultKey = "peerjs" 17 | 18 | var socketEvents = []string{ 19 | enums.SocketEventTypeMessage, 20 | enums.SocketEventTypeError, 21 | enums.SocketEventTypeDisconnected, 22 | enums.SocketEventTypeClose, 23 | } 24 | 25 | type socketEventWrapper struct { 26 | Event string 27 | Data interface{} 28 | } 29 | 30 | type PeerError struct { 31 | Err error 32 | Type string 33 | } 34 | 35 | func (e PeerError) Unwrap() error { return e.Err } 36 | func (e PeerError) Error() string { return e.Err.Error() } 37 | 38 | // NewPeer initializes a new Peer object 39 | func NewPeer(id string, opts Options) (*Peer, error) { 40 | p := &Peer{ 41 | Emitter: emitter.NewEmitter(), 42 | opts: opts, 43 | api: NewAPI(opts), 44 | socket: NewSocket(opts), 45 | lostMessages: make(map[string][]models.Message), 46 | connections: make(map[string]map[string]Connection), 47 | } 48 | 49 | if id == "" { 50 | raw, err := p.api.RetrieveID() 51 | id = string(raw) 52 | if err != nil { 53 | return p, err 54 | } 55 | } 56 | 57 | p.log = createLogger(fmt.Sprintf("peer:%s", id), opts.Debug) 58 | 59 | err := p.initialize(id) 60 | if err != nil { 61 | return p, err 62 | } 63 | 64 | return p, nil 65 | } 66 | 67 | // Peer expose the PeerJS API 68 | type Peer struct { 69 | emitter.Emitter 70 | ID string 71 | opts Options 72 | connections map[string]map[string]Connection 73 | api API 74 | socket *Socket 75 | log *logrus.Entry 76 | open bool 77 | destroyed bool 78 | disconnected bool 79 | lastServerID string 80 | lostMessages map[string][]models.Message 81 | } 82 | 83 | // GetSocket return this peer's socket connection 84 | func (p *Peer) GetSocket() *Socket { 85 | return p.socket 86 | } 87 | 88 | // GetOptions return options 89 | func (p *Peer) GetOptions() Options { 90 | return p.opts 91 | } 92 | 93 | // GetSocket return this peer's is open state 94 | func (p *Peer) GetOpen() bool { 95 | return p.open 96 | } 97 | 98 | // GetDestroyed return this peer's is destroyed state 99 | // true if this peer and all of its connections can no longer be used. 100 | func (p *Peer) GetDestroyed() bool { 101 | return p.destroyed 102 | } 103 | 104 | // GetDisconnected return this peer's is disconnected state 105 | // returns false if there is an active connection to the PeerServer. 106 | func (p *Peer) GetDisconnected() bool { 107 | return p.disconnected 108 | } 109 | 110 | // AddConnection add the connection to the peer 111 | func (p *Peer) AddConnection(peerID string, connection Connection) { 112 | if _, ok := p.connections[peerID]; !ok { 113 | p.connections[peerID] = make(map[string]Connection) 114 | } 115 | p.connections[peerID][connection.GetID()] = connection 116 | } 117 | 118 | // RemoveConnection removes the connection from the peer 119 | func (p *Peer) RemoveConnection(connection Connection) { 120 | peerID := connection.GetPeerID() 121 | id := connection.GetID() 122 | if connections, ok := p.connections[peerID]; ok { 123 | for id := range connections { 124 | if id == connection.GetID() { 125 | delete(p.connections[peerID], id) 126 | } 127 | } 128 | } 129 | // remove lost messages 130 | if _, ok := p.lostMessages[id]; ok { 131 | delete(p.lostMessages, id) 132 | } 133 | } 134 | 135 | // GetConnection return a connection based on peerID and connectionID 136 | func (p *Peer) GetConnection(peerID string, connectionID string) (Connection, bool) { 137 | _, ok := p.connections[peerID] 138 | if !ok { 139 | return nil, false 140 | } 141 | conn, ok := p.connections[peerID][connectionID] 142 | return conn, ok 143 | } 144 | 145 | func (p *Peer) messageHandler(msg SocketEvent) { 146 | peerID := msg.Message.GetSrc() 147 | payload := msg.Message.GetPayload() 148 | switch msg.Message.GetType() { 149 | case enums.ServerMessageTypeOpen: 150 | p.lastServerID = p.ID 151 | p.open = true 152 | p.log.Debugf("Open session with id=%s", p.ID) 153 | p.Emit(enums.PeerEventTypeOpen, p.ID) 154 | break 155 | case enums.ServerMessageTypeError: 156 | if msg.Error == nil { 157 | msg.Error = errors.New(payload.Msg) 158 | } 159 | p.abort(enums.PeerErrorTypeServerError, msg.Error) 160 | break 161 | case enums.ServerMessageTypeIDTaken: // The selected ID is taken. 162 | p.abort(enums.PeerErrorTypeUnavailableID, fmt.Errorf("ID %s is taken", p.ID)) 163 | break 164 | case enums.ServerMessageTypeInvalidKey: // The given API key cannot be found. 165 | p.abort(enums.PeerErrorTypeInvalidKey, fmt.Errorf("API KEY %s is invalid", p.opts.Key)) 166 | break 167 | case enums.ServerMessageTypeLeave: // Another peer has closed its connection to this peer. 168 | peerID := msg.Message.GetSrc() 169 | p.log.Debugf("Received leave message from %s", peerID) 170 | p.cleanupPeer(peerID) 171 | if _, ok := p.connections[peerID]; ok { 172 | delete(p.connections, peerID) 173 | } 174 | break 175 | case enums.ServerMessageTypeExpire: // The offer sent to a peer has expired without response. 176 | p.EmitError(enums.PeerErrorTypePeerUnavailable, fmt.Errorf("Could not connect to peer %s", peerID)) 177 | break 178 | case enums.ServerMessageTypeOffer: 179 | 180 | // we should consider switching this to CALL/CONNECT, but this is the least breaking option. 181 | connectionID := payload.ConnectionID 182 | connection, ok := p.GetConnection(peerID, connectionID) 183 | 184 | if ok { 185 | connection.Close() 186 | p.log.Warnf("Offer received for existing Connection ID %s", connectionID) 187 | } 188 | 189 | var err error 190 | // Create a new connection. 191 | if payload.Type == enums.ConnectionTypeMedia { 192 | connection, err = NewMediaConnection(peerID, p, ConnectionOptions{ 193 | ConnectionID: connectionID, 194 | Payload: payload, 195 | Metadata: payload.Metadata, 196 | }) 197 | if err != nil { 198 | p.log.Errorf("Cannot initialize MediaConnection: %s", err) 199 | return 200 | } 201 | p.AddConnection(peerID, connection) 202 | p.Emit(enums.PeerEventTypeCall, connection) 203 | } else if payload.Type == enums.ConnectionTypeData { 204 | connection, err = NewDataConnection(peerID, p, ConnectionOptions{ 205 | ConnectionID: connectionID, 206 | Payload: payload, 207 | Metadata: payload.Metadata, 208 | Label: payload.Label, 209 | Serialization: payload.Serialization, 210 | Reliable: payload.Reliable, 211 | SDP: *payload.SDP, 212 | }) 213 | if err != nil { 214 | p.log.Errorf("Cannot initialize DataConnection: %s", err) 215 | return 216 | } 217 | p.AddConnection(peerID, connection) 218 | p.Emit(enums.PeerEventTypeConnection, connection) 219 | } else { 220 | p.log.Warnf(`Received malformed connection type:%s`, payload.Type) 221 | return 222 | } 223 | 224 | // Find messages. 225 | messages := p.GetMessages(connectionID) 226 | for _, message := range messages { 227 | connection.HandleMessage(&message) 228 | } 229 | 230 | break 231 | default: 232 | 233 | if msg.Message == nil { 234 | p.log.Warnf(`You received a malformed message from %s of type %s`, peerID, msg.Type) 235 | return 236 | } 237 | 238 | connectionID := msg.Message.GetPayload().ConnectionID 239 | connection, ok := p.GetConnection(peerID, connectionID) 240 | 241 | if ok && connection.GetPeerConnection() != nil { 242 | // Pass it on. 243 | connection.HandleMessage(msg.Message) 244 | } else if connectionID != "" { 245 | // Store for possible later use 246 | p.storeMessage(connectionID, *msg.Message) 247 | } else { 248 | p.log.Warnf("You received an unrecognized message: %v", msg.Message) 249 | } 250 | break 251 | } 252 | } 253 | 254 | // handles socket events 255 | func (p *Peer) socketEventHandler(data interface{}) { 256 | ev := data.(SocketEvent) 257 | switch ev.Type { 258 | case enums.SocketEventTypeMessage: 259 | p.messageHandler(ev) 260 | break 261 | case enums.SocketEventTypeError: 262 | p.abort(enums.PeerErrorTypeSocketError, ev.Error) 263 | break 264 | case enums.SocketEventTypeDisconnected: 265 | if p.disconnected { 266 | return 267 | } 268 | p.EmitError(enums.PeerErrorTypeNetwork, errors.New("Lost connection to server")) 269 | p.Disconnect() 270 | break 271 | case enums.SocketEventTypeClose: 272 | if p.disconnected { 273 | return 274 | } 275 | p.abort(enums.PeerErrorTypeSocketClosed, errors.New("Underlying socket is already closed")) 276 | break 277 | } 278 | } 279 | 280 | func (p *Peer) unregisterSocketHandlers() { 281 | for _, messageType := range socketEvents { 282 | p.socket.Off(messageType, p.socketEventHandler) 283 | } 284 | } 285 | 286 | func (p *Peer) registerSocketHandlers() { 287 | for _, messageType := range socketEvents { 288 | p.socket.On(messageType, p.socketEventHandler) 289 | } 290 | } 291 | 292 | // Stores messages without a set up connection, to be claimed later 293 | func (p *Peer) storeMessage(connectionID string, message models.Message) { 294 | if _, ok := p.lostMessages[connectionID]; !ok { 295 | p.lostMessages[connectionID] = []models.Message{} 296 | } 297 | p.lostMessages[connectionID] = append(p.lostMessages[connectionID], message) 298 | } 299 | 300 | // GetMessages Retrieve messages from lost message store 301 | func (p *Peer) GetMessages(connectionID string) []models.Message { 302 | if messages, ok := p.lostMessages[connectionID]; ok { 303 | delete(p.lostMessages, connectionID) 304 | return messages 305 | } 306 | return []models.Message{} 307 | } 308 | 309 | // Close closes the peer instance 310 | func (p *Peer) Close() { 311 | if p.lastServerID != "" { 312 | p.Destroy() 313 | } else { 314 | p.Disconnect() 315 | } 316 | } 317 | 318 | // Connect returns a DataConnection to the specified peer. See documentation for a 319 | // complete list of options. 320 | func (p *Peer) Connect(peerID string, opts *ConnectionOptions) (*DataConnection, error) { 321 | 322 | if opts == nil { 323 | opts = NewConnectionOptions() 324 | } 325 | 326 | if p.disconnected { 327 | p.log.Warn(` 328 | You cannot connect to a new Peer because you called .disconnect() on this Peer 329 | and ended your connection with the server. You can create a new Peer to reconnect, 330 | or call reconnect on this peer if you believe its ID to still be available`) 331 | err := errors.New("Cannot connect to new Peer after disconnecting from server") 332 | p.EmitError( 333 | enums.PeerErrorTypeDisconnected, 334 | err, 335 | ) 336 | return nil, err 337 | } 338 | 339 | // indicate we are starting the connection 340 | opts.Originator = true 341 | 342 | if opts.Debug == -1 { 343 | opts.Debug = p.opts.Debug 344 | } 345 | 346 | dataConnection, err := NewDataConnection(peerID, p, *opts) 347 | if err != nil { 348 | return dataConnection, err 349 | } 350 | 351 | p.AddConnection(peerID, dataConnection) 352 | return dataConnection, nil 353 | } 354 | 355 | // Call returns a MediaConnection to the specified peer. See documentation for a complete list of options. 356 | // To add more than one track to the call, set the stream parameter in the ConnectionOptions. 357 | // - Example: 358 | // connectionOpts := *peer.NewConnectionOptions() 359 | // connectionOpts.Stream = peer.NewMediaStreamWithTrack([]peer.MediaStreamTrack{track1, track2}) 360 | // Peer.Call("peer-id", nil, &connectionOpts) 361 | func (p *Peer) Call(peerID string, track webrtc.TrackLocal, opts *ConnectionOptions) (*MediaConnection, error) { 362 | 363 | if opts == nil { 364 | opts = NewConnectionOptions() 365 | } 366 | 367 | if p.disconnected { 368 | p.log.Warn("You cannot connect to a new Peer because you called .disconnect() on this Peer and ended your connection with the server. You can create a new Peer to reconnect") 369 | err := errors.New("Cannot connect to new Peer after disconnecting from server") 370 | p.EmitError( 371 | enums.PeerErrorTypeDisconnected, 372 | err, 373 | ) 374 | return nil, err 375 | } 376 | 377 | if track == nil && opts.Stream != nil { 378 | err := errors.New("To call a peer, you must provide a stream") 379 | p.log.Error(err) 380 | return nil, err 381 | } 382 | 383 | if opts.Stream == nil { 384 | opts.Stream = NewMediaStreamWithTrack([]MediaStreamTrack{track}) 385 | } 386 | 387 | mediaConnection, err := NewMediaConnection(peerID, p, *opts) 388 | if err != nil { 389 | p.log.Errorf("Failed to create a MediaConnection: %s", err) 390 | return nil, err 391 | } 392 | p.AddConnection(peerID, mediaConnection) 393 | return mediaConnection, nil 394 | } 395 | 396 | func (p *Peer) abort(errType string, err error) error { 397 | p.log.Error("Aborting!") 398 | p.EmitError(errType, err) 399 | p.Close() 400 | return err 401 | } 402 | 403 | // EmitError emits an error 404 | func (p *Peer) EmitError(errType string, err error) { 405 | p.log.Errorf("Error: %s", err) 406 | p.Emit(enums.PeerEventTypeError, PeerError{ 407 | Type: errType, 408 | Err: err, 409 | }) 410 | } 411 | 412 | func (p *Peer) initialize(id string) error { 413 | p.log.Debugf("Initializing id=%s", id) 414 | p.ID = id 415 | //register event handler 416 | p.registerSocketHandlers() 417 | return p.socket.Start(id, p.opts.Token) 418 | } 419 | 420 | // destroys the Peer: closes all active connections as well as the connection 421 | // to the server. 422 | // Warning: The peer can no longer create or accept connections after being 423 | // destroyed. 424 | func (p *Peer) Destroy() { 425 | 426 | if p.destroyed { 427 | return 428 | } 429 | 430 | p.log.Debugf(`Destroy peer with ID:%s`, p.ID) 431 | 432 | p.Disconnect() 433 | p.cleanup() 434 | 435 | p.destroyed = true 436 | 437 | p.Emit(enums.PeerEventTypeClose, nil) 438 | } 439 | 440 | // cleanup Disconnects every connection on this peer. 441 | func (p *Peer) cleanup() { 442 | for peerID := range p.connections { 443 | p.cleanupPeer(peerID) 444 | delete(p.connections, peerID) 445 | } 446 | 447 | err := p.socket.Close() 448 | p.socket = nil 449 | if err != nil { 450 | p.log.Warnf("Failed to close socket: %s", err) 451 | } 452 | } 453 | 454 | // cleanupPeer Closes all connections to this peer. 455 | func (p *Peer) cleanupPeer(peerID string) { 456 | connections, ok := p.connections[peerID] 457 | if !ok { 458 | return 459 | } 460 | for _, connection := range connections { 461 | connection.Close() 462 | } 463 | } 464 | 465 | // Disconnect disconnects the Peer's connection to the PeerServer. Does not close any 466 | // active connections. 467 | // Warning: The peer can no longer create or accept connections after being 468 | // disconnected. It also cannot reconnect to the server. 469 | func (p *Peer) Disconnect() { 470 | if p.disconnected { 471 | return 472 | } 473 | 474 | currentID := p.ID 475 | 476 | p.log.Debugf("Disconnect peer with ID:%s", currentID) 477 | 478 | p.disconnected = true 479 | p.open = false 480 | 481 | // remove registered handlers 482 | p.unregisterSocketHandlers() 483 | p.socket.Close() 484 | 485 | p.lastServerID = currentID 486 | p.ID = "" 487 | 488 | p.Emit(enums.PeerEventTypeDisconnected, currentID) 489 | } 490 | 491 | // Reconnect Attempts to reconnect with the same ID 492 | func (p *Peer) Reconnect() error { 493 | 494 | if p.disconnected && !p.destroyed { 495 | p.log.Debugf(`Attempting reconnection to server with ID %s`, p.lastServerID) 496 | p.disconnected = false 497 | p.initialize(p.lastServerID) 498 | return nil 499 | } 500 | 501 | if p.destroyed { 502 | return errors.New("This peer cannot reconnect to the server. It has already been destroyed") 503 | } 504 | 505 | if !p.disconnected && !p.open { 506 | // Do nothing. We're still connecting the first time. 507 | p.log.Error("In a hurry? We're still trying to make the initial connection!") 508 | return nil 509 | } 510 | 511 | return fmt.Errorf(`Peer %s cannot reconnect because it is not disconnected from the server`, p.ID) 512 | } 513 | 514 | // ListAllPeers Get a list of available peer IDs. If you're running your own server, you'll 515 | // want to set allow_discovery: true in the PeerServer options. If you're using 516 | // the cloud server, email team@peerjs.com to get the functionality enabled for 517 | // your key. 518 | func (p *Peer) ListAllPeers() ([]string, error) { 519 | 520 | peers := []string{} 521 | raw, err := p.api.ListAllPeers() 522 | if err != nil { 523 | return peers, p.abort(enums.PeerErrorTypeServerError, err) 524 | } 525 | 526 | err = json.Unmarshal(raw, &peers) 527 | if err != nil { 528 | return peers, p.abort(enums.PeerErrorTypeServerError, err) 529 | } 530 | 531 | return peers, nil 532 | } 533 | -------------------------------------------------------------------------------- /peer_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "testing" 8 | "time" 9 | 10 | "github.com/muka/peerjs-go/server" 11 | "github.com/muka/peerjs-go/util" 12 | "github.com/pion/webrtc/v3" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func rndName(name string) string { 17 | return fmt.Sprintf("%s_%s", name, util.RandomToken()) 18 | } 19 | 20 | func getTestOpts(serverOpts server.Options) Options { 21 | opts := NewOptions() 22 | opts.Path = serverOpts.Path 23 | opts.Host = serverOpts.Host 24 | opts.Port = serverOpts.Port 25 | opts.Secure = false 26 | opts.Debug = 3 27 | return opts 28 | } 29 | 30 | func startServer() (*server.PeerServer, server.Options) { 31 | opts := server.NewOptions() 32 | opts.Port = 9000 33 | opts.Host = "localhost" 34 | opts.Path = "/myapp" 35 | opts.LogLevel = "debug" 36 | return server.New(opts), opts 37 | } 38 | 39 | func TestNewPeer(t *testing.T) { 40 | peerServer, serverOpts := startServer() 41 | err := peerServer.Start() 42 | if err != nil { 43 | t.Logf("Server error: %s", err) 44 | t.FailNow() 45 | } 46 | defer peerServer.Stop() 47 | p, err := NewPeer("test", getTestOpts(serverOpts)) 48 | assert.NoError(t, err) 49 | assert.NotEmpty(t, p.ID) 50 | p.Close() 51 | } 52 | 53 | func TestNewPeerRandomID(t *testing.T) { 54 | peerServer, serverOpts := startServer() 55 | err := peerServer.Start() 56 | if err != nil { 57 | t.Logf("Server error: %s", err) 58 | t.FailNow() 59 | } 60 | defer peerServer.Stop() 61 | p, err := NewPeer("", getTestOpts(serverOpts)) 62 | assert.NoError(t, err) 63 | assert.NotEmpty(t, p.ID) 64 | p.Close() 65 | } 66 | 67 | func TestNewPeerEvents(t *testing.T) { 68 | peerServer, serverOpts := startServer() 69 | err := peerServer.Start() 70 | if err != nil { 71 | t.Logf("Server error: %s", err) 72 | t.FailNow() 73 | } 74 | defer peerServer.Stop() 75 | p, err := NewPeer(rndName("test"), getTestOpts(serverOpts)) 76 | // done := false 77 | // p.On(PeerEventTypeOpen, func(data interface{}) { 78 | // done = true 79 | // }) 80 | assert.NoError(t, err) 81 | assert.NotEmpty(t, p.ID) 82 | 83 | p.Close() 84 | // <-time.After(time.Millisecond * 1000) 85 | // assert.True(t, done) 86 | } 87 | 88 | func TestDuplicatedID(t *testing.T) { 89 | 90 | peer1Name := rndName("duplicated") 91 | 92 | peerServer, serverOpts := startServer() 93 | err := peerServer.Start() 94 | if err != nil { 95 | t.Logf("Server error: %s", err) 96 | t.FailNow() 97 | } 98 | defer peerServer.Stop() 99 | 100 | peer1, err := NewPeer(peer1Name, getTestOpts(serverOpts)) 101 | assert.NoError(t, err) 102 | defer peer1.Close() 103 | 104 | peer2, err := NewPeer(peer1Name, getTestOpts(serverOpts)) 105 | assert.NoError(t, err) 106 | defer peer2.Close() 107 | 108 | peer2.On("error", func(raw interface{}) { 109 | err := raw.(error) 110 | assert.Error(t, err) 111 | assert.Contains(t, err.Error(), "taken") 112 | }) 113 | 114 | <-time.After(time.Second * 1) 115 | } 116 | 117 | func TestHelloWorld(t *testing.T) { 118 | 119 | peer1Name := rndName("peer1") 120 | peer2Name := rndName("peer2") 121 | 122 | peerServer, serverOpts := startServer() 123 | err := peerServer.Start() 124 | if err != nil { 125 | t.Logf("Server error: %s", err) 126 | t.FailNow() 127 | } 128 | defer peerServer.Stop() 129 | 130 | <-time.After(4 * time.Second) 131 | println("STARTING PEERS") 132 | 133 | peer1, err := NewPeer(peer1Name, getTestOpts(serverOpts)) 134 | assert.NoError(t, err) 135 | defer peer1.Close() 136 | 137 | peer2, err := NewPeer(peer2Name, getTestOpts(serverOpts)) 138 | assert.NoError(t, err) 139 | defer peer2.Close() 140 | 141 | // done := false 142 | done := false 143 | peer2.On("connection", func(data interface{}) { 144 | print("peer2 recived connection!") 145 | conn2 := data.(*DataConnection) 146 | conn2.On("data", func(data interface{}) { 147 | // Will print 'hi!' 148 | log.Println("Received") 149 | done = true 150 | }) 151 | }) 152 | 153 | conn1, err := peer1.Connect(peer2Name, nil) 154 | assert.NoError(t, err) 155 | conn1.On("open", func(data interface{}) { 156 | print("Conn1 open!") 157 | conn1.Send([]byte("hi!"), false) 158 | for { 159 | conn1.Send([]byte("hi!"), false) 160 | <-time.After(time.Millisecond * 1000) 161 | } 162 | }) 163 | 164 | select { 165 | case <-time.After(time.Second * 20): 166 | assert.True(t, done) 167 | default: 168 | if done == true { 169 | return 170 | } 171 | } 172 | } 173 | 174 | func TestLongPayload(t *testing.T) { 175 | 176 | peer1Name := rndName("peer1") 177 | peer2Name := rndName("peer2") 178 | 179 | peerServer, serverOpts := startServer() 180 | err := peerServer.Start() 181 | if err != nil { 182 | t.Logf("Server error: %s", err) 183 | t.FailNow() 184 | } 185 | defer peerServer.Stop() 186 | 187 | peer1, err := NewPeer(peer1Name, getTestOpts(serverOpts)) 188 | assert.NoError(t, err) 189 | defer peer1.Close() 190 | 191 | peer2, err := NewPeer(peer2Name, getTestOpts(serverOpts)) 192 | assert.NoError(t, err) 193 | defer peer2.Close() 194 | 195 | done := make(chan bool) 196 | peer2.On("connection", func(data interface{}) { 197 | conn2 := data.(*DataConnection) 198 | conn2.On("data", func(data interface{}) { 199 | log.Printf("Received\n") 200 | done <- true 201 | }) 202 | }) 203 | 204 | conn1, err := peer1.Connect(peer2Name, nil) 205 | assert.NoError(t, err) 206 | if err != nil { 207 | t.Fatal(err) 208 | } 209 | conn1.On("open", func(data interface{}) { 210 | raw := bytes.NewBuffer([]byte{}) 211 | for { 212 | raw.Write([]byte("test")) 213 | if raw.Len() > 60000 { 214 | log.Printf("Msg size %d\n", raw.Len()) 215 | break 216 | } 217 | } 218 | conn1.Send(raw.Bytes(), false) 219 | }) 220 | 221 | <-done 222 | } 223 | 224 | func TestMediaCall(t *testing.T) { 225 | 226 | peer1Name := rndName("peer1") 227 | peer2Name := rndName("peer2") 228 | 229 | peerServer, serverOpts := startServer() 230 | err := peerServer.Start() 231 | if err != nil { 232 | t.Logf("Server error: %s", err) 233 | t.FailNow() 234 | } 235 | defer peerServer.Stop() 236 | 237 | peer1, err := NewPeer(peer1Name, getTestOpts(serverOpts)) 238 | assert.NoError(t, err) 239 | defer peer1.Close() 240 | 241 | peer2, err := NewPeer(peer2Name, getTestOpts(serverOpts)) 242 | assert.NoError(t, err) 243 | defer peer2.Close() 244 | 245 | track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: "video/vp8"}, "video", "pion") 246 | if err != nil { 247 | panic(err) 248 | } 249 | 250 | call1, err := peer1.Call(peer2Name, track, nil) 251 | assert.NoError(t, err) 252 | 253 | peer2.On("call", func(raw interface{}) { 254 | // Answer the call, providing our mediaStream 255 | call := raw.(MediaConnection) 256 | var mediaStream webrtc.TrackLocal 257 | 258 | call.Answer(mediaStream, nil) 259 | call.On("stream", func(raw interface{}) { 260 | // stream := raw.(MediaStream) 261 | t.Log("peer2: Received remote stream") 262 | }) 263 | }) 264 | 265 | call1.On("stream", func(raw interface{}) { 266 | // stream := raw.(MediaStream) 267 | t.Log("peer1: Received remote stream") 268 | }) 269 | 270 | } 271 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | // NewEncodingQueue initializes an EncodingQueue 4 | // func NewEncodingQueue() *EncodingQueue { 5 | // return &EncodingQueue{ 6 | // Emitter: NewEmitter(), 7 | // Queue: [][]byte{}, 8 | // } 9 | // } 10 | 11 | // //EncodingQueue encoding queue 12 | // type EncodingQueue struct { 13 | // Emitter 14 | // Processing bool 15 | // Queue [][]byte 16 | // } 17 | 18 | // // Destroy destroys the queue instance 19 | // func (e *EncodingQueue) Destroy() { 20 | // e.Processing = false 21 | // e.Queue = [][]byte{} 22 | // } 23 | 24 | // // Size return the queue size 25 | // func (e *EncodingQueue) Size() int { 26 | // return len(e.Queue) 27 | // } 28 | 29 | // // Enque add element to the queue 30 | // func (e *EncodingQueue) Enque(raw []byte) { 31 | // // e.queue = append(e.queue, raw) 32 | // // TODO understand if conversion to ArrayBuffer is needed 33 | // e.Emit("done", raw) 34 | // } 35 | -------------------------------------------------------------------------------- /server/auth.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // NewAuth init a new Auth middleware 12 | func NewAuth(realm IRealm, opts Options) *Auth { 13 | a := new(Auth) 14 | a.opts = opts 15 | a.realm = realm 16 | a.log = createLogger("auth", opts) 17 | return a 18 | } 19 | 20 | // Auth handles request authentication 21 | type Auth struct { 22 | opts Options 23 | log *logrus.Entry 24 | realm IRealm 25 | } 26 | 27 | // AuthError is an error that occurs during authentication and contains the original error plus the http status code that should be returned to the client 28 | type AuthError struct { 29 | Err error 30 | StatusCode int 31 | } 32 | 33 | func (e AuthError) Error() string { return e.Err.Error() } 34 | 35 | // premade auth errors: 36 | var errInvalidKey = AuthError{Err: errors.New(ErrorInvalidKey), StatusCode: http.StatusUnauthorized} 37 | var errInvalidToken = AuthError{Err: errors.New(ErrorInvalidToken), StatusCode: http.StatusUnauthorized} 38 | var errUnauthorized = AuthError{Err: errors.New(http.StatusText(http.StatusUnauthorized)), StatusCode: http.StatusUnauthorized} 39 | 40 | //checkRequest check if the input is valid 41 | func (a *Auth) checkRequest(key, id, token string) error { 42 | 43 | if key != a.opts.Key { 44 | return errInvalidKey 45 | } 46 | 47 | if id == "" { 48 | return errUnauthorized 49 | } 50 | 51 | client := a.realm.GetClientByID(id) 52 | 53 | if client == nil { 54 | return errUnauthorized // client not found should return errUnauthorized status code per peerjs server implementation 55 | } 56 | 57 | if len(client.GetToken()) > 0 && client.GetToken() != token { 58 | return errInvalidToken 59 | } 60 | 61 | return nil // no error 62 | } 63 | 64 | //WSHandler return a websocket handler middleware 65 | func (a *Auth) WSHandler(handler http.HandlerFunc) http.HandlerFunc { 66 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | 68 | // keys := r.URL.Query() 69 | // key := keys.Get("key") 70 | // id := keys.Get("id") 71 | // token := keys.Get("token") 72 | 73 | // err := a.checkRequest(key, id, token) 74 | // if err != nil { 75 | // http.Error(w, err.Error(), http.StatusUnauthorized) 76 | // return 77 | // } 78 | 79 | handler(w, r) 80 | }) 81 | } 82 | 83 | //HTTPHandler return an HTTP handler middleware 84 | func (a *Auth) HTTPHandler(handler http.HandlerFunc) http.Handler { 85 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 | 87 | params := mux.Vars(r) 88 | key := params["key"] 89 | id := params["id"] 90 | token := params["token"] 91 | 92 | err := a.checkRequest(key, id, token) 93 | if err != nil { 94 | http.Error(w, err.Error(), err.(AuthError).StatusCode) 95 | return 96 | } 97 | 98 | handler(w, r) 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /server/auth_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewAuth(t *testing.T) { 10 | a := NewAuth(NewRealm(), NewOptions()) 11 | assert.NotNil(t, a) 12 | } 13 | 14 | func testGetAuth() *Auth { 15 | o := NewOptions() 16 | r := NewRealm() 17 | a := NewAuth(r, o) 18 | return a 19 | } 20 | 21 | func TestAuthHandlerWrongKey(t *testing.T) { 22 | 23 | a := testGetAuth() 24 | id := a.realm.GenerateClientID() 25 | token := a.realm.GenerateClientID() 26 | 27 | // wrong key 28 | err := a.checkRequest("wrong key", id, token) 29 | assert.Equal(t, err, errInvalidKey) 30 | } 31 | func TestAuthHandlerClientValid(t *testing.T) { 32 | 33 | a := testGetAuth() 34 | id := a.realm.GenerateClientID() 35 | token := a.realm.GenerateClientID() 36 | 37 | var err error 38 | 39 | // valid client 40 | client := NewClient(id, token) 41 | a.realm.SetClient(client, id) 42 | err = a.checkRequest(a.opts.Key, id, token) 43 | assert.Nil(t, err) 44 | } 45 | 46 | func TestAuthHandlerClientIDEmpty(t *testing.T) { 47 | 48 | a := testGetAuth() 49 | token := a.realm.GenerateClientID() 50 | 51 | var err error 52 | 53 | err = a.checkRequest(a.opts.Key, "", token) 54 | assert.Equal(t, err, errUnauthorized) 55 | 56 | } 57 | func TestAuthHandlerClientTokenEmpty(t *testing.T) { 58 | 59 | a := testGetAuth() 60 | id := a.realm.GenerateClientID() 61 | 62 | var err error 63 | 64 | err = a.checkRequest(a.opts.Key, id, "") 65 | assert.Equal(t, err, errUnauthorized) 66 | 67 | } 68 | func TestAuthHandlerClientTokenInvalid(t *testing.T) { 69 | 70 | a := testGetAuth() 71 | id := a.realm.GenerateClientID() 72 | token := a.realm.GenerateClientID() 73 | 74 | var err error 75 | 76 | client := NewClient(id, token) 77 | a.realm.SetClient(client, id) 78 | err = a.checkRequest(a.opts.Key, id, token) 79 | 80 | err = a.checkRequest(a.opts.Key, id, "wrong") 81 | assert.Equal(t, err, errInvalidToken) 82 | 83 | } 84 | func TestAuthHandlerClientRemoved(t *testing.T) { 85 | 86 | a := testGetAuth() 87 | id := a.realm.GenerateClientID() 88 | token := a.realm.GenerateClientID() 89 | 90 | var err error 91 | 92 | // valid client 93 | client := NewClient(id, token) 94 | a.realm.SetClient(client, id) 95 | err = a.checkRequest(a.opts.Key, id, token) 96 | assert.Nil(t, err) 97 | 98 | a.realm.RemoveClientByID(id) 99 | assert.Nil(t, a.realm.GetClientByID(id)) 100 | 101 | err = a.checkRequest(a.opts.Key, id, token) 102 | assert.Error(t, err) 103 | 104 | } 105 | -------------------------------------------------------------------------------- /server/broken_connections.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | const DefaultCheckInterval = 300 10 | 11 | //NewCheckBrokenConnections create a new CheckBrokenConnections 12 | func NewCheckBrokenConnections(realm IRealm, opts Options, onClose func(client IClient)) *CheckBrokenConnections { 13 | if onClose == nil { 14 | onClose = func(client IClient) {} 15 | } 16 | return &CheckBrokenConnections{ 17 | realm: realm, 18 | opts: opts, 19 | onClose: onClose, 20 | log: createLogger("checkBrokenConnections", opts), 21 | close: make(chan bool, 1), 22 | } 23 | } 24 | 25 | //CheckBrokenConnections watch for broken connections 26 | type CheckBrokenConnections struct { 27 | realm IRealm 28 | opts Options 29 | onClose func(IClient) 30 | ticker *time.Ticker 31 | log *logrus.Entry 32 | close chan bool 33 | } 34 | 35 | func (b *CheckBrokenConnections) checkConnections() { 36 | 37 | clientsIds := b.realm.GetClientsIds() 38 | now := getTime() 39 | aliveTimeout := b.opts.AliveTimeout 40 | 41 | for _, clientID := range clientsIds { 42 | 43 | client := b.realm.GetClientByID(clientID) 44 | if client == nil { 45 | continue 46 | } 47 | 48 | timeSinceLastPing := now - client.GetLastPing() 49 | 50 | if timeSinceLastPing < aliveTimeout { 51 | continue 52 | } 53 | 54 | socket := client.GetSocket() 55 | if socket != nil { 56 | b.log.Infof("Closing broken connection clientID=%s", clientID) 57 | err := socket.Close() 58 | if err != nil { 59 | b.log.Warnf("Failed to close socket: %s", err) 60 | } 61 | } 62 | b.realm.ClearMessageQueue(clientID) 63 | b.realm.RemoveClientByID(clientID) 64 | client.SetSocket(nil) 65 | b.onClose(client) 66 | } 67 | } 68 | 69 | //Stop close the connection checker 70 | func (b *CheckBrokenConnections) Stop() { 71 | if b.ticker == nil { 72 | return 73 | } 74 | b.close <- true 75 | } 76 | 77 | //Start initialize the connection checker 78 | func (b *CheckBrokenConnections) Start() { 79 | 80 | b.ticker = time.NewTicker(DefaultCheckInterval * time.Millisecond) 81 | 82 | go func() { 83 | for { 84 | select { 85 | case <-b.close: 86 | b.ticker.Stop() 87 | b.ticker = nil 88 | return 89 | case <-b.ticker.C: 90 | b.checkConnections() 91 | } 92 | } 93 | }() 94 | 95 | } 96 | -------------------------------------------------------------------------------- /server/client.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/gorilla/websocket" 4 | 5 | // IClient client interface 6 | type IClient interface { 7 | GetID() string 8 | GetToken() string 9 | GetSocket() *websocket.Conn 10 | SetSocket(socket *websocket.Conn) 11 | GetLastPing() int64 12 | SetLastPing(lastPing int64) 13 | Send(data []byte) error 14 | } 15 | 16 | // Client implementation 17 | type Client struct { 18 | id string 19 | token string 20 | socket *websocket.Conn 21 | lastPing int64 22 | } 23 | 24 | //NewClient initialize a new client 25 | func NewClient(id string, token string) *Client { 26 | c := new(Client) 27 | c.id = id 28 | c.token = token 29 | c.SetLastPing(getTime()) 30 | return c 31 | } 32 | 33 | //GetID return client id 34 | func (c *Client) GetID() string { 35 | return c.id 36 | } 37 | 38 | //GetToken return client token 39 | func (c *Client) GetToken() string { 40 | return c.token 41 | } 42 | 43 | //GetSocket return the web socket server 44 | func (c *Client) GetSocket() *websocket.Conn { 45 | return c.socket 46 | } 47 | 48 | //SetSocket set the web socket handler 49 | func (c *Client) SetSocket(socket *websocket.Conn) { 50 | c.socket = socket 51 | } 52 | 53 | // GetLastPing return the last ping timestamp 54 | func (c *Client) GetLastPing() int64 { 55 | return c.lastPing 56 | } 57 | 58 | //SetLastPing set last ping timestamp 59 | func (c *Client) SetLastPing(lastPing int64) { 60 | c.lastPing = lastPing 61 | } 62 | 63 | //Send send data 64 | func (c *Client) Send(data []byte) error { 65 | return c.socket.WriteMessage(websocket.BinaryMessage, data) 66 | } 67 | -------------------------------------------------------------------------------- /server/enum.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | const ( 4 | // ErrorInvalidKey Invalid key provided 5 | ErrorInvalidKey = "Invalid key provided" 6 | // ErrorInvalidToken Invalid token provided 7 | ErrorInvalidToken = "Invalid token provided" 8 | // ErrorInvalidWSParameters No id, token, or key supplied to websocket server 9 | ErrorInvalidWSParameters = "No id, token, or key supplied to websocket server" 10 | // ErrorConnectionLimitExceeded Server has reached its concurrent user limit 11 | ErrorConnectionLimitExceeded = "Server has reached its concurrent user limit" 12 | // MessageTypeOpen OPEN 13 | MessageTypeOpen = "OPEN" 14 | // MessageTypeLeave LEAVE 15 | MessageTypeLeave = "LEAVE" 16 | // MessageTypeCandidate CANDIDATE 17 | MessageTypeCandidate = "CANDIDATE" 18 | // MessageTypeOffer OFFER 19 | MessageTypeOffer = "OFFER" 20 | // MessageTypeAnswer ANSWER 21 | MessageTypeAnswer = "ANSWER" 22 | // MessageTypeExpire EXPIRE 23 | MessageTypeExpire = "EXPIRE" 24 | // MessageTypeHeartbeat HEARTBEAT 25 | MessageTypeHeartbeat = "HEARTBEAT" 26 | // MessageTypeIDTaken ID-TAKEN 27 | MessageTypeIDTaken = "ID-TAKEN" 28 | // MessageTypeError ERROR 29 | MessageTypeError = "ERROR" 30 | 31 | // WebsocketEventMessage message 32 | WebsocketEventMessage = "message" 33 | // WebsocketEventConnection connection 34 | WebsocketEventConnection = "connection" 35 | // WebsocketEventError error 36 | WebsocketEventError = "error" 37 | // WebsocketEventClose close 38 | WebsocketEventClose = "close" 39 | ) 40 | -------------------------------------------------------------------------------- /server/handlers_heartbeat.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/muka/peerjs-go/models" 5 | ) 6 | 7 | //NewHeartbeatHandler handles a heartbeat 8 | func NewHeartbeatHandler(opts Options) func(client IClient, message models.IMessage) bool { 9 | return func(client IClient, message models.IMessage) bool { 10 | if client != nil { 11 | client.SetLastPing(getTime()) 12 | } 13 | 14 | return true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/handlers_registry.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/muka/peerjs-go/models" 5 | ) 6 | 7 | //Handler wrap a callback 8 | type Handler func(client IClient, message models.IMessage) bool 9 | 10 | // IHandlersRegistry interface for HandlersRegistry 11 | type IHandlersRegistry interface { 12 | RegisterHandler(messageType string, handler Handler) 13 | Handle(client IClient, message models.IMessage) bool 14 | } 15 | 16 | //NewHandlersRegistry creates a new HandlersRegistry 17 | func NewHandlersRegistry() IHandlersRegistry { 18 | h := &HandlersRegistry{ 19 | handlers: make(map[string]Handler), 20 | } 21 | return h 22 | } 23 | 24 | // HandlersRegistry handlers registry 25 | type HandlersRegistry struct { 26 | handlers map[string]Handler 27 | } 28 | 29 | // RegisterHandler register an handler 30 | func (r *HandlersRegistry) RegisterHandler(messageType string, handler Handler) { 31 | if _, ok := r.handlers[messageType]; ok { 32 | return 33 | } 34 | r.handlers[messageType] = handler 35 | } 36 | 37 | //Handle handles a message 38 | func (r *HandlersRegistry) Handle(client IClient, message models.IMessage) bool { 39 | handler, ok := r.handlers[message.GetType()] 40 | if !ok { 41 | return false 42 | } 43 | return handler(client, message) 44 | } 45 | -------------------------------------------------------------------------------- /server/handlers_transmission.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/muka/peerjs-go/models" 7 | ) 8 | 9 | //NewTransmissionHandler handles transmission of messages 10 | func NewTransmissionHandler(realm IRealm, opts Options) func(client IClient, message models.IMessage) bool { 11 | 12 | var handle func(client IClient, message models.IMessage) bool 13 | 14 | handle = func(client IClient, message models.IMessage) bool { 15 | 16 | clientID := "" 17 | if client != nil { 18 | clientID = client.GetID() 19 | } 20 | 21 | log := createLogger("client:"+clientID, opts) 22 | 23 | mtype := message.GetType() 24 | srcID := message.GetSrc() 25 | dstID := message.GetDst() 26 | 27 | destinationClient := realm.GetClientByID(dstID) 28 | 29 | // User is connected! 30 | if destinationClient != nil { 31 | socket := destinationClient.GetSocket() 32 | var err error 33 | if socket != nil { 34 | err = socket.WriteJSON(message) 35 | } else { 36 | err = errors.New("Peer dead") 37 | } 38 | 39 | if err != nil { 40 | // This happens when a peer disconnects without closing connections and 41 | // the associated WebSocket has not closed. 42 | // Tell other side to stop trying. 43 | log.Warnf("Error: %s", err) 44 | if socket != nil { 45 | socket.Close() 46 | } else { 47 | realm.RemoveClientByID(destinationClient.GetID()) 48 | } 49 | 50 | handle(client, models.Message{ 51 | Type: MessageTypeLeave, 52 | Src: dstID, 53 | Dst: srcID, 54 | }) 55 | } 56 | 57 | } else { 58 | // Wait for this client to connect/reconnect (XHR) for important 59 | // messages. 60 | if (mtype != MessageTypeLeave && mtype != MessageTypeExpire) && dstID != "" { 61 | realm.AddMessageToQueue(dstID, message) 62 | } else if mtype == MessageTypeLeave && dstID == "" { 63 | realm.RemoveClientByID(srcID) 64 | } else { 65 | // Unavailable destination specified with message LEAVE or EXPIRE 66 | // Ignore 67 | } 68 | } 69 | 70 | return true 71 | } 72 | 73 | return handle 74 | } 75 | -------------------------------------------------------------------------------- /server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/muka/peerjs-go/models" 12 | "github.com/rs/cors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // NewOptions create default options 17 | func NewOptions() Options { 18 | return Options{ 19 | Port: 9000, 20 | Host: "0.0.0.0", 21 | LogLevel: "info", 22 | ExpireTimeout: 5000, 23 | AliveTimeout: 60000, 24 | Key: "peerjs", 25 | Path: "/", 26 | ConcurrentLimit: 5000, 27 | AllowDiscovery: false, 28 | CleanupOutMsgs: 1000, 29 | } 30 | } 31 | 32 | // Options peer server options 33 | type Options struct { 34 | Port int 35 | Host string 36 | LogLevel string 37 | ExpireTimeout int64 38 | AliveTimeout int64 39 | Key string 40 | Path string 41 | ConcurrentLimit int 42 | AllowDiscovery bool 43 | CleanupOutMsgs int 44 | } 45 | 46 | // HTTPServer peer server 47 | type HTTPServer struct { 48 | opts Options 49 | realm IRealm 50 | log *logrus.Entry 51 | messageHandler *MessageHandler 52 | router *mux.Router 53 | http *http.Server 54 | handlers []func(http.HandlerFunc) http.HandlerFunc 55 | auth *Auth 56 | wss *WebSocketServer 57 | } 58 | 59 | // NewHTTPServer init a server 60 | func NewHTTPServer(realm IRealm, auth *Auth, wss *WebSocketServer, opts Options) *HTTPServer { 61 | 62 | r := mux.NewRouter() 63 | 64 | s := &HTTPServer{ 65 | opts: opts, 66 | realm: realm, 67 | log: createLogger("http", opts), 68 | router: r, 69 | // http: srv, 70 | handlers: []func(http.HandlerFunc) http.HandlerFunc{}, 71 | messageHandler: NewMessageHandler(realm, nil, opts), 72 | auth: auth, 73 | wss: wss, 74 | } 75 | 76 | return s 77 | } 78 | 79 | func (h *HTTPServer) handler() http.HandlerFunc { 80 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 81 | vars := mux.Vars(r) 82 | id := vars["id"] 83 | 84 | if id == "" { 85 | http.Error(w, "Missing client id", http.StatusBadRequest) 86 | return 87 | } 88 | 89 | client := h.realm.GetClientByID(id) 90 | if client == nil { 91 | http.Error(w, fmt.Sprintf("Client %s not found", id), http.StatusNotFound) 92 | return 93 | } 94 | 95 | body, err := ioutil.ReadAll(r.Body) 96 | if err != nil { 97 | http.Error(w, "Failed to read body", http.StatusInternalServerError) 98 | return 99 | } 100 | 101 | payload := new(models.Message) 102 | err = json.Unmarshal(body, payload) 103 | if err != nil { 104 | http.Error(w, "Failed to decode message", http.StatusInternalServerError) 105 | return 106 | } 107 | 108 | message := models.Message{ 109 | Type: payload.Type, 110 | Src: id, 111 | Dst: payload.Dst, 112 | Payload: payload.Payload, 113 | } 114 | 115 | h.messageHandler.Handle(client, message) 116 | 117 | w.WriteHeader(200) 118 | w.Write([]byte{}) 119 | }) 120 | } 121 | 122 | func (h *HTTPServer) peersHandler() http.HandlerFunc { 123 | return func(rw http.ResponseWriter, r *http.Request) { 124 | if !h.opts.AllowDiscovery { 125 | rw.WriteHeader(http.StatusUnauthorized) 126 | rw.Write([]byte{}) 127 | return 128 | } 129 | 130 | rw.Header().Add("content-type", "application/json") 131 | raw, err := json.Marshal(h.realm.GetClientsIds()) 132 | if err != nil { 133 | h.log.Warnf("/peers: Marshal error %s", err) 134 | rw.WriteHeader(http.StatusInternalServerError) 135 | rw.Write([]byte{}) 136 | return 137 | } 138 | rw.Write(raw) 139 | } 140 | } 141 | 142 | func (h *HTTPServer) registerHandlers() error { 143 | 144 | baseRoute := h.router.PathPrefix(h.opts.Path).Subrouter() 145 | h.log.Debugf("Path prefix: %s", h.opts.Path) 146 | 147 | err := baseRoute. 148 | HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) { 149 | rw.Header().Add("content-type", "application/json") 150 | rw.Write([]byte(`{ 151 | "name": "PeerJS Server", 152 | "description": "A server side element to broker connections between PeerJS clients.", 153 | "website": "https://github.com/muka/peerjs-go/tree/main/server" 154 | }`)) 155 | }). 156 | Methods("GET").GetError() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | // public API 162 | err = baseRoute. 163 | HandleFunc("/{key}/id", func(rw http.ResponseWriter, r *http.Request) { 164 | rw.Header().Add("content-type", "text/html") 165 | rw.Write([]byte(h.realm.GenerateClientID())) 166 | }). 167 | Methods("GET").GetError() 168 | if err != nil { 169 | return err 170 | } 171 | 172 | // public API 173 | err = baseRoute. 174 | Path("/{key}/peers"). 175 | Handler(h.peersHandler()). 176 | Methods("GET").GetError() 177 | if err != nil { 178 | return err 179 | } 180 | 181 | // handle WS route 182 | err = baseRoute. 183 | Path("/peerjs"). 184 | Handler(h.auth.WSHandler(h.wss.Handler())). 185 | Methods("GET").GetError() 186 | if err != nil { 187 | return err 188 | } 189 | 190 | paths := []string{ 191 | "offer", 192 | "candidate", 193 | "answer", 194 | "leave", 195 | } 196 | 197 | for _, p := range paths { 198 | endpoint := fmt.Sprintf("/{key}/{id}/{token}/%s", p) 199 | err := baseRoute. 200 | Path(endpoint). 201 | Handler(h.auth.HTTPHandler(h.handler())). 202 | Methods("POST").GetError() 203 | if err != nil { 204 | return err 205 | } 206 | } 207 | 208 | return nil 209 | } 210 | 211 | // Start start the HTTP server 212 | func (h *HTTPServer) Start() error { 213 | 214 | err := h.registerHandlers() 215 | if err != nil { 216 | return err 217 | } 218 | 219 | c := cors.New(cors.Options{ 220 | AllowedOrigins: []string{"*"}, 221 | AllowCredentials: true, 222 | }) 223 | 224 | handler := c.Handler(h.router) 225 | 226 | h.http = &http.Server{ 227 | Addr: fmt.Sprintf("%s:%d", h.opts.Host, h.opts.Port), 228 | Handler: handler, 229 | ReadTimeout: 10 * time.Second, 230 | WriteTimeout: 10 * time.Second, 231 | MaxHeaderBytes: 1 << 20, 232 | } 233 | 234 | return h.http.ListenAndServe() 235 | } 236 | 237 | // Start start the HTTPS server 238 | func (h *HTTPServer) StartTLS(certFile, keyFile string) error { 239 | 240 | err := h.registerHandlers() 241 | if err != nil { 242 | return err 243 | } 244 | 245 | c := cors.New(cors.Options{ 246 | AllowedOrigins: []string{"*"}, 247 | AllowCredentials: true, 248 | }) 249 | 250 | handler := c.Handler(h.router) 251 | 252 | h.http = &http.Server{ 253 | Addr: fmt.Sprintf("%s:%d", h.opts.Host, h.opts.Port), 254 | Handler: handler, 255 | ReadTimeout: 10 * time.Second, 256 | WriteTimeout: 10 * time.Second, 257 | MaxHeaderBytes: 1 << 20, 258 | } 259 | 260 | return h.http.ListenAndServeTLS(certFile, keyFile) 261 | } 262 | 263 | // Stop stops the HTTP server 264 | func (h *HTTPServer) Stop() error { 265 | return h.http.Close() 266 | } 267 | -------------------------------------------------------------------------------- /server/http_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/muka/peerjs-go/enums" 12 | "github.com/muka/peerjs-go/models" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestHTTPServerGetID(t *testing.T) { 17 | opts := NewOptions() 18 | opts.Port = 64666 19 | opts.Host = "localhost" 20 | opts.AllowDiscovery = true 21 | 22 | getURL := func(path string) string { 23 | return fmt.Sprintf("http://%s:%d/%s%s", opts.Host, opts.Port, opts.Key, path) 24 | } 25 | 26 | realm := NewRealm() 27 | srv := NewHTTPServer(realm, NewAuth(realm, opts), nil, opts) 28 | 29 | go srv.Start() 30 | defer srv.Stop() 31 | // wait for server to start 32 | <-time.After(time.Millisecond * 200) 33 | 34 | resp, err := http.Get(getURL("/id")) 35 | assert.NoError(t, err) 36 | assert.NotNil(t, resp) 37 | assert.Equal(t, http.StatusOK, resp.StatusCode) 38 | 39 | resp, err = http.Get(getURL("/peers")) 40 | assert.NoError(t, err) 41 | assert.NotNil(t, resp) 42 | assert.Equal(t, http.StatusOK, resp.StatusCode) 43 | 44 | } 45 | func TestHTTPServerNoDiscovery(t *testing.T) { 46 | opts := NewOptions() 47 | opts.Port = 64666 48 | opts.Host = "localhost" 49 | opts.AllowDiscovery = false 50 | 51 | getURL := func(path string) string { 52 | return fmt.Sprintf("http://%s:%d/%s%s", opts.Host, opts.Port, opts.Key, path) 53 | } 54 | 55 | realm := NewRealm() 56 | srv := NewHTTPServer(realm, NewAuth(realm, opts), nil, opts) 57 | 58 | go srv.Start() 59 | defer srv.Stop() 60 | // wait for server to start 61 | <-time.After(time.Millisecond * 200) 62 | 63 | resp, err := http.Get(getURL("/peers")) 64 | assert.NoError(t, err) 65 | assert.NotNil(t, resp) 66 | assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 67 | 68 | } 69 | func TestHTTPServerExchange(t *testing.T) { 70 | opts := NewOptions() 71 | opts.Port = 64666 72 | opts.Host = "localhost" 73 | opts.AllowDiscovery = false 74 | 75 | id := "myid" 76 | token := "mytoken" 77 | 78 | getURL := func(path string) string { 79 | return fmt.Sprintf("http://%s:%d%s", opts.Host, opts.Port, path) 80 | } 81 | 82 | realm := NewRealm() 83 | auth := NewAuth(realm, opts) 84 | wss := NewWebSocketServer(realm, opts) 85 | srv := NewHTTPServer(realm, auth, wss, opts) 86 | 87 | go srv.Start() 88 | defer srv.Stop() 89 | // wait for server to start 90 | <-time.After(time.Millisecond * 200) 91 | 92 | msg := models.Message{ 93 | Type: enums.ServerMessageTypeOffer, 94 | Src: "foo", 95 | Dst: "bar", 96 | Payload: models.Payload{}, 97 | } 98 | 99 | raw, err := json.Marshal(msg) 100 | assert.NoError(t, err) 101 | url := getURL(fmt.Sprintf("/%s/%s/%s/offer", opts.Key, id, token)) 102 | 103 | // client not found 104 | resp, err := http.Post(url, "application/json", bytes.NewReader(raw)) 105 | assert.NoError(t, err) 106 | assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 107 | 108 | c := NewClient(id, token) 109 | srv.realm.SetClient(c, id) 110 | 111 | resp, err = http.Post(url, "application/json", bytes.NewReader(raw)) 112 | assert.NoError(t, err) 113 | assert.NotNil(t, resp) 114 | assert.Equal(t, http.StatusOK, resp.StatusCode) 115 | 116 | } 117 | -------------------------------------------------------------------------------- /server/message_expire.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/muka/peerjs-go/models" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | //IMessagesExpire MessagesExpire interface 12 | type IMessagesExpire interface { 13 | Start() 14 | Stop() 15 | } 16 | 17 | func NewMessagesExpire(realm IRealm, opts Options, messageHandler IMessageHandler) *MessagesExpire { 18 | return &MessagesExpire{ 19 | realm: realm, 20 | opts: opts, 21 | messageHandler: messageHandler, 22 | log: createLogger("messageExpire", opts), 23 | close: make(chan bool, 1), 24 | } 25 | } 26 | 27 | //MessagesExpire check for expired messages 28 | type MessagesExpire struct { 29 | realm IRealm 30 | opts Options 31 | messageHandler IMessageHandler 32 | ticker *time.Ticker 33 | log *logrus.Entry 34 | close chan bool 35 | } 36 | 37 | func (b *MessagesExpire) pruneOutstanding() { 38 | destinationClientsIds := b.realm.GetClientsIdsWithQueue() 39 | 40 | now := getTime() 41 | maxDiff := b.opts.ExpireTimeout 42 | 43 | seen := map[string]bool{} 44 | 45 | for _, destinationClientID := range destinationClientsIds { 46 | 47 | messageQueue := b.realm.GetMessageQueueByID(destinationClientID) 48 | if messageQueue == nil { 49 | continue 50 | } 51 | 52 | lastReadDiff := now - messageQueue.GetLastReadAt() 53 | if lastReadDiff < maxDiff { 54 | continue 55 | } 56 | 57 | messages := messageQueue.GetMessages() 58 | for _, message := range messages { 59 | seenKey := fmt.Sprintf("%s_%s", message.GetSrc(), message.GetDst()) 60 | 61 | if _, ok := seen[seenKey]; !ok { 62 | b.messageHandler.Handle(nil, models.Message{ 63 | Type: MessageTypeExpire, 64 | Src: message.GetDst(), 65 | Dst: message.GetSrc(), 66 | }) 67 | 68 | seen[seenKey] = true 69 | } 70 | } 71 | 72 | b.realm.ClearMessageQueue(destinationClientID) 73 | } 74 | } 75 | 76 | //Start the message expire check 77 | func (b *MessagesExpire) Start() { 78 | 79 | b.ticker = time.NewTicker(DefaultCheckInterval * time.Millisecond) 80 | 81 | go func() { 82 | for { 83 | select { 84 | case <-b.close: 85 | b.ticker.Stop() 86 | b.ticker = nil 87 | return 88 | case <-b.ticker.C: 89 | b.pruneOutstanding() 90 | } 91 | } 92 | }() 93 | 94 | } 95 | 96 | //Stop the message expire check 97 | func (b *MessagesExpire) Stop() { 98 | if b.ticker == nil { 99 | return 100 | } 101 | b.close <- true 102 | } 103 | -------------------------------------------------------------------------------- /server/message_handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/muka/peerjs-go/models" 5 | ) 6 | 7 | //IMessageHandler interface for MessageHandler 8 | type IMessageHandler interface { 9 | Handle(client IClient, message models.IMessage) bool 10 | } 11 | 12 | //NewMessageHandler creates a new MessageHandler 13 | func NewMessageHandler(realm IRealm, handlersRegistry IHandlersRegistry, opts Options) *MessageHandler { 14 | 15 | if handlersRegistry == nil { 16 | handlersRegistry = NewHandlersRegistry() 17 | } 18 | 19 | m := &MessageHandler{ 20 | realm: realm, 21 | handlersRegistry: handlersRegistry, 22 | } 23 | 24 | transmissionHandler := NewTransmissionHandler(realm, opts) 25 | heartbeatHandler := NewHeartbeatHandler(opts) 26 | 27 | handleHeartbeat := func(client IClient, message models.IMessage) bool { 28 | return heartbeatHandler(client, message) 29 | } 30 | 31 | handleTransmission := func(client IClient, message models.IMessage) bool { 32 | return transmissionHandler(client, message) 33 | } 34 | 35 | m.handlersRegistry.RegisterHandler(MessageTypeHeartbeat, handleHeartbeat) 36 | m.handlersRegistry.RegisterHandler(MessageTypeOffer, handleTransmission) 37 | m.handlersRegistry.RegisterHandler(MessageTypeAnswer, handleTransmission) 38 | m.handlersRegistry.RegisterHandler(MessageTypeCandidate, handleTransmission) 39 | m.handlersRegistry.RegisterHandler(MessageTypeLeave, handleTransmission) 40 | m.handlersRegistry.RegisterHandler(MessageTypeExpire, handleTransmission) 41 | 42 | return m 43 | } 44 | 45 | //MessageHandler wrap the message handler 46 | type MessageHandler struct { 47 | realm IRealm 48 | handlersRegistry IHandlersRegistry 49 | } 50 | 51 | //Handle handles a message 52 | func (m *MessageHandler) Handle(client IClient, message models.IMessage) bool { 53 | return m.handlersRegistry.Handle(client, message) 54 | } 55 | -------------------------------------------------------------------------------- /server/message_queue.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/muka/peerjs-go/models" 7 | ) 8 | 9 | // NewMessageQueue creates a new MessageQueue 10 | func NewMessageQueue() *MessageQueue { 11 | mq := new(MessageQueue) 12 | mq.lastReadAt = getTime() 13 | return mq 14 | } 15 | 16 | // IMessageQueue message queue interface 17 | type IMessageQueue interface { 18 | GetLastReadAt() int64 19 | AddMessage(message models.IMessage) 20 | ReadMessage() models.IMessage 21 | GetMessages() []models.IMessage 22 | } 23 | 24 | //MessageQueue type 25 | type MessageQueue struct { 26 | lastReadAt int64 27 | messages []models.IMessage 28 | mMutex sync.Mutex 29 | } 30 | 31 | //GetLastReadAt return last message read time 32 | func (mq *MessageQueue) GetLastReadAt() int64 { 33 | return mq.lastReadAt 34 | } 35 | 36 | //AddMessage add message to queue 37 | func (mq *MessageQueue) AddMessage(message models.IMessage) { 38 | mq.mMutex.Lock() 39 | defer mq.mMutex.Unlock() 40 | mq.messages = append(mq.messages, message) 41 | } 42 | 43 | //ReadMessage read last message 44 | func (mq *MessageQueue) ReadMessage() models.IMessage { 45 | if len(mq.messages) > 0 { 46 | mq.mMutex.Lock() 47 | defer mq.mMutex.Unlock() 48 | mq.lastReadAt = getTime() 49 | msg := mq.messages[0] 50 | mq.messages = mq.messages[1:] 51 | return msg 52 | } 53 | return nil 54 | } 55 | 56 | //GetMessages return all queued messages 57 | func (mq *MessageQueue) GetMessages() []models.IMessage { 58 | return mq.messages 59 | } 60 | -------------------------------------------------------------------------------- /server/realm.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/muka/peerjs-go/models" 7 | "github.com/muka/peerjs-go/util" 8 | ) 9 | 10 | // ClientIDGenerator default hash generator 11 | var ClientIDGenerator = func() string { 12 | return util.RandomToken() 13 | } 14 | 15 | // NewRealm creates a new Realm 16 | func NewRealm() *Realm { 17 | r := &Realm{ 18 | clients: map[string]IClient{}, 19 | messageQueues: map[string]IMessageQueue{}, 20 | cMutex: sync.Mutex{}, 21 | mMutex: sync.Mutex{}, 22 | } 23 | return r 24 | } 25 | 26 | //IRealm interface for Realm 27 | type IRealm interface { 28 | GetClientsIds() []string 29 | GetClientByID(clientID string) IClient 30 | GetClientsIdsWithQueue() []string 31 | SetClient(client IClient, id string) 32 | RemoveClientByID(id string) bool 33 | GetMessageQueueByID(id string) IMessageQueue 34 | AddMessageToQueue(id string, message models.IMessage) 35 | ClearMessageQueue(id string) 36 | GenerateClientID() string 37 | } 38 | 39 | // Realm implementation of a realm 40 | type Realm struct { 41 | clients map[string]IClient 42 | messageQueues map[string]IMessageQueue 43 | cMutex sync.Mutex 44 | mMutex sync.Mutex 45 | } 46 | 47 | //GetClientsIds return the list of client id 48 | func (r *Realm) GetClientsIds() []string { 49 | keys := []string{} 50 | for key := range r.clients { 51 | keys = append(keys, key) 52 | } 53 | return keys 54 | } 55 | 56 | //GetClientByID return client by id 57 | func (r *Realm) GetClientByID(clientID string) IClient { 58 | c, ok := r.clients[clientID] 59 | if !ok { 60 | return nil 61 | } 62 | return c 63 | } 64 | 65 | // GetClientsIdsWithQueue retur clients with queue 66 | func (r *Realm) GetClientsIdsWithQueue() []string { 67 | keys := []string{} 68 | for key := range r.messageQueues { 69 | keys = append(keys, key) 70 | } 71 | return keys 72 | } 73 | 74 | // SetClient set a client 75 | func (r *Realm) SetClient(client IClient, id string) { 76 | r.cMutex.Lock() 77 | defer r.cMutex.Unlock() 78 | r.clients[id] = client 79 | } 80 | 81 | //RemoveClientByID remove a client by id 82 | func (r *Realm) RemoveClientByID(id string) bool { 83 | client := r.GetClientByID(id) 84 | if client == nil { 85 | return false 86 | } 87 | r.cMutex.Lock() 88 | defer r.cMutex.Unlock() 89 | delete(r.clients, id) 90 | return true 91 | } 92 | 93 | // GetMessageQueueByID get message by queue id 94 | func (r *Realm) GetMessageQueueByID(id string) IMessageQueue { 95 | m, ok := r.messageQueues[id] 96 | if !ok { 97 | return nil 98 | } 99 | return m 100 | } 101 | 102 | // AddMessageToQueue add message to queue 103 | func (r *Realm) AddMessageToQueue(id string, message models.IMessage) { 104 | if r.GetMessageQueueByID(id) == nil { 105 | r.mMutex.Lock() 106 | r.messageQueues[id] = NewMessageQueue() 107 | r.mMutex.Unlock() 108 | } 109 | 110 | m := r.GetMessageQueueByID(id) 111 | if m != nil { 112 | m.AddMessage(message) 113 | } 114 | } 115 | 116 | // ClearMessageQueue clear message queue 117 | func (r *Realm) ClearMessageQueue(id string) { 118 | r.mMutex.Lock() 119 | defer r.mMutex.Unlock() 120 | delete(r.messageQueues, id) 121 | } 122 | 123 | // GenerateClientID generate a client id 124 | func (r *Realm) GenerateClientID() string { 125 | return ClientIDGenerator() 126 | } 127 | -------------------------------------------------------------------------------- /server/realm_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/muka/peerjs-go/models" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type xMessage struct{} 11 | 12 | func TestRealmClients(t *testing.T) { 13 | r := NewRealm() 14 | c0 := NewClient("1", "test") 15 | r.SetClient(c0, "1") 16 | c1 := r.GetClientByID("1") 17 | assert.Equal(t, c0, c1) 18 | } 19 | 20 | func TestRealmMessage(t *testing.T) { 21 | r := NewRealm() 22 | c0 := NewClient("1", "a") 23 | r.SetClient(c0, c0.GetID()) 24 | m0 := models.Message{} 25 | r.AddMessageToQueue("1", m0) 26 | c1 := r.GetClientByID("1") 27 | assert.Equal(t, c0, c1) 28 | } 29 | 30 | func TestRealmRandomID(t *testing.T) { 31 | r := NewRealm() 32 | assert.NotEqual(t, r.GenerateClientID(), r.GenerateClientID()) 33 | } 34 | 35 | func TestRealmQueueByID(t *testing.T) { 36 | r := NewRealm() 37 | c0 := NewClient("1", "a") 38 | r.SetClient(c0, c0.GetID()) 39 | m0 := models.Message{} 40 | r.AddMessageToQueue("1", m0) 41 | 42 | c1 := r.GetClientsIds() 43 | assert.Equal(t, len(c1), 1) 44 | 45 | c2 := r.GetClientsIdsWithQueue() 46 | assert.Equal(t, len(c2), 1) 47 | 48 | q := r.GetMessageQueueByID("1") 49 | assert.Equal(t, len(q.GetMessages()), 1) 50 | 51 | r.ClearMessageQueue("1") 52 | q = r.GetMessageQueueByID("1") 53 | assert.Nil(t, q) 54 | 55 | m1 := models.Message{} 56 | r.AddMessageToQueue("1", m1) 57 | q = r.GetMessageQueueByID("1") 58 | assert.NotNil(t, q) 59 | assert.NotZero(t, q.GetLastReadAt()) 60 | m2 := q.ReadMessage() 61 | assert.Equal(t, m1, m2) 62 | assert.NotZero(t, q.GetLastReadAt()) 63 | assert.Empty(t, q.ReadMessage()) 64 | 65 | } 66 | func TestRealmRemove(t *testing.T) { 67 | r := NewRealm() 68 | c0 := NewClient("1", "a") 69 | r.SetClient(c0, c0.GetID()) 70 | 71 | r.RemoveClientByID("1") 72 | r.RemoveClientByID("1") 73 | 74 | assert.Equal(t, len(r.GetClientsIds()), 0) 75 | } 76 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/muka/peerjs-go/emitter" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // New creates a new PeerServer 11 | func New(opts Options) *PeerServer { 12 | 13 | s := new(PeerServer) 14 | s.Emitter = emitter.NewEmitter() 15 | s.log = createLogger("peer", opts) 16 | s.realm = NewRealm() 17 | s.auth = NewAuth(s.realm, opts) 18 | s.wss = NewWebSocketServer(s.realm, opts) 19 | 20 | s.http = NewHTTPServer(s.realm, s.auth, s.wss, opts) 21 | 22 | s.checkBrokenConnections = NewCheckBrokenConnections( 23 | s.realm, 24 | opts, 25 | func(client IClient) { 26 | s.Emit("disconnect", client) 27 | }, 28 | ) 29 | 30 | s.messageExpire = NewMessagesExpire(s.realm, opts, s.http.messageHandler) 31 | 32 | s.initialize() 33 | 34 | return s 35 | } 36 | 37 | // PeerServer wrap the peer server functionalities 38 | type PeerServer struct { 39 | emitter.Emitter 40 | log *logrus.Entry 41 | http *HTTPServer 42 | realm IRealm 43 | auth *Auth 44 | wss *WebSocketServer 45 | checkBrokenConnections *CheckBrokenConnections 46 | messageExpire IMessagesExpire 47 | } 48 | 49 | func (p *PeerServer) initialize() { 50 | 51 | p.wss.On("connection", func(data interface{}) { 52 | client := data.(IClient) 53 | mq := p.realm.GetMessageQueueByID(client.GetID()) 54 | if mq != nil { 55 | for { 56 | message := mq.ReadMessage() 57 | if message == nil { 58 | break 59 | } 60 | p.http.messageHandler.Handle(client, message) 61 | } 62 | p.realm.ClearMessageQueue(client.GetID()) 63 | } 64 | p.Emit("connection", client) 65 | }) 66 | 67 | p.wss.On("message", func(data interface{}) { 68 | cm := data.(ClientMessage) 69 | p.Emit("message", cm) 70 | p.http.messageHandler.Handle(cm.Client, cm.Message) 71 | }) 72 | 73 | p.wss.On("close", func(data interface{}) { 74 | client := data.(IClient) 75 | p.Emit("disconnect", client) 76 | }) 77 | 78 | p.wss.On("error", func(data interface{}) { 79 | err := data.(error) 80 | p.Emit("error", err) 81 | }) 82 | 83 | p.messageExpire.Start() 84 | p.checkBrokenConnections.Start() 85 | } 86 | 87 | // Stop stops the peer server 88 | func (p *PeerServer) Stop() error { 89 | p.http.Stop() 90 | p.messageExpire.Stop() 91 | p.checkBrokenConnections.Stop() 92 | p.log.Info("Peer server stopped") 93 | return nil 94 | } 95 | 96 | // Start start the peer server 97 | func (p *PeerServer) Start() error { 98 | 99 | var err error 100 | go func() { 101 | err = p.http.Start() 102 | if err != nil { 103 | p.Emit("error", err) 104 | } 105 | }() 106 | 107 | <-time.After(time.Millisecond * 500) 108 | if err == nil { 109 | p.log.Infof("Peer server started (:%d)", p.http.opts.Port) 110 | } 111 | return err 112 | } 113 | 114 | // Start start the TLS peer server 115 | func (p *PeerServer) StartTLS(certFile, keyFile string) error { 116 | 117 | var err error 118 | go func() { 119 | err = p.http.StartTLS(certFile, keyFile) 120 | if err != nil { 121 | p.Emit("error", err) 122 | } 123 | }() 124 | 125 | <-time.After(time.Millisecond * 500) 126 | if err == nil { 127 | p.log.Infof("Peer server started (:%d)", p.http.opts.Port) 128 | } 129 | return err 130 | } 131 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "testing" 7 | "time" 8 | 9 | "github.com/muka/peerjs-go" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestPeerServer_StartStop(t *testing.T) { 14 | opts := NewOptions() 15 | opts.Port = 9001 16 | p := New(opts) 17 | 18 | var err error 19 | go func() { 20 | err = p.Start() 21 | }() 22 | 23 | <-time.After(time.Millisecond * 500) 24 | if err != nil { 25 | t.Logf("Start error: %v", err) 26 | assert.NoError(t, err) 27 | } 28 | err = p.Stop() 29 | assert.NoError(t, err) 30 | } 31 | 32 | func TestPeerServer_ClientPingPong(t *testing.T) { 33 | 34 | opts := NewOptions() 35 | opts.Port = 9001 36 | opts.Path = "/myapp" 37 | server := New(opts) 38 | defer server.Stop() 39 | 40 | peerOpts := peer.NewOptions() 41 | peerOpts.Host = opts.Host 42 | peerOpts.Port = opts.Port 43 | peerOpts.Path = opts.Path 44 | peerOpts.Secure = false 45 | 46 | var err error 47 | go func() { 48 | err = server.Start() 49 | }() 50 | 51 | <-time.After(time.Millisecond * 1000) 52 | 53 | if err != nil { 54 | t.FailNow() 55 | } 56 | 57 | peer1, err := peer.NewPeer("peer1", peerOpts) 58 | assert.NoError(t, err) 59 | assert.NotNil(t, peer1) 60 | if peer1 != nil { 61 | defer peer1.Close() 62 | } 63 | 64 | peer2, err := peer.NewPeer("peer2", peerOpts) 65 | assert.NoError(t, err) 66 | assert.NotNil(t, peer2) 67 | if peer2 != nil { 68 | defer peer2.Close() 69 | } 70 | 71 | done := make(chan error) 72 | peer2.On("connection", func(data interface{}) { 73 | conn2 := data.(*peer.DataConnection) 74 | conn2.On("data", func(data interface{}) { 75 | log.Printf("Received\n") 76 | done <- nil 77 | }) 78 | }) 79 | 80 | conn1, err := peer1.Connect("peer2", nil) 81 | assert.NoError(t, err) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | conn1.On("open", func(data interface{}) { 86 | conn1.Send([]byte("hello world"), false) 87 | }) 88 | 89 | go func() { 90 | <-time.After(time.Millisecond * 5000) 91 | done <- errors.New("Timeout") 92 | }() 93 | 94 | err = <-done 95 | if err != nil { 96 | t.Error(err) 97 | t.FailNow() 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /server/util.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // return time in millis 10 | // credits https://stackoverflow.com/questions/24122821/go-golang-time-now-unixnano-convert-to-milliseconds 11 | func getTime() int64 { 12 | return time.Now().UnixNano() / int64(time.Millisecond) 13 | } 14 | 15 | func createLogger(ctx string, opts Options) *logrus.Entry { 16 | logger := logrus.New() 17 | level, err := logrus.ParseLevel(opts.LogLevel) 18 | if err != nil { 19 | logger.Fatalf("Cannot parse log level %s", opts.LogLevel) 20 | } 21 | logger.SetLevel(level) 22 | 23 | return logger.WithFields(logrus.Fields{ 24 | "context": ctx, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /server/wss.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/muka/peerjs-go/emitter" 13 | "github.com/muka/peerjs-go/models" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // ClientMessage wrap a message received by a client 18 | type ClientMessage struct { 19 | Client IClient 20 | Message *models.Message 21 | } 22 | 23 | //NewWebSocketServer create a new WebSocketServer 24 | func NewWebSocketServer(realm IRealm, opts Options) *WebSocketServer { 25 | wss := WebSocketServer{ 26 | Emitter: emitter.NewEmitter(), 27 | upgrader: websocket.Upgrader{}, 28 | log: createLogger("websocket-server", opts), 29 | realm: realm, 30 | opts: opts, 31 | } 32 | 33 | wss.upgrader.CheckOrigin = func(r *http.Request) bool { 34 | // TODO: expose as options 35 | return true 36 | } 37 | 38 | return &wss 39 | } 40 | 41 | // WebSocketServer wrap the websocket server 42 | type WebSocketServer struct { 43 | emitter.Emitter 44 | upgrader websocket.Upgrader 45 | clients []*websocket.Conn 46 | cMutex sync.Mutex 47 | log *logrus.Entry 48 | realm IRealm 49 | opts Options 50 | } 51 | 52 | // Send send data to the clients 53 | func (wss *WebSocketServer) Send(data []byte) { 54 | for _, conn := range wss.clients { 55 | err := conn.WriteMessage(websocket.BinaryMessage, data) 56 | if err != nil { 57 | wss.log.Warnf("Write failed: %s", err) 58 | } 59 | } 60 | } 61 | 62 | //onSocketConnection called when a client connect 63 | func (wss *WebSocketServer) sendErrorAndClose(conn *websocket.Conn, msg string) error { 64 | err := conn.WriteJSON(models.Message{ 65 | Type: MessageTypeError, 66 | Payload: models.Payload{ 67 | Msg: msg, 68 | }, 69 | }) 70 | if err != nil { 71 | return err 72 | } 73 | err = conn.Close() 74 | if err != nil { 75 | return err 76 | } 77 | return nil 78 | } 79 | 80 | // 81 | func (wss *WebSocketServer) configureWS(conn *websocket.Conn, client IClient) error { 82 | client.SetSocket(conn) 83 | 84 | conn.SetPingHandler(func(appData string) error { 85 | // wss.log.Debugf("[%s] Ping received", client.GetID()) 86 | client.SetLastPing(getTime()) 87 | return nil 88 | }) 89 | 90 | closed := false 91 | conn.SetCloseHandler(func(code int, text string) error { 92 | // if any close error happens, stop the loop and remove the client 93 | wss.log.Debugf("Closed connection, cleaning up %s", client.GetID()) 94 | if client.GetSocket() == conn { 95 | wss.realm.RemoveClientByID(client.GetID()) 96 | } 97 | conn.Close() 98 | wss.Emit(WebsocketEventClose, client) 99 | closed = true 100 | return nil 101 | }) 102 | 103 | go func() { 104 | for { 105 | if closed { 106 | return 107 | } 108 | _, raw, err := conn.ReadMessage() 109 | if err != nil { 110 | wss.log.Errorf("[%s] Read WS error: %s", client.GetID(), err) 111 | return 112 | } 113 | 114 | // message handling 115 | data, err := ioutil.ReadAll(bytes.NewReader(raw)) 116 | if err != nil { 117 | wss.log.Errorf("client message read error: %s", err) 118 | wss.Emit(WebsocketEventError, err) 119 | continue 120 | } 121 | 122 | message := new(models.Message) 123 | err = json.Unmarshal(data, message) 124 | if err != nil { 125 | wss.log.Errorf("client message unmarshal error: %s", err) 126 | wss.Emit(WebsocketEventError, err) 127 | continue 128 | } 129 | 130 | message.Src = client.GetID() 131 | wss.Emit(WebsocketEventMessage, ClientMessage{client, message}) 132 | } 133 | }() 134 | 135 | wss.Emit(WebsocketEventConnection, client) 136 | return nil 137 | } 138 | 139 | //registerClient 140 | func (wss *WebSocketServer) registerClient(conn *websocket.Conn, id, token string) error { 141 | // Check concurrent limit 142 | clientsCount := len(wss.realm.GetClientsIds()) 143 | 144 | if clientsCount >= wss.opts.ConcurrentLimit { 145 | err := wss.sendErrorAndClose(conn, ErrorConnectionLimitExceeded) 146 | if err != nil { 147 | wss.log.Errorf("[sendErrorAndClose] Error: %s", err) 148 | } 149 | return nil 150 | } 151 | 152 | client := NewClient(id, token) 153 | wss.realm.SetClient(client, id) 154 | 155 | err := conn.WriteJSON(models.Message{Type: MessageTypeOpen}) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | err = wss.configureWS(conn, client) 161 | if err != nil { 162 | return err 163 | } 164 | return nil 165 | } 166 | 167 | //onSocketConnection called when a client connect 168 | func (wss *WebSocketServer) onSocketConnection(conn *websocket.Conn, r *http.Request) { 169 | query := r.URL.Query() 170 | id := query.Get("id") 171 | token := query.Get("token") 172 | key := query.Get("key") 173 | 174 | if id == "" || token == "" || key == "" { 175 | err := wss.sendErrorAndClose(conn, ErrorInvalidWSParameters) 176 | if err != nil { 177 | wss.log.Errorf("[sendErrorAndClose] Error: %s", err) 178 | } 179 | return 180 | } 181 | 182 | if key != wss.opts.Key { 183 | err := wss.sendErrorAndClose(conn, ErrorInvalidKey) 184 | if err != nil { 185 | wss.log.Errorf("[sendErrorAndClose] Error: %s", err) 186 | } 187 | return 188 | } 189 | 190 | client := wss.realm.GetClientByID(id) 191 | 192 | if client == nil { 193 | err := wss.registerClient(conn, id, token) 194 | if err != nil { 195 | wss.log.Errorf("[registerClient] Error: %s", err) 196 | } 197 | return 198 | } 199 | 200 | if token != client.GetToken() { 201 | // ID-taken, invalid token 202 | err := conn.WriteJSON(models.Message{ 203 | Type: MessageTypeIDTaken, 204 | Payload: models.Payload{ 205 | Msg: "ID is taken", 206 | }, 207 | }) 208 | if err != nil { 209 | wss.log.Errorf("[%s] Failed to write message: %s", MessageTypeIDTaken, err) 210 | } 211 | go func() { 212 | // wait for the client to receive the response message 213 | <-time.After(time.Millisecond * 100) 214 | conn.Close() 215 | }() 216 | return 217 | } 218 | 219 | wss.configureWS(conn, client) 220 | } 221 | 222 | // Handler expose the http handler for websocket 223 | func (wss *WebSocketServer) Handler() http.HandlerFunc { 224 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 225 | 226 | c, err := wss.upgrader.Upgrade(w, r, nil) 227 | if err != nil { 228 | wss.log.Warnf("upgrade error: %s", err) 229 | w.WriteHeader(500) 230 | // next.ServeHTTP(w, r) 231 | return 232 | } 233 | wss.onSocketConnection(c, r) 234 | }) 235 | } 236 | -------------------------------------------------------------------------------- /socket.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gorilla/websocket" 12 | "github.com/muka/peerjs-go/emitter" 13 | "github.com/muka/peerjs-go/enums" 14 | "github.com/muka/peerjs-go/models" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // SocketEvent carries an event from the socket 19 | type SocketEvent struct { 20 | Type string 21 | Message *models.Message 22 | Error error 23 | } 24 | 25 | // NewSocket create a socket instance 26 | func NewSocket(opts Options) *Socket { 27 | s := &Socket{ 28 | Emitter: emitter.NewEmitter(), 29 | log: createLogger("socket", opts.Debug), 30 | } 31 | s.opts = opts 32 | s.disconnected = true 33 | return s 34 | } 35 | 36 | // Socket abstract websocket exposing an event emitter like interface 37 | type Socket struct { 38 | emitter.Emitter 39 | id string 40 | opts Options 41 | baseURL string 42 | disconnected bool 43 | conn *websocket.Conn 44 | log *logrus.Entry 45 | mutex sync.Mutex 46 | wsPingTimer *time.Timer 47 | } 48 | 49 | func (s *Socket) buildBaseURL() string { 50 | proto := "ws" 51 | if s.opts.Secure { 52 | proto = "wss" 53 | } 54 | port := strconv.Itoa(s.opts.Port) 55 | 56 | path := s.opts.Path 57 | if path == "/" { 58 | path = "" 59 | } 60 | 61 | return fmt.Sprintf( 62 | "%s://%s:%s%s/peerjs?key=%s", 63 | proto, 64 | s.opts.Host, 65 | port, 66 | path, 67 | s.opts.Key, 68 | ) 69 | } 70 | 71 | func (s *Socket) scheduleHeartbeat() { 72 | s.wsPingTimer = time.AfterFunc(time.Millisecond*time.Duration(s.opts.PingInterval), func() { 73 | s.sendHeartbeat() 74 | }) 75 | } 76 | 77 | func (s *Socket) sendHeartbeat() { 78 | if s.conn == nil { 79 | s.log.Debug(`Cannot send heartbeat, because socket closed`) 80 | return 81 | } 82 | 83 | msg := models.Message{ 84 | Type: enums.ServerMessageTypeHeartbeat, 85 | } 86 | 87 | res, err := json.Marshal(msg) 88 | if err != nil { 89 | s.log.Errorf("sendHeartbeat: Failed to serialize message: %s", err) 90 | } 91 | 92 | // s.log.Debug("Send heartbeat") 93 | err = s.Send(res) 94 | if err != nil { 95 | s.log.Errorf("sendHeartbeat: Failed to send message: %s", err) 96 | return 97 | } 98 | 99 | s.scheduleHeartbeat() 100 | } 101 | 102 | // Start initiate the connection 103 | func (s *Socket) Start(id string, token string) error { 104 | 105 | if !s.disconnected { 106 | return nil 107 | } 108 | 109 | if s.baseURL == "" { 110 | s.baseURL = s.buildBaseURL() 111 | } 112 | 113 | url := s.baseURL + fmt.Sprintf("&id=%s&token=%s", id, token) 114 | s.log.Debugf("Connecting to %s", url) 115 | c, _, err := websocket.DefaultDialer.Dial(url, nil) 116 | if err != nil { 117 | return err 118 | } 119 | s.conn = c 120 | 121 | s.conn.SetCloseHandler(func(code int, text string) error { 122 | // s.log.Debug("WS closed") 123 | s.disconnected = true 124 | s.conn = nil 125 | return nil 126 | }) 127 | 128 | // ws ping by sending heartbeat message 129 | s.scheduleHeartbeat() 130 | 131 | // collect messages 132 | go func() { 133 | for { 134 | 135 | if s.conn == nil { 136 | s.log.Debug("WS connection unset, closing read go routine") 137 | return 138 | } 139 | 140 | msgType, raw, err := s.conn.ReadMessage() 141 | s.log.Debugf("WS msg %v", msgType) 142 | if err != nil { 143 | // catch close error, avoid panic reading a closed conn 144 | if _, ok := err.(*websocket.CloseError); ok { 145 | s.log.Debugf("websocket closed: %s", err) 146 | s.Emit(enums.SocketEventTypeDisconnected, SocketEvent{enums.SocketEventTypeDisconnected, nil, err}) 147 | return 148 | } else if opErr, ok := err.(*net.OpError); ok { 149 | s.log.Debugf("websocket closed: %s OpErr Op %s", opErr, opErr.Op) 150 | s.Emit(enums.SocketEventTypeDisconnected, SocketEvent{enums.SocketEventTypeDisconnected, nil, err}) 151 | return 152 | } 153 | s.log.Warnf("websocket read error: %s", err) 154 | continue 155 | } 156 | 157 | s.log.Infof("websocket message: %s", raw) 158 | 159 | if msgType == websocket.TextMessage { 160 | 161 | msg := models.Message{} 162 | err = json.Unmarshal(raw, &msg) 163 | if err != nil { 164 | s.log.Errorf("Failed to decode websocket message=%s %s", string(raw), err) 165 | } 166 | 167 | s.Emit(enums.SocketEventTypeMessage, SocketEvent{enums.SocketEventTypeMessage, &msg, err}) 168 | } else { 169 | s.log.Warnf("Unmanaged websocket message type %d", msgType) 170 | } 171 | 172 | } 173 | }() 174 | 175 | return nil 176 | } 177 | 178 | // Close close the websocket connection 179 | func (s *Socket) Close() error { 180 | if s.disconnected { 181 | return nil 182 | } 183 | err := s.conn.WriteMessage( 184 | websocket.CloseMessage, 185 | websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), 186 | ) 187 | if err != nil { 188 | s.log.Debugf("Failed to send close message: %s", err) 189 | } 190 | err = s.conn.Close() 191 | if err != nil { 192 | s.log.Warnf("WS close error: %s", err) 193 | } 194 | s.log.Debug("Closed websocket") 195 | s.disconnected = true 196 | s.conn = nil 197 | return err 198 | } 199 | 200 | // Send send a message 201 | func (s *Socket) Send(msg []byte) error { 202 | if s.conn == nil { 203 | return nil 204 | } 205 | s.mutex.Lock() 206 | defer s.mutex.Unlock() 207 | return s.conn.WriteMessage(websocket.TextMessage, msg) 208 | } 209 | -------------------------------------------------------------------------------- /socket_test.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | "time" 7 | 8 | "github.com/muka/peerjs-go/enums" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNewSocket(t *testing.T) { 13 | srv, srvOpts := startServer() 14 | srv.Start() 15 | defer srv.Stop() 16 | s := NewSocket(getTestOpts(srvOpts)) 17 | done := false 18 | s.On(enums.SocketEventTypeMessage, func(data interface{}) { 19 | ev := data.(SocketEvent) 20 | assert.Equal(t, ev.Type, enums.SocketEventTypeMessage) 21 | log.Println("socket received") 22 | done = true 23 | }) 24 | err := s.Start("test", "test") 25 | assert.NoError(t, err) 26 | err = s.Close() 27 | assert.NoError(t, err) 28 | <-time.After(time.Millisecond * 500) 29 | assert.True(t, done) 30 | } 31 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // Seed personal random source - faster and won't mess with global one 10 | var tokenRand = rand.New(rand.NewSource(time.Now().UnixNano())) 11 | 12 | const ( 13 | tokenChars = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 14 | // ChunkedMTU payload size for a single message 15 | ChunkedMTU = 16300 16 | ) 17 | 18 | func RandomToken() string { 19 | b := make([]byte, 11) // PeerJS random tokens are 11 chars long 20 | for i := range b { 21 | b[i] = tokenChars[tokenRand.Intn(len(tokenChars))] 22 | } 23 | return string(b) 24 | } 25 | 26 | //ChunckedData wraps a data slice with metadata to assemble back the whole data 27 | type ChunckedData struct { 28 | PeerData int `json:"__peerData"` 29 | N int `json:"n"` 30 | Total int `json:"total"` 31 | Data []byte `json:"data"` 32 | } 33 | 34 | //Chunk slices a data payload in a list of ChunckedData 35 | func Chunk(raw []byte) (chunks []ChunckedData) { 36 | s := slicer{ 37 | chunks: chunks, 38 | } 39 | return s.chunk(raw) 40 | } 41 | 42 | type slicer struct { 43 | dataCount int 44 | chunks []ChunckedData 45 | } 46 | 47 | func (s *slicer) chunk(raw []byte) []ChunckedData { 48 | size := len(raw) 49 | total := int(math.Ceil(float64(size) / ChunkedMTU)) 50 | index := 0 51 | start := 0 52 | 53 | for start < size { 54 | end := math.Min(float64(size), float64(start)+ChunkedMTU) 55 | b := raw[start:int(end)] 56 | 57 | chunk := ChunckedData{ 58 | PeerData: s.dataCount, 59 | N: index, 60 | Data: b, 61 | Total: total, 62 | } 63 | 64 | s.chunks = append(s.chunks, chunk) 65 | 66 | start = int(end) 67 | index++ 68 | } 69 | 70 | s.dataCount++ 71 | 72 | return s.chunks 73 | } 74 | -------------------------------------------------------------------------------- /util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestChunk(t *testing.T) { 11 | data := bytes.NewBuffer([]byte{}) 12 | for data.Len() < ChunkedMTU { 13 | data.Write([]byte("another piece to the chunk")) 14 | } 15 | chunks := Chunk(data.Bytes()) 16 | assert.NotEmpty(t, chunks) 17 | } 18 | --------------------------------------------------------------------------------