├── .gitignore ├── LICENSE ├── README.md ├── client └── client.go ├── examples ├── certs │ ├── cert.pem │ └── key.pem ├── dart │ ├── .gitignore │ ├── README.md │ ├── protoo_dart_client_test.dart │ └── pubspec.yaml ├── go │ ├── client │ │ └── main.go │ └── server │ │ └── main.go └── js │ ├── .babelrc │ ├── README.md │ ├── dist │ └── index.html │ ├── package.json │ ├── src │ └── index.js │ └── webpack.config.js ├── go.mod ├── go.sum ├── logger └── logger.go ├── peer ├── message.go └── peer.go ├── renovate.json ├── room └── room.go ├── server └── websocket_server.go └── transport └── websocket_transport.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 湖北捷智云技术有限公司 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-protoo 2 | 3 | ## How to test. 4 | ### Run go-protoo-server. 5 | ``` 6 | git clone https://github.com/cloudwebrtc/go-protoo 7 | cd go-protoo/examples/go 8 | go run server/main.go 9 | ``` 10 | ### Golang client test. 11 | ``` 12 | cd go-protoo/examples/go 13 | go run client/main.go 14 | ``` 15 | ### JS client test. 16 | ``` 17 | cd go-protoo/example/js 18 | npm i 19 | npm start 20 | ``` 21 | ### Dart client test. 22 | ``` 23 | cd go-protoo/example/dart 24 | pub get 25 | dart protoo_dart_client_test.dart 26 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/chuckpreslar/emission" 9 | "github.com/cloudwebrtc/go-protoo/logger" 10 | "github.com/cloudwebrtc/go-protoo/transport" 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | const pingPeriod = 5 * time.Second 15 | 16 | type WebSocketClient struct { 17 | emission.Emitter 18 | socket *websocket.Conn 19 | transport *transport.WebSocketTransport 20 | handleWebSocket func(ws *transport.WebSocketTransport) 21 | } 22 | 23 | func NewClient(url string, handleWebSocket func(ws *transport.WebSocketTransport)) *WebSocketClient { 24 | var client WebSocketClient 25 | client.Emitter = *emission.NewEmitter() 26 | logger.Infof("Connecting to %s", url) 27 | 28 | responseHeader := http.Header{} 29 | responseHeader.Add("Sec-WebSocket-Protocol", "protoo") 30 | 31 | // only for testing 32 | tls_cfg := &tls.Config{ 33 | InsecureSkipVerify: true, 34 | } 35 | 36 | dialer := websocket.Dialer{ 37 | Proxy: http.ProxyFromEnvironment, 38 | HandshakeTimeout: 45 * time.Second, 39 | TLSClientConfig: tls_cfg, 40 | } 41 | 42 | socket, _, err := dialer.Dial(url, responseHeader) 43 | if err != nil { 44 | logger.Errorf("Dial failed: %v", err) 45 | return nil 46 | } 47 | client.socket = socket 48 | client.handleWebSocket = handleWebSocket 49 | client.transport = transport.NewWebSocketTransport(socket) 50 | client.transport.Start() 51 | client.handleWebSocket(client.transport) 52 | return &client 53 | } 54 | 55 | func (client *WebSocketClient) GetTransport() *transport.WebSocketTransport { 56 | return client.transport 57 | } 58 | 59 | func (client *WebSocketClient) Close() { 60 | client.transport.Close() 61 | } 62 | -------------------------------------------------------------------------------- /examples/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDTCCAfWgAwIBAgIRAIxTJQ2vDIkg+GzuMEKURfAwDQYJKoZIhvcNAQELBQAw 3 | DzENMAsGA1UEChMEdGVzdDAeFw0xOTA0MTUxMzUxMjlaFw0yMDA0MTQxMzUxMjla 4 | MA8xDTALBgNVBAoTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 5 | AQDMjW1V8RifkyTLpQK+7s2nurIqqaDKk9y+d4T34GJB/PrI5gw8vsNehX+9WcNw 6 | XHc1wZga0uAuKeJW7aDOWc9H9yXcyjDrmi145zJKU8KI5KqG3iP6768h2ovQ/ynE 7 | Mgns3osfzkrmVa/Sb5YZ5wCyIgwn5HeyrCT9jT83ssW8AlUDGSDEV26It2HoeoPg 8 | emApKOy6OZajA3bop0/ve8r3ejcEA2WQmBXzD4xjwnyuJQlhdiKcB2VWEPfbDhyo 9 | aSz67DNEWWS2wvlimC16bwL9UOadhlZApXWOyqGqVwmj1sQ7LE2Pk+TmQ48ZrqZs 10 | jsOrHJlaNMLV59/qVlzvpWIrAgMBAAGjZDBiMA4GA1UdDwEB/wQEAwIFoDAdBgNV 11 | HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAjBgNVHREE 12 | HDAagglsb2NhbGhvc3SBDXRlc3RAdGVzdC5jb20wDQYJKoZIhvcNAQELBQADggEB 13 | AFkpWzMYVABmvsjy8ml3KfQAZTqRoidc0Qi9/8CNAlmviiDqDkoYS29LH+6Bp9bG 14 | 4TRCUTZU9VES/8gk0F8YKbOwboibvbs0MI6LsDmrg5CzruwuibyKZ52XNOoqYWGi 15 | I5vwqxwpLPZE0JQlungKOMUb+WWVHieThF4ONx5ZhFRA2iMl10redam39T6tOEks 16 | A3rOy/3FFWyHtnCezFjWb4iGLbedCUeO3MxrVfNC4QPGttPr+FpI1md+QbFCfMJI 17 | 8NLTakz/OrgFMerj+f9wFPUWdiM1RsP9fM3gB3FCvbqKitdeCXiPkbqfeDuQMxh0 18 | +ZQi1Bf5uLMYigdJALcUgRU= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /examples/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAzI1tVfEYn5Mky6UCvu7Np7qyKqmgypPcvneE9+BiQfz6yOYM 3 | PL7DXoV/vVnDcFx3NcGYGtLgLiniVu2gzlnPR/cl3Mow65oteOcySlPCiOSqht4j 4 | +u+vIdqL0P8pxDIJ7N6LH85K5lWv0m+WGecAsiIMJ+R3sqwk/Y0/N7LFvAJVAxkg 5 | xFduiLdh6HqD4HpgKSjsujmWowN26KdP73vK93o3BANlkJgV8w+MY8J8riUJYXYi 6 | nAdlVhD32w4cqGks+uwzRFlktsL5Ypgtem8C/VDmnYZWQKV1jsqhqlcJo9bEOyxN 7 | j5Pk5kOPGa6mbI7DqxyZWjTC1eff6lZc76ViKwIDAQABAoIBAQClqznNbFpsC8m2 8 | YevqZhEMcuoQIZiH2d/kJ3r7I7GRDmqzQZbRsBMdv/PokQX4P/uQ/z3JI9fLq8KU 9 | cxtVZWm7OJFi1CNw/ksr69xQVKxvIPx4BPyRJYTrX0z0NWVHcNuXC9sUJImHKgkf 10 | 355rViu7GlZMDP9hJ4lCDh8QZms9zBXSvFbxNL/0r6/yznPzZ07+Q3gnApiuaV3a 11 | HxD1hjZm5V4VCp1+gVqUZ2Gkwt3f2HgOI2fx/2E/e3mf0JQSadDfOSeh1ewy+yXt 12 | 5X1SAnD8QqtUZpeTX0tdS2OEAnOR7g7D46N6wHXdack4q6CCQXArxnPXk7CQ8PdS 13 | p0TJ9eexAoGBANLlZZUBLwIFEdFdKyJiQBKaDJTxwj8dn3Qean+giwzGgZ7Zg6b0 14 | TkINihpnUDTeUI6NhI/sqk6D4+yXEvvLmlR9Z1yUDB0wPaUOX7vPltz9xwZges5B 15 | V92Q2Ms25U5odMuD7PKU915aVJhq6al9a8tI4fF3wzuWWZNRdweabLApAoGBAPhM 16 | txUvroRXs4dqq8CJKkFDL1CII2J0axXAbY8zKLqKLaqy5lPC3Qyj7IC9Lj/BBZBe 17 | i3cA/cfZkO2FTX3EnI4MEAHJfCJ5xGK/4d2EUcFs9arI6ZPSzUnqFu6rjiGX7Z97 18 | HgCoIPeM08/XQ51xsgmjrdXwcSu0Zm0bI4YKDTozAoGBAK5zCQMndn4COYnoupx3 19 | jy6T9SJjlN1moJaJ7mhiIYNQ1LnUjV1WK0HiN0G+Ag+pchNpJqVzDDFkUe5LzW0x 20 | LxkCnrRq1lZyqBtXd4NApTdqSO6SciMY1Bi4D0yRvT6Nk0cheqfy52hJ6j4l+eC0 21 | ma0PPa7i9UDeDnyY35APgiZ5AoGBAJnZ7xeRk/Es5nDiB8olM6l8l7Supj47JJ81 22 | vK0eZf4v4FwEvfLoq09JwyGCF0Yps+jgrEnws09B4CtMjdeAHAFoOALjdadnC+ik 23 | PK+lbYMMoKxSEWknFUzXX55Dbq4iFXrcLpr5knPRLu151MuZx1dxtCT79yO9hDnW 24 | tww27jtBAoGAGXIlsMu9pXBG9Aw4mHl+c5lumeDKrSzyywoo5gUwOH8UgoH2YWsP 25 | BObdknWIWq9wYkGelm0x2csW+Ghp18aUA2+Grl+2R6cftUmzJTNNSxqc1eBHpIAh 26 | FYeel4jFXN6DMGkXQv6AuIsppNqPWQNmLYE7LnunCMOwsEp8WvJiL+8= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/dart/.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | -------------------------------------------------------------------------------- /examples/dart/README.md: -------------------------------------------------------------------------------- 1 | A sample command-line application. 2 | 3 | ## Usage 4 | 5 | ``` 6 | pub get 7 | dart protoo_dart_client_test.dart 8 | ``` 9 | -------------------------------------------------------------------------------- /examples/dart/protoo_dart_client_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:protoo_client/protoo_client.dart'; 2 | 3 | main() async { 4 | 5 | Peer peer = new Peer('https://127.0.0.1:8443/ws?peer=dart-client-id-xxxx'); 6 | 7 | peer.on('open', () { 8 | 9 | print('open'); 10 | 11 | peer.send('login', {"username":"alice","password":"alicespass"}).then((data) { 12 | print('response: ' + data.toString()); 13 | }).catchError((error) { 14 | print('response error: ' + error.toString()); 15 | }); 16 | 17 | peer.send('offer', {'sdp':'empty'}).then((data) { 18 | print('response: ' + data.toString()); 19 | }).catchError((error) { 20 | print('response error: ' + error.toString()); 21 | }); 22 | }); 23 | 24 | peer.on('close', () { 25 | print('close'); 26 | }); 27 | 28 | peer.on('error', (error) { 29 | print('error ' + error); 30 | }); 31 | 32 | peer.on('request', (request, accept, reject) { 33 | print('request: ' + request.toString()); 34 | if(request['method'] == 'kick') 35 | reject(486, 'Busy Here'); 36 | else 37 | accept({}); 38 | }); 39 | 40 | await peer.connect(); 41 | 42 | //peer.close(); 43 | } 44 | 45 | -------------------------------------------------------------------------------- /examples/dart/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart 2 | description: A sample command-line application. 3 | # version: 1.0.0 4 | # homepage: https://www.example.com 5 | # author: 湖北捷智云技术有限公司 6 | 7 | environment: 8 | sdk: '>=2.2.0 <3.0.0' 9 | 10 | dependencies: 11 | protoo_client: 12 | git: https://github.com/cloudwebrtc/dart-protoo-client 13 | 14 | #dev_dependencies: 15 | # pedantic: ^1.0.0 16 | # test: ^1.0.0 17 | -------------------------------------------------------------------------------- /examples/go/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | "github.com/cloudwebrtc/go-protoo/client" 8 | "github.com/cloudwebrtc/go-protoo/logger" 9 | "github.com/cloudwebrtc/go-protoo/peer" 10 | "github.com/cloudwebrtc/go-protoo/transport" 11 | ) 12 | 13 | var peerId = "go-client-id-xxxx" 14 | 15 | func handleWebSocketOpen(con *transport.WebSocketTransport) { 16 | logger.Infof("handleWebSocketOpen") 17 | 18 | pr := peer.NewPeer(peerId, con) 19 | 20 | handleRequest := func(request peer.Request, accept peer.RespondFunc, reject peer.RejectFunc) { 21 | method := request.Method 22 | logger.Infof("handleRequest => (%s) ", method) 23 | if method == "kick" { 24 | reject(486, "Busy Here") 25 | } else { 26 | accept(nil) 27 | } 28 | } 29 | 30 | handleNotification := func(notification peer.Notification) { 31 | logger.Infof("handleNotification => %s", notification.Method) 32 | } 33 | 34 | handleClose := func(err transport.TransportErr) { 35 | logger.Infof("handleClose => peer (%s) [%d] %s", pr.ID(), err.Code, err.Text) 36 | } 37 | 38 | go func() { 39 | for { 40 | select { 41 | case msg := <-pr.OnNotification: 42 | log.Println(msg) 43 | handleNotification(msg) 44 | case msg := <-pr.OnRequest: 45 | handleRequest(msg.Request, msg.Accept, msg.Reject) 46 | case msg := <-pr.OnClose: 47 | handleClose(msg) 48 | } 49 | } 50 | }() 51 | 52 | pr.Request("login", json.RawMessage(`{"username":"alice","password":"alicespass"}`), 53 | func(result json.RawMessage) { 54 | logger.Infof("login success: => %s", result) 55 | }, 56 | func(code int, err string) { 57 | logger.Infof("login reject: %d => %s", code, err) 58 | }) 59 | pr.Request("offer", json.RawMessage(`{"sdp":"empty"}`), 60 | func(result json.RawMessage) { 61 | logger.Infof("offer success: => %s", result) 62 | }, 63 | func(code int, err string) { 64 | logger.Infof("offer reject: %d => %s", code, err) 65 | }) 66 | /* 67 | pr.Request("join", json.RawMessage(`{"client":"aaa", "type":"sender"}`), 68 | func(result json.RawMessage) { 69 | logger.Infof("join success: => %s", result) 70 | }, 71 | func(code int, err string) { 72 | logger.Infof("join reject: %d => %s", code, err) 73 | }) 74 | pr.Request("publish", json.RawMessage(`{"type":"sender", "jsep":{"type":"offer", "sdp":"111111111111111"}}`), 75 | func(result json.RawMessage) { 76 | logger.Infof("publish success: => %s", result) 77 | }, 78 | func(code int, err string) { 79 | logger.Infof("publish reject: %d => %s", code, err) 80 | }) 81 | */ 82 | } 83 | 84 | func main() { 85 | client.NewClient("ws://127.0.0.1:9090/ws?peer="+peerId, handleWebSocketOpen) 86 | for { 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/go/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/cloudwebrtc/go-protoo/logger" 9 | "github.com/cloudwebrtc/go-protoo/peer" 10 | "github.com/cloudwebrtc/go-protoo/room" 11 | "github.com/cloudwebrtc/go-protoo/server" 12 | "github.com/cloudwebrtc/go-protoo/transport" 13 | ) 14 | 15 | var testRoom *room.Room 16 | 17 | type KickMsg struct { 18 | Reason string `json:"reason"` 19 | } 20 | 21 | type LoginMsg struct { 22 | Status string `json:"status"` 23 | } 24 | 25 | type Request struct { 26 | Method string 27 | Data json.RawMessage 28 | } 29 | 30 | type LoginData struct { 31 | Username string 32 | Password string 33 | } 34 | 35 | type OfferData struct { 36 | Sdp string `json:"sdp"` 37 | } 38 | 39 | func handleNewWebSocket(transport *transport.WebSocketTransport, request *http.Request) { 40 | log.Println("Handle socket") 41 | 42 | //https://127.0.0.1:8443/ws?peer=alice 43 | vars := request.URL.Query() 44 | peerId := vars["peer"][0] 45 | 46 | logger.Infof("handleNewWebSocket peerId => (%s)", peerId) 47 | 48 | pr := testRoom.CreatePeer(peerId, transport) 49 | 50 | handleRequest := func(request peer.Request, accept peer.RespondFunc, reject peer.RejectFunc) { 51 | method := request.Method 52 | 53 | /*handle login and offer request*/ 54 | if method == "login" { 55 | var data LoginData 56 | if err := json.Unmarshal(request.Data, &data); err != nil { 57 | log.Fatal("Marshal error") 58 | } 59 | username := data.Username 60 | password := data.Password 61 | logger.Infof("Handle login username => %s, password => %s", username, password) 62 | accept(LoginMsg{Status: "login success!"}) 63 | } else if method == "offer" { 64 | var data OfferData 65 | if err := json.Unmarshal(request.Data, &data); err != nil { 66 | log.Fatal("Marshal error") 67 | } 68 | sdp := data.Sdp 69 | logger.Infof("Handle offer sdp => %s", sdp) 70 | if sdp == "empty" { 71 | reject(500, "sdp error!") 72 | } else { 73 | accept(nil) 74 | } 75 | } 76 | 77 | /*send `kick` request to peer*/ 78 | pr.Request("kick", KickMsg{Reason: "go away!"}, 79 | func(result json.RawMessage) { 80 | logger.Infof("kick success: => %s", result) 81 | // close transport 82 | pr.Close() 83 | }, 84 | func(code int, err string) { 85 | logger.Infof("kick reject: %d => %s", code, err) 86 | }) 87 | } 88 | 89 | handleNotification := func(notification map[string]interface{}) { 90 | logger.Infof("handleNotification => %s", notification["method"]) 91 | 92 | method := notification["method"].(string) 93 | data := notification["data"].(map[string]interface{}) 94 | 95 | //Forward notification to testRoom. 96 | testRoom.Notify(pr, method, data) 97 | } 98 | 99 | handleClose := func(code int, err string) { 100 | logger.Infof("handleClose => peer (%s) [%d] %s", pr.ID(), code, err) 101 | } 102 | 103 | _, _, _ = handleRequest, handleNotification, handleClose 104 | 105 | for { 106 | select { 107 | case msg := <-pr.OnNotification: 108 | log.Println("OnNotification msg", msg) 109 | // handleNotification 110 | case msg := <-pr.OnRequest: 111 | handleRequest(msg.Request, msg.Accept, msg.Reject) 112 | // log.Println(msg) 113 | case msg := <-pr.OnClose: 114 | log.Println("Close msg", msg) 115 | } 116 | } 117 | } 118 | 119 | func main() { 120 | testRoom = room.NewRoom("room1") 121 | protooServer := server.NewWebSocketServer(handleNewWebSocket) 122 | config := server.DefaultConfig() 123 | config.Port = 9090 124 | // config.CertFile = "../certs/cert.pem" 125 | // config.KeyFile = "../certs/key.pem" 126 | protooServer.Bind(config) 127 | } 128 | -------------------------------------------------------------------------------- /examples/js/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } -------------------------------------------------------------------------------- /examples/js/README.md: -------------------------------------------------------------------------------- 1 | # go-protoo-server-js-demo 2 | -------------------------------------------------------------------------------- /examples/js/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | protoo client test 7 | 8 | 9 | 10 | 11 |
12 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-go-protoo-client-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "webpack --mode=production --config webpack.config.js", 8 | "start": "webpack-dev-server --config ./webpack.config.js --mode development --open --https --cert ../certs/cert.pem --key ../certs/key.pem", 9 | "test": "echo \"No test specified\" && exit 0" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@babel/core": "^7.4.3", 16 | "@babel/preset-env": "^7.4.3", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "webpack": "^4.30.0", 20 | "webpack-cli": "^3.3.1", 21 | "webpack-dev-server": "^3.3.1" 22 | }, 23 | "dependencies": { 24 | "events": "^3.0.0", 25 | "protoo-client": "^4.0.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/js/src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventEmitter 3 | } from 'events'; 4 | import protooClient from 'protoo-client'; 5 | 6 | export default class ProtooClientTest extends EventEmitter { 7 | 8 | constructor(url) { 9 | super() 10 | let transport = new protooClient.WebSocketTransport(url); 11 | // protoo-client Peer instance. 12 | this._protooPeer = new protooClient.Peer(transport); 13 | this._protooPeer.on('open', () => { 14 | console.log('Peer "open" event'); 15 | this._login(); 16 | }); 17 | 18 | this._protooPeer.on('disconnected', () => { 19 | console.log('protoo Peer "disconnected" event'); 20 | }); 21 | 22 | this._protooPeer.on('close', () => { 23 | console.log('protoo Peer "close" event'); 24 | }); 25 | 26 | this._protooPeer.on('request', this._handleRequest.bind(this)); 27 | 28 | } 29 | 30 | _login() { 31 | this._protooPeer.request('login', { username: 'alice',password: 'alicespass'}).then((data) => { 32 | console.log('login success: result => ' + JSON.stringify(data)); 33 | alert('login success, send offer now !'); 34 | this._offer(); 35 | }).catch((error) => { 36 | console.log('login reject: error =>' + JSON.stringify(error)); 37 | }); 38 | } 39 | 40 | _offer() { 41 | this._protooPeer.request('offer', { 'sdp': 'empty'}).then((data) => { 42 | console.log('offer success: result => ' + JSON.stringify(data)); 43 | }).catch((error) => { 44 | alert('offer request failed, error => '+ JSON.stringify(error)); 45 | console.log('offer reject: error => ' + JSON.stringify(error)); 46 | }); 47 | } 48 | 49 | _handleRequest(request, accept, reject) { 50 | console.log('_handleRequest() [method:%s, data:%o]', request.method, request.data); 51 | alert('Handle request ' + request.method); 52 | switch (request.method) { 53 | case 'kick': { 54 | reject(486, 'Busy Here') 55 | } 56 | break; 57 | default: 58 | console.log('other method =' + request.method); 59 | } 60 | } 61 | } 62 | 63 | window.ProtooClientTest = ProtooClientTest 64 | -------------------------------------------------------------------------------- /examples/js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.(js|jsx)$/, 9 | exclude: /node_modules/, 10 | use: ['babel-loader'] 11 | } 12 | ] 13 | }, 14 | resolve: { 15 | extensions: ['*', '.js', '.jsx'] 16 | }, 17 | output: { 18 | path: __dirname + '/dist', 19 | publicPath: '/', 20 | filename: 'bundle.js' 21 | }, 22 | plugins: [ 23 | new webpack.HotModuleReplacementPlugin() 24 | ], 25 | devServer: { 26 | contentBase: './dist', 27 | hot: true, 28 | host: '0.0.0.0' 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudwebrtc/go-protoo 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 7 | github.com/gorilla/websocket v1.4.1 8 | github.com/pion/ion-log v1.0.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4= 2 | github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9/go.mod h1:2wSM9zJkl1UQEFZgSd68NfCgRz1VL1jzy/RjCg+ULrs= 3 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 4 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 5 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 6 | github.com/pion/ion-log v1.0.0 h1:2lJLImCmfCWCR38hLWsjQfBWe6NFz/htbqiYHwvOP/Q= 7 | github.com/pion/ion-log v1.0.0/go.mod h1:jwcla9KoB9bB/4FxYDSRJPcPYSLp5XiUUMnOLaqwl4E= 8 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 10 | github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= 11 | github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 14 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 18 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 19 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | log "github.com/pion/ion-log" 5 | ) 6 | 7 | func init() { 8 | fixByFile := []string{"asm_amd64.s", "proc.go"} 9 | fixByFunc := []string{} 10 | log.Init("debug", fixByFile, fixByFunc) 11 | } 12 | 13 | func Infof(format string, v ...interface{}) { 14 | log.Infof(format, v...) 15 | } 16 | 17 | func Debugf(format string, v ...interface{}) { 18 | log.Debugf(format, v...) 19 | } 20 | 21 | func Warnf(format string, v ...interface{}) { 22 | log.Warnf(format, v...) 23 | } 24 | 25 | func Errorf(format string, v ...interface{}) { 26 | log.Errorf(format, v...) 27 | } 28 | -------------------------------------------------------------------------------- /peer/message.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "encoding/json" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | type RespondFunc func(data interface{}) 10 | type AcceptFunc func(data json.RawMessage) 11 | type RejectFunc func(errorCode int, errorReason string) 12 | 13 | type PeerMsg struct { 14 | Request bool `json:"request"` 15 | Response bool `json:"response"` 16 | Ok bool `json:"ok"` 17 | Notification bool `json:"notification"` 18 | } 19 | 20 | /* 21 | * Request 22 | { 23 | request : true, 24 | id : 12345678, 25 | method : 'chatmessage', 26 | data : 27 | { 28 | type : 'text', 29 | value : 'Hi there!' 30 | } 31 | } 32 | */ 33 | type Request struct { 34 | Request bool `json:"request"` 35 | Id int `json:"id"` 36 | Method string `json:"method"` 37 | Data json.RawMessage `json:"data"` 38 | } 39 | 40 | /* 41 | * Success response 42 | { 43 | response : true, 44 | id : 12345678, 45 | ok : true, 46 | data : 47 | { 48 | foo : 'lalala' 49 | } 50 | } 51 | */ 52 | type Response struct { 53 | Response bool `json:"response"` 54 | Id int `json:"id"` 55 | Ok bool `json:"ok"` 56 | Data json.RawMessage `json:"data"` 57 | } 58 | 59 | /* 60 | * Error response 61 | { 62 | response : true, 63 | id : 12345678, 64 | ok : false, 65 | errorCode : 123, 66 | errorReason : 'Something failed' 67 | } 68 | */ 69 | type ResponseError struct { 70 | Response bool `json:"response"` 71 | Id int `json:"id"` 72 | Ok bool `json:"ok"` 73 | ErrorCode int `json:"errorCode"` 74 | ErrorReason string `json:"errorReason"` 75 | } 76 | 77 | /* 78 | * Notification 79 | { 80 | notification : true, 81 | method : 'chatmessage', 82 | data : 83 | { 84 | foo : 'bar' 85 | } 86 | } 87 | */ 88 | type Notification struct { 89 | Notification bool `json:"notification"` 90 | Method string `json:"method"` 91 | Data json.RawMessage `json:"data"` 92 | } 93 | 94 | func RandInt(min, max int) int { 95 | rand.Seed(time.Now().UnixNano()) 96 | if min >= max || min == 0 || max == 0 { 97 | return max 98 | } 99 | return rand.Intn(max-min) + min 100 | } 101 | 102 | func GenerateRandomNumber() int { 103 | return RandInt(1000000, 9999999) 104 | } 105 | -------------------------------------------------------------------------------- /peer/peer.go: -------------------------------------------------------------------------------- 1 | package peer 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/cloudwebrtc/go-protoo/logger" 7 | "github.com/cloudwebrtc/go-protoo/transport" 8 | ) 9 | 10 | type PeerErr transport.TransportErr 11 | 12 | type Transcation struct { 13 | id int 14 | accept AcceptFunc 15 | reject RejectFunc 16 | close func() 17 | resultChan chan ResultFuture 18 | } 19 | 20 | type RequestData struct { 21 | Accept RespondFunc 22 | Reject RejectFunc 23 | Request Request 24 | } 25 | 26 | type SendRequestData struct { 27 | *Request 28 | *Transcation 29 | } 30 | 31 | type PeerChans struct { 32 | OnRequest chan RequestData 33 | SendRequest chan SendRequestData 34 | OnNotification chan Notification 35 | OnClose chan transport.TransportErr 36 | OnError chan transport.TransportErr 37 | } 38 | 39 | type Peer struct { 40 | PeerChans 41 | id string 42 | transport *transport.WebSocketTransport 43 | transcations map[int]*Transcation 44 | } 45 | 46 | func NewPeer(id string, con *transport.WebSocketTransport) *Peer { 47 | var peer Peer 48 | peer.id = id 49 | peer.transport = con 50 | peer.PeerChans = PeerChans{ 51 | OnRequest: make(chan RequestData, 100), 52 | OnNotification: make(chan Notification, 100), 53 | SendRequest: make(chan SendRequestData, 100), 54 | OnClose: make(chan transport.TransportErr, 1), 55 | } 56 | peer.transcations = make(map[int]*Transcation) 57 | 58 | go peer.Run() 59 | return &peer 60 | } 61 | 62 | func (peer *Peer) Run() { 63 | for { 64 | select { 65 | case msg := <-peer.transport.OnMsg: 66 | peer.handleMessage(msg) 67 | case err := <-peer.transport.OnErr: 68 | peer.handleErr(err) 69 | case err := <-peer.transport.OnClose: 70 | peer.handleClose(err) 71 | case data := <-peer.SendRequest: 72 | peer.sendRequest(data) 73 | } 74 | } 75 | } 76 | 77 | func (peer *Peer) sendRequest(req SendRequestData) { 78 | request := req.Request 79 | 80 | str, err := json.Marshal(request) 81 | if err != nil { 82 | logger.Errorf("Marshal %v", err) 83 | return 84 | } 85 | 86 | peer.transcations[request.Id] = req.Transcation 87 | logger.Infof("Send request [%s]", request.Method) 88 | peer.transport.SendCh <- str 89 | } 90 | 91 | func (peer *Peer) handleClose(err transport.TransportErr) { 92 | logger.Infof("Transport closed [%d] %s", err.Code, err.Text) 93 | peer.OnClose <- err 94 | } 95 | 96 | func (peer *Peer) handleErr(err transport.TransportErr) { 97 | logger.Warnf("Transport got error (%d, %s)", err.Code, err.Text) 98 | // Transport error triggers close currently 99 | } 100 | 101 | func (peer *Peer) Close() { 102 | peer.transport.Close() 103 | } 104 | 105 | func (peer *Peer) ID() string { 106 | return peer.id 107 | } 108 | 109 | type ResultFuture struct { 110 | Result json.RawMessage 111 | Err *PeerErr 112 | } 113 | 114 | func (peer *Peer) Request(method string, data interface{}, success AcceptFunc, reject RejectFunc) chan ResultFuture { 115 | id := GenerateRandomNumber() 116 | resChan := make(chan ResultFuture, 1) 117 | dataStr, err := json.Marshal(data) 118 | if err != nil { 119 | logger.Errorf("Marshal data %v", err) 120 | resChan <- ResultFuture{nil, &PeerErr{10, err.Error()}} 121 | return resChan 122 | } 123 | 124 | request := &Request{ 125 | Request: true, 126 | Id: id, 127 | Method: method, 128 | Data: dataStr, 129 | } 130 | 131 | transcation := &Transcation{ 132 | id: id, 133 | accept: success, 134 | reject: reject, 135 | close: func() { 136 | logger.Infof("Transport closed !") 137 | }, 138 | resultChan: resChan, 139 | } 140 | 141 | peer.SendRequest <- SendRequestData{request, transcation} 142 | return resChan 143 | } 144 | 145 | func (peer *Peer) Notify(method string, data interface{}) { 146 | dataStr, err := json.Marshal(data) 147 | if err != nil { 148 | logger.Errorf("Marshal data %v", err) 149 | return 150 | } 151 | notification := &Notification{ 152 | Notification: true, 153 | Method: method, 154 | Data: dataStr, 155 | } 156 | str, err := json.Marshal(notification) 157 | if err != nil { 158 | logger.Errorf("Marshal %v", err) 159 | return 160 | } 161 | logger.Infof("Send notification [%s]", method) 162 | peer.transport.SendCh <- str 163 | } 164 | 165 | func (peer *Peer) handleMessage(message []byte) { 166 | var msg PeerMsg 167 | if err := json.Unmarshal(message, &msg); err != nil { 168 | logger.Errorf("Marshal %v", err) 169 | return 170 | } 171 | if msg.Request { 172 | var data Request 173 | if err := json.Unmarshal(message, &data); err != nil { 174 | logger.Errorf("Request Marshal %v", err) 175 | return 176 | } 177 | peer.handleRequest(data) 178 | } else if msg.Response { 179 | if msg.Ok { 180 | var data Response 181 | if err := json.Unmarshal(message, &data); err != nil { 182 | logger.Errorf("Response Marshal %v", err) 183 | return 184 | } 185 | peer.handleResponse(data) 186 | } else { 187 | var data ResponseError 188 | if err := json.Unmarshal(message, &data); err != nil { 189 | logger.Errorf("ResponseError Marshal %v", err) 190 | return 191 | } 192 | peer.handleResponseError(data) 193 | } 194 | } else if msg.Notification { 195 | var data Notification 196 | if err := json.Unmarshal(message, &data); err != nil { 197 | logger.Errorf("Notification Marshal %v", err) 198 | return 199 | } 200 | peer.handleNotification(data) 201 | } 202 | } 203 | 204 | func (peer *Peer) handleRequest(request Request) { 205 | 206 | logger.Infof("Handle request [%s]", request.Method) 207 | 208 | accept := func(data interface{}) { 209 | dataStr, err := json.Marshal(data) 210 | if err != nil { 211 | logger.Errorf("Marshal %v", err) 212 | return 213 | } 214 | response := &Response{ 215 | Response: true, 216 | Ok: true, 217 | Id: request.Id, 218 | Data: dataStr, 219 | } 220 | str, err := json.Marshal(response) 221 | if err != nil { 222 | logger.Errorf("Marshal %v", err) 223 | return 224 | } 225 | //send accept 226 | logger.Infof("Accept [%s] => (%s)", request.Method, str) 227 | peer.transport.SendCh <- str 228 | } 229 | 230 | reject := func(errorCode int, errorReason string) { 231 | response := &ResponseError{ 232 | Response: true, 233 | Ok: false, 234 | Id: request.Id, 235 | ErrorCode: errorCode, 236 | ErrorReason: errorReason, 237 | } 238 | str, err := json.Marshal(response) 239 | if err != nil { 240 | logger.Errorf("Marshal %v", err) 241 | return 242 | } 243 | //send reject 244 | logger.Infof("Reject [%s] => (errorCode:%d, errorReason:%s)", request.Method, errorCode, errorReason) 245 | peer.transport.SendCh <- str 246 | } 247 | _, _ = accept, reject 248 | peer.OnRequest <- RequestData{ 249 | Accept: accept, 250 | Reject: reject, 251 | Request: request, 252 | } 253 | } 254 | 255 | func (peer *Peer) handleResponse(response Response) { 256 | id := response.Id 257 | transcation := peer.transcations[id] 258 | if transcation == nil { 259 | logger.Errorf("received response does not match any sent request [id:%d]", id) 260 | return 261 | } 262 | 263 | if transcation.accept != nil { 264 | transcation.accept(response.Data) 265 | } else { 266 | transcation.resultChan <- ResultFuture{response.Data, nil} 267 | } 268 | 269 | delete(peer.transcations, id) 270 | } 271 | 272 | func (peer *Peer) handleResponseError(response ResponseError) { 273 | id := response.Id 274 | transcation := peer.transcations[id] 275 | if transcation == nil { 276 | logger.Errorf("received response does not match any sent request [id:%d]", id) 277 | return 278 | } 279 | 280 | if transcation.reject != nil { 281 | transcation.reject(response.ErrorCode, response.ErrorReason) 282 | } else { 283 | transcation.resultChan <- ResultFuture{nil, &PeerErr{ 284 | Code: response.ErrorCode, 285 | Text: response.ErrorReason, 286 | }} 287 | } 288 | 289 | delete(peer.transcations, id) 290 | } 291 | 292 | func (peer *Peer) handleNotification(notification Notification) { 293 | peer.OnNotification <- notification 294 | } 295 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /room/room.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/cloudwebrtc/go-protoo/logger" 7 | "github.com/cloudwebrtc/go-protoo/peer" 8 | "github.com/cloudwebrtc/go-protoo/transport" 9 | ) 10 | 11 | type Room struct { 12 | *sync.Mutex 13 | peers map[string]*peer.Peer 14 | closed bool 15 | id string 16 | } 17 | 18 | func NewRoom(roomId string) *Room { 19 | room := &Room{ 20 | peers: make(map[string]*peer.Peer), 21 | closed: false, 22 | id: roomId, 23 | } 24 | room.Mutex = new(sync.Mutex) 25 | return room 26 | } 27 | 28 | func (room *Room) CreatePeer(peerId string, transport *transport.WebSocketTransport) *peer.Peer { 29 | newPeer := peer.NewPeer(peerId, transport) 30 | // Maybe add peer here 31 | return newPeer 32 | } 33 | 34 | func (room *Room) AddPeer(newPeer *peer.Peer) { 35 | room.Lock() 36 | defer room.Unlock() 37 | room.peers[newPeer.ID()] = newPeer 38 | 39 | // TODO auto disconnect from all rooms, but client does this too.. 40 | } 41 | 42 | func (room *Room) GetPeer(peerId string) *peer.Peer { 43 | room.Lock() 44 | defer room.Unlock() 45 | if peer, ok := room.peers[peerId]; ok { 46 | return peer 47 | } 48 | return nil 49 | } 50 | 51 | func (room *Room) Map(fn func(string, *peer.Peer)) { 52 | room.Lock() 53 | defer room.Unlock() 54 | for id, peer := range room.peers { 55 | fn(id, peer) 56 | } 57 | } 58 | 59 | func (room *Room) GetPeers() map[string]*peer.Peer { 60 | return room.peers 61 | } 62 | 63 | func (room *Room) RemovePeer(peerId string) { 64 | room.Lock() 65 | defer room.Unlock() 66 | delete(room.peers, peerId) 67 | } 68 | 69 | func (room *Room) ID() string { 70 | return room.id 71 | } 72 | 73 | func (room *Room) HasPeer(peerId string) bool { 74 | room.Lock() 75 | defer room.Unlock() 76 | _, ok := room.peers[peerId] 77 | return ok 78 | } 79 | 80 | func (room *Room) Notify(from *peer.Peer, method string, data interface{}) { 81 | room.Lock() 82 | defer room.Unlock() 83 | for id, peer := range room.peers { 84 | //send to other peers 85 | if id != from.ID() { 86 | peer.Notify(method, data) 87 | } 88 | } 89 | } 90 | 91 | func (room *Room) Close() { 92 | logger.Warnf("Close all peers !") 93 | room.Lock() 94 | defer room.Unlock() 95 | for id, peer := range room.peers { 96 | logger.Warnf("Close => peer(%s).", id) 97 | peer.Close() 98 | } 99 | room.closed = true 100 | } 101 | -------------------------------------------------------------------------------- /server/websocket_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/cloudwebrtc/go-protoo/logger" 8 | "github.com/cloudwebrtc/go-protoo/transport" 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | type WebSocketServerConfig struct { 13 | Host string 14 | Port int 15 | CertFile string 16 | KeyFile string 17 | HTMLRoot string 18 | WebSocketPath string 19 | } 20 | 21 | func DefaultConfig() WebSocketServerConfig { 22 | return WebSocketServerConfig{ 23 | Host: "0.0.0.0", 24 | Port: 8443, 25 | HTMLRoot: ".", 26 | WebSocketPath: "/ws", 27 | } 28 | } 29 | 30 | type WebSocketServer struct { 31 | handleWebSocket func(ws *transport.WebSocketTransport, request *http.Request) 32 | // Websocket upgrader 33 | upgrader websocket.Upgrader 34 | } 35 | 36 | func NewWebSocketServer(handler func(ws *transport.WebSocketTransport, request *http.Request)) *WebSocketServer { 37 | var server = &WebSocketServer{ 38 | handleWebSocket: handler, 39 | } 40 | server.upgrader = websocket.Upgrader{ 41 | CheckOrigin: func(r *http.Request) bool { 42 | return true 43 | }, 44 | } 45 | return server 46 | } 47 | 48 | func (server *WebSocketServer) handleWebSocketRequest(writer http.ResponseWriter, request *http.Request) { 49 | responseHeader := http.Header{} 50 | responseHeader.Add("Sec-WebSocket-Protocol", "protoo") 51 | socket, err := server.upgrader.Upgrade(writer, request, responseHeader) 52 | if err != nil { 53 | panic(err) 54 | } 55 | wsTransport := transport.NewWebSocketTransport(socket) 56 | wsTransport.Start() 57 | 58 | server.handleWebSocket(wsTransport, request) 59 | } 60 | 61 | func (server *WebSocketServer) Bind(cfg WebSocketServerConfig) { 62 | // Websocket handle func 63 | http.HandleFunc(cfg.WebSocketPath, server.handleWebSocketRequest) 64 | http.Handle("/", http.FileServer(http.Dir(cfg.HTMLRoot))) 65 | 66 | if cfg.CertFile == "" || cfg.KeyFile == "" { 67 | logger.Infof("non-TLS WebSocketServer listening on: %s:%d", cfg.Host, cfg.Port) 68 | panic(http.ListenAndServe(cfg.Host+":"+strconv.Itoa(cfg.Port), nil)) 69 | } else { 70 | logger.Infof("TLS WebSocketServer listening on: %s:%d", cfg.Host, cfg.Port) 71 | panic(http.ListenAndServeTLS(cfg.Host+":"+strconv.Itoa(cfg.Port), cfg.CertFile, cfg.KeyFile, nil)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /transport/websocket_transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/cloudwebrtc/go-protoo/logger" 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | const ( 12 | // Time allowed to write a message to the peer. 13 | writeWait = 10 * time.Second 14 | 15 | // Time allowed to read the next pong message from the peer. 16 | pongWait = 60 * time.Second 17 | 18 | // Send pings to peer with this period. Must be less than pongWait. 19 | pingPeriod = 5 * time.Second //(pongWait * 8) / 10 20 | 21 | // Maximum message size allowed from peer. 22 | maxMessageSize = 131072 //128k 23 | ) 24 | 25 | type TransportErr struct { 26 | Code int 27 | Text string 28 | } 29 | 30 | type TransportChans struct { 31 | OnMsg chan []byte 32 | OnErr chan TransportErr 33 | OnClose chan TransportErr 34 | SendCh chan []byte 35 | } 36 | 37 | type WebSocketTransport struct { 38 | TransportChans 39 | socket *websocket.Conn 40 | closed bool 41 | stop chan bool 42 | stopLock sync.RWMutex 43 | shutdown bool 44 | tErr *TransportErr 45 | } 46 | 47 | 48 | 49 | func NewWebSocketTransport(socket *websocket.Conn) *WebSocketTransport { 50 | var transport WebSocketTransport 51 | transport.socket = socket 52 | transport.closed = false 53 | transport.tErr = nil 54 | transport.stop = make(chan bool, 100) 55 | 56 | transport.socket.SetCloseHandler(func(code int, text string) error { 57 | logger.Warnf("On transport close %s [%d]", text, code) 58 | transport.tErr = &TransportErr{code, text} 59 | transport.Stop() 60 | return nil 61 | }) 62 | 63 | transport.TransportChans = TransportChans{ 64 | OnMsg: make(chan []byte, 100), 65 | OnErr: make(chan TransportErr, 1), 66 | OnClose: make(chan TransportErr, 1), 67 | SendCh: make(chan []byte, 100), 68 | } 69 | return &transport 70 | } 71 | 72 | func (transport *WebSocketTransport) Start() { 73 | go transport.ReadLoop() 74 | go transport.WriteLoop() 75 | } 76 | 77 | func (transport *WebSocketTransport) ReadLoop() { 78 | defer func() { 79 | logger.Debugf("Exiting transport ReadLoop") 80 | // Signal stop if not already in progress 81 | transport.Stop() 82 | }() 83 | 84 | transport.socket.SetReadLimit(maxMessageSize) 85 | transport.socket.SetReadDeadline(time.Now().Add(pongWait)) 86 | transport.socket.SetPongHandler(func(string) error { 87 | transport.socket.SetReadDeadline(time.Now().Add(pongWait)) 88 | return nil 89 | }) 90 | 91 | for { 92 | _, message, err := transport.socket.ReadMessage() 93 | if err != nil { 94 | if wsErr, ok := err.(*websocket.CloseError); ok { 95 | transport.tErr = &TransportErr{wsErr.Code, wsErr.Text} 96 | } 97 | logger.Debugf("Got error: %v", err) 98 | break 99 | } 100 | 101 | logger.Debugf("Received: %s", message) 102 | transport.OnMsg <- []byte(message) 103 | 104 | // Check stop 105 | transport.stopLock.RLock() 106 | stop := transport.shutdown 107 | transport.stopLock.RUnlock() 108 | if stop { 109 | return 110 | } 111 | } 112 | 113 | 114 | } 115 | 116 | func (transport *WebSocketTransport) WriteLoop() { 117 | defer func() { 118 | logger.Debugf("Exiting transport WriteLoop") 119 | // Make sure the whole transport is marked for stop if not already 120 | transport.Stop() 121 | // Shut down the connection. Will kill reader if blocked on read 122 | transport.close() 123 | }() 124 | 125 | pingTicker := time.NewTicker(pingPeriod) 126 | 127 | for { 128 | select { 129 | case _ = <-pingTicker.C: 130 | logger.Debugf("Send keepalive !!!") 131 | if err := transport.socket.WriteMessage(websocket.PingMessage, nil); err != nil { 132 | logger.Warnf("Keepalive has failed") 133 | pingTicker.Stop() 134 | if wsErr, ok := err.(*websocket.CloseError); ok { 135 | transport.tErr = &TransportErr{wsErr.Code, wsErr.Text} 136 | } 137 | return 138 | } 139 | case message := <-transport.SendCh: 140 | { 141 | logger.Debugf("Send data: %s", message) 142 | err := transport.socket.WriteMessage(websocket.TextMessage, message) 143 | // TODO handle send error 144 | if err != nil { 145 | logger.Warnf("Socker send Error %v", err) 146 | } 147 | } 148 | case <-transport.stop: 149 | return 150 | } 151 | } 152 | } 153 | 154 | func (transport *WebSocketTransport) Close() { 155 | transport.Stop() 156 | } 157 | 158 | // Start shutdown 159 | // Set shutdown bool to trigger read loop exit on next loop 160 | // signal write loop to exit and begin socket close 161 | 162 | func (transport *WebSocketTransport) Stop() { 163 | transport.stopLock.Lock() 164 | defer transport.stopLock.Unlock() 165 | if !transport.shutdown { 166 | logger.Infof("Trigger transport shutdown") 167 | transport.shutdown = true 168 | transport.stop <- true 169 | } 170 | } 171 | 172 | 173 | /* 174 | * Close connection. 175 | */ 176 | 177 | func (transport *WebSocketTransport) close() { 178 | if transport.closed == false { 179 | logger.Debugf("Close ws transport now") 180 | transport.socket.Close() 181 | transport.closed = true 182 | if transport.tErr != nil { 183 | transport.OnClose <- *transport.tErr 184 | 185 | } else { 186 | transport.OnClose <- TransportErr{100, "Closed"} 187 | } 188 | } else { 189 | logger.Warnf("Transport already closed") 190 | } 191 | } 192 | --------------------------------------------------------------------------------