├── ui ├── imgs │ └── favicon.png └── css │ └── chatbox.css ├── go.mod ├── LICENSE ├── go.sum ├── README.md ├── index.html ├── client.js └── signaling └── signaling.go /ui/imgs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmailhos/webrtc-messaging/HEAD/ui/imgs/favicon.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module signaling 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.4.1 7 | github.com/sirupsen/logrus v1.4.2 8 | ) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mathieu Mailhos 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 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 3 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 4 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 5 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 8 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 9 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 11 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 12 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTC Messaging This repo implements a simple messaging system accross web browsers using WebRTC to allow direct communications between clients. A third party, the signaling server, is used to let the clients exchange communication information. Then the signaling server is not used anymore during the conversation. The repo is hosting a simple webchat UI with its client.js and a signaling server made in Go. *Update 12th July 2016: Now available on Opera, Firefox and Chrome.* *Update 9th January 2019: rewrote signaling server in a more idiomatic way.*
chat room
## Install & Run ``` go run signaling/signaling.go ``` Open the index page in two different tabs. On MacOSX, it would be done like: ``` open index.html ``` ## Simple testing process * Open your browser and access to the chat by opening index.html. * Open a second tab for easy testing. * Log in with 'User A' * Log in with 'User B' * From 'User A', establish a connection with 'User B'. * Happy chatting! ## References - [Google.IO WebRTC Introduction Tutorial](https://www.youtube.com/watch?v=5ci91dfKCyc) - [WebRTC API Doc](http://docs.webplatform.org/wiki/apis/webrtc) -------------------------------------------------------------------------------- /ui/css/chatbox.css: -------------------------------------------------------------------------------- 1 | 2 | .portlet { 3 | margin-bottom: 15px; 4 | } 5 | 6 | .btn-white { 7 | border-color: #cccccc; 8 | color: #333333; 9 | background-color: #ffffff; 10 | } 11 | 12 | .portlet { 13 | border: 1px solid; 14 | } 15 | 16 | .portlet .portlet-heading { 17 | padding: 0 15px; 18 | } 19 | 20 | .portlet .portlet-heading h4 { 21 | padding: 1px 0; 22 | font-size: 16px; 23 | } 24 | 25 | .portlet .portlet-heading a { 26 | color: #fff; 27 | } 28 | 29 | .portlet .portlet-heading a:hover, 30 | .portlet .portlet-heading a:active, 31 | .portlet .portlet-heading a:focus { 32 | outline: none; 33 | } 34 | 35 | .portlet .portlet-widgets .dropdown-menu a { 36 | color: #333; 37 | } 38 | 39 | .portlet .portlet-widgets ul.dropdown-menu { 40 | min-width: 0; 41 | } 42 | 43 | .portlet .portlet-heading .portlet-title { 44 | float: left; 45 | } 46 | 47 | .portlet .portlet-heading .portlet-title h4 { 48 | margin: 10px 0; 49 | } 50 | 51 | .portlet .portlet-heading .portlet-widgets { 52 | float: right; 53 | margin: 8px 0; 54 | } 55 | 56 | .portlet .portlet-heading .portlet-widgets .tabbed-portlets { 57 | display: inline; 58 | } 59 | 60 | .portlet .portlet-heading .portlet-widgets .divider { 61 | margin: 0 5px; 62 | } 63 | 64 | .portlet .portlet-body { 65 | padding: 15px; 66 | background: #fff; 67 | } 68 | 69 | .portlet .portlet-footer { 70 | padding: 10px 15px; 71 | background: #e0e7e8; 72 | } 73 | 74 | .portlet .portlet-footer ul { 75 | margin: 0; 76 | } 77 | 78 | .portlet-green, 79 | .portlet-green>.portlet-heading { 80 | border-color: #16a085; 81 | } 82 | 83 | .portlet-green>.portlet-heading { 84 | color: #fff; 85 | background-color: #16a085; 86 | } 87 | 88 | .portlet-orange, 89 | .portlet-orange>.portlet-heading { 90 | border-color: #f39c12; 91 | } 92 | 93 | .portlet-orange>.portlet-heading { 94 | color: #fff; 95 | background-color: #f39c12; 96 | } 97 | 98 | .portlet-blue, 99 | .portlet-blue>.portlet-heading { 100 | border-color: #2980b9; 101 | } 102 | 103 | .portlet-blue>.portlet-heading { 104 | color: #fff; 105 | background-color: #2980b9; 106 | } 107 | 108 | .portlet-red, 109 | .portlet-red>.portlet-heading { 110 | border-color: #e74c3c; 111 | } 112 | 113 | .portlet-red>.portlet-heading { 114 | color: #fff; 115 | background-color: #e74c3c; 116 | } 117 | 118 | .portlet-purple, 119 | .portlet-purple>.portlet-heading { 120 | border-color: #8e44ad; 121 | } 122 | 123 | .portlet-purple>.portlet-heading { 124 | color: #fff; 125 | background-color: #8e44ad; 126 | } 127 | 128 | .portlet-default, 129 | .portlet-dark-blue, 130 | .portlet-default>.portlet-heading, 131 | .portlet-dark-blue>.portlet-heading { 132 | border-color: #34495e; 133 | } 134 | 135 | .portlet-default>.portlet-heading, 136 | .portlet-dark-blue>.portlet-heading { 137 | color: #fff; 138 | background-color: #34495e; 139 | } 140 | 141 | .portlet-basic, 142 | .portlet-basic>.portlet-heading { 143 | border-color: #333; 144 | } 145 | 146 | .portlet-basic>.portlet-heading { 147 | border-bottom: 1px solid #333; 148 | color: #333; 149 | background-color: #fff; 150 | } 151 | 152 | @media(min-width:768px) { 153 | .portlet { 154 | margin-bottom: 30px; 155 | } 156 | } 157 | 158 | .text-green { 159 | color: #16a085; 160 | } 161 | 162 | .text-orange { 163 | color: #f39c12; 164 | } 165 | 166 | .text-red { 167 | color: #e74c3c; 168 | } 169 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chat Room 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |

Chat Room

35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |

Connected Users

80 |
81 |
82 | 83 | 84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var connection = new WebSocket('ws://localhost:9090'); 2 | 3 | var loginInput = document.querySelector('#loginInput'); 4 | var loginBtn = document.querySelector('#loginBtn'); 5 | var otherUsernameInput = document.querySelector('#otherUsernameInput'); 6 | var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn'); 7 | var msgInput = document.querySelector('#msgInput'); 8 | var sendMsgBtn = document.querySelector('#sendMsgBtn'); 9 | 10 | var connectedUser, peerConnection, dataChannel; 11 | var name = ""; 12 | 13 | window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; 14 | window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate; 15 | window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription; 16 | 17 | //Messages received from the Signaling Server 18 | connection.onmessage = function (message) { 19 | var data = JSON.parse(message.data); 20 | 21 | switch(data.type) { 22 | case "login": 23 | onLogin(data.success); 24 | break; 25 | case "offer": 26 | onOffer(data.offer, data.name); 27 | break; 28 | case "answer": 29 | onAnswer(data.answer); 30 | break; 31 | case "candidate": 32 | onCandidate(data.candidate); 33 | break; 34 | case "leave": 35 | onLeave(); 36 | case "users": 37 | onUsers(data.users); 38 | default: 39 | break; 40 | } 41 | }; 42 | connection.onopen = function () { 43 | console.log("Connected to the signaling server."); 44 | }; 45 | 46 | connection.onerror = function (err) { 47 | console.log("Got error on trying to connect to the signaling server:", err); 48 | }; 49 | 50 | //Login Click button 51 | loginBtn.addEventListener("click", function(event) { 52 | name = loginInput.value; 53 | 54 | if(name.length > 0) { 55 | send({ 56 | type: "login", 57 | name: name 58 | }); 59 | } else { 60 | writetochat("Please enter a username.", "server"); 61 | } 62 | }); 63 | 64 | //Establishing a connection with an other peer and creating an offer. 65 | connectToOtherUsernameBtn.addEventListener("click", function () { 66 | 67 | connectedUser = otherUsernameInput.value; 68 | 69 | if (connectedUser.length > 0) { 70 | //Create channel before sending the offer 71 | //We are setting the dataChannel as reliable (means TCP) as we are sending message, not a stream. 72 | var dataChannelOptions = { 73 | reliable: true 74 | }; 75 | dataChannel = peerConnection.createDataChannel(connectedUser + "-dataChannel", dataChannelOptions); 76 | openDataChannel() 77 | peerConnection.createOffer(function (offer) { 78 | send({ 79 | type: "offer", 80 | offer: offer 81 | }); 82 | 83 | peerConnection.setLocalDescription(offer); 84 | }, function (error) { 85 | console.log("Error: ", error); 86 | writetochat("Error contacting remote peer: " + error, "server"); 87 | }); 88 | } 89 | }); 90 | 91 | //Sending message to remote peer 92 | sendMsgBtn.addEventListener("click", function (event) { 93 | var val = msgInput.value; 94 | dataChannel.send(val); 95 | writetochat(val, capitalizeFirstLetter(name)); 96 | }); 97 | 98 | //When a user logs in 99 | function onLogin(success) { 100 | 101 | if (success === false) { 102 | if (name != "") { 103 | writetochat("You are already logged in."); 104 | } else { 105 | writetochat("Username already taken! Connection refused.", "server"); 106 | } 107 | } else { 108 | //Known ICE Servers 109 | var configuration = { 110 | "iceServers": [{ "urls": "stun:stun.1.google.com:19302" }] 111 | }; 112 | 113 | peerConnection = new RTCPeerConnection(configuration); 114 | 115 | //Definition of the data channel 116 | peerConnection.ondatachannel = function(ev) { 117 | dataChannel = ev.channel; 118 | openDataChannel() 119 | }; 120 | writetochat("Connected.", "server"); 121 | 122 | //When we get our own ICE Candidate, we provide it to the other Peer. 123 | peerConnection.onicecandidate = function (event) { 124 | if (event.candidate) { 125 | send({ 126 | type: "candidate", 127 | candidate: event.candidate 128 | }); 129 | } 130 | }; 131 | peerConnection.oniceconnectionstatechange = function(e) { 132 | var iceState = peerConnection.iceConnectionState; 133 | console.log("Changing connection state:", iceState) 134 | if (iceState == "connected") { 135 | writetochat("Connection established with user " + capitalizeFirstLetter(connectedUser), "server"); 136 | } else if (iceState =="disconnected" || iceState == "closed") { 137 | onLeave(); 138 | } 139 | }; 140 | } 141 | }; 142 | 143 | //When we are receiving an offer from a remote peer 144 | function onOffer(offer, name) { 145 | connectedUser = name; 146 | peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); 147 | 148 | peerConnection.createAnswer(function (answer) { 149 | peerConnection.setLocalDescription(answer); 150 | send({ 151 | type: "answer", 152 | answer: answer 153 | }); 154 | 155 | }, function (error) { 156 | console.log("Error on receiving the offer: ", error); 157 | writetochat("Error on receiving offer from remote peer: " + error, "server"); 158 | }); 159 | } 160 | 161 | //Changes the remote description associated with the connection 162 | function onAnswer(answer) { 163 | peerConnection.setRemoteDescription(new RTCSessionDescription(answer)); 164 | } 165 | 166 | //Adding new ICE candidate 167 | function onCandidate(candidate) { 168 | peerConnection.addIceCandidate(new RTCIceCandidate(candidate)); 169 | console.log("ICE Candidate added."); 170 | } 171 | 172 | //Leave sent by the signaling server or remote peer 173 | function onLeave() { 174 | try { 175 | peerConnection.close(); 176 | console.log("Connection closed by " + connectedUser); 177 | writetochat(capitalizeFirstLetter(connectedUser) + " closed the connection.", "server"); 178 | } catch(err) { 179 | console.log("Connection already closed"); 180 | } 181 | } 182 | 183 | //Received list of users by the signaling server 184 | function onUsers(users) { 185 | var div = document.getElementById('userbox'); 186 | data = '' 187 | for (var i = 0; i < users.length; i++) { 188 | if (users[i] != name && users[i] != "") { 189 | data = data + capitalizeFirstLetter(users[i]) + '
'; 190 | } 191 | } 192 | data = data + '
'; 193 | div.innerHTML = data; 194 | } 195 | 196 | // Alias for sending to remote peer the message on JSON format 197 | function send(message) { 198 | if (connectedUser) { 199 | message.name = connectedUser; 200 | } 201 | connection.send(JSON.stringify(message)); 202 | }; 203 | 204 | //DataChannel callbacks definitions 205 | function openDataChannel() { 206 | dataChannel.onerror = function (error) { 207 | console.log("Error on data channel:", error); 208 | writetochat("Error: " + error, "server"); 209 | }; 210 | 211 | dataChannel.onmessage = function (event) { 212 | console.log("Message received:", event.data); 213 | writetochat(event.data, connectedUser); 214 | }; 215 | 216 | dataChannel.onopen = function() { 217 | console.log("Channel established."); 218 | }; 219 | 220 | dataChannel.onclose = function() { 221 | console.log("Channel closed."); 222 | }; 223 | } 224 | 225 | //Write message to chat 226 | function writetochat(data, user){ 227 | var div = document.getElementById('chatbox'); 228 | if (user != null && user != "server") { 229 | div.innerHTML = div.innerHTML + capitalizeFirstLetter(user) + ': ' + data + '
'; 230 | } else if (user == "server") { 231 | div.innerHTML = div.innerHTML + '' + data + '
'; 232 | } 233 | } 234 | 235 | //Set the first letter of the user in uppercase 236 | function capitalizeFirstLetter(string) { 237 | return string.charAt(0).toUpperCase() + string.slice(1); 238 | } 239 | -------------------------------------------------------------------------------- /signaling/signaling.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/http" 7 | "os" 8 | "sync" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/gorilla/websocket" 13 | ) 14 | 15 | type SignalingServer struct { 16 | users []*User 17 | upgrader websocket.Upgrader 18 | mux sync.Mutex 19 | } 20 | 21 | func (ss *SignalingServer) AddUser(conn *websocket.Conn, name string) { 22 | ss.mux.Lock() 23 | defer ss.mux.Unlock() 24 | 25 | ss.users = append(ss.users, &User{Name: name, Conn: conn}) 26 | } 27 | 28 | func (ss *SignalingServer) AllUserNames() []string { 29 | ss.mux.Lock() 30 | defer ss.mux.Unlock() 31 | 32 | users := make([]string, len(ss.users)) 33 | for _, user := range ss.users { 34 | users = append(users, user.Name) 35 | } 36 | 37 | return users 38 | } 39 | 40 | func (ss *SignalingServer) NotifyUsers(notify func(*User)) { 41 | // TODO: handle error 42 | for _, user := range ss.users { 43 | notify(user) 44 | } 45 | } 46 | 47 | func (ss *SignalingServer) PeerFromConn(conn *websocket.Conn) *websocket.Conn { 48 | for _, user := range ss.users { 49 | if user.Conn == conn { 50 | for _, peerUser := range ss.users { 51 | if peerUser.Name == user.Peer { 52 | return peerUser.Conn 53 | } 54 | } 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (ss *SignalingServer) PeerFromName(name string) *User { 62 | for _, user := range ss.users { 63 | for _, peerUser := range ss.users { 64 | if user.Peer == peerUser.Name { 65 | return peerUser 66 | } 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (ss *SignalingServer) UserFromConn(conn *websocket.Conn) *User { 74 | for _, user := range ss.users { 75 | if user.Conn == conn { 76 | return user 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | 83 | func (ss *SignalingServer) UserFromName(name string) *User { 84 | for _, user := range ss.users { 85 | if user.Name == name { 86 | return user 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | func (ss *SignalingServer) UpdatePeer(origin, peer string) error { 94 | ss.mux.Lock() 95 | defer ss.mux.Unlock() 96 | 97 | for _, user := range ss.users { 98 | if user.Name == origin { 99 | user.Peer = peer 100 | return nil 101 | } 102 | } 103 | 104 | return errors.New("missing origin user") 105 | } 106 | 107 | // User template 108 | type User struct { 109 | Name string 110 | Peer string 111 | Conn *websocket.Conn 112 | } 113 | 114 | // SignalMessage template to establish connection 115 | type SignalMessage struct { 116 | Type string `json:"type,omitempty"` 117 | Name string `json:"name,omitempty"` 118 | Offer *Offer `json:"offer,omitempty"` 119 | Answer *Answer `json:"answer,omitempty"` 120 | Candidate *Candidate `json:"candidate,omitempty"` 121 | } 122 | 123 | // LoginResponse is a LoginRequest response from the server 124 | // External struct to manage Success bool independently 125 | type LoginResponse struct { 126 | Type string `json:"type"` 127 | Success bool `json:"success"` 128 | } 129 | 130 | //Users list 131 | type Users struct { 132 | Type string `json:"type"` 133 | Users []string `json:"users"` 134 | } 135 | 136 | // Offer struct 137 | type Offer struct { 138 | Type string `json:"type"` 139 | Sdp string `json:"sdp"` 140 | } 141 | 142 | // Answer struct 143 | type Answer struct { 144 | Type string `json:"type"` 145 | Sdp string `json:"sdp"` 146 | } 147 | 148 | // Candidate struct 149 | type Candidate struct { 150 | Candidate string `json:"candidate"` 151 | SdpMid string `json:"sdpMid"` 152 | SdpMLineIndex int `json:"sdpMLineIndex"` 153 | } 154 | 155 | // Leaving struct 156 | type Leaving struct { 157 | Type string `json:"type"` 158 | } 159 | 160 | // DefaultError struct 161 | type DefaultError struct { 162 | Type string `json:"type"` 163 | Message string `json:"message"` 164 | } 165 | 166 | // offerEvent forwards an offer to a remote peer 167 | func (ss *SignalingServer) offerEvent(conn *websocket.Conn, data SignalMessage) error { 168 | var sm SignalMessage 169 | 170 | author := ss.UserFromConn(conn) 171 | if author == nil { 172 | log.Errorf("offerEvent: unregistered author") 173 | return errors.New("unregistered author") 174 | } 175 | 176 | peer := ss.UserFromName(data.Name) 177 | if peer == nil { 178 | log.Errorf("offerEvent: unknown peer author") 179 | return errors.New("unknown peer") 180 | } 181 | 182 | log.Infof("offerEvent: message received from %v", author.Name) 183 | 184 | if err := ss.UpdatePeer(author.Name, peer.Name); err != nil { 185 | return err 186 | } 187 | 188 | log.Debugf("offerEvent: set peer of author %v to %v", author.Name, peer.Name) 189 | 190 | sm.Name = author.Name 191 | sm.Offer = data.Offer 192 | sm.Type = "offer" 193 | out, err := json.Marshal(sm) 194 | if err != nil { 195 | return err 196 | } 197 | 198 | if err = peer.Conn.WriteMessage(websocket.TextMessage, out); err != nil { 199 | return err 200 | } 201 | 202 | log.Infof("Offer forwarded to %v", peer.Name) 203 | 204 | return nil 205 | } 206 | 207 | //answerEvent forwards Answer to original peer 208 | func (ss *SignalingServer) answerEvent(conn *websocket.Conn, data SignalMessage) error { 209 | var sm SignalMessage 210 | 211 | author := ss.UserFromConn(conn) 212 | if author == nil { 213 | return errors.New("unregistered author") 214 | } 215 | 216 | peer := ss.UserFromName(data.Name) 217 | if peer == nil { 218 | return errors.New("unknown requested peer") 219 | } 220 | 221 | if err := ss.UpdatePeer(author.Name, data.Name); err != nil { 222 | return err 223 | } 224 | 225 | sm.Answer = data.Answer 226 | sm.Type = "answer" 227 | out, err := json.Marshal(sm) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | if err = peer.Conn.WriteMessage(websocket.TextMessage, out); err != nil { 233 | return err 234 | } 235 | 236 | log.Infof("answerEvent: answer forwarded to %v", author.Name) 237 | 238 | return nil 239 | } 240 | 241 | //candidateEvent forwards candidate to original peer 242 | func (ss *SignalingServer) candidateEvent(conn *websocket.Conn, data SignalMessage) error { 243 | var sm SignalMessage 244 | 245 | author := ss.UserFromConn(conn) 246 | if author == nil { 247 | return errors.New("unregistered connection") 248 | } 249 | 250 | log.Infof("candidateEvent received from %v", author.Name) 251 | 252 | peer := ss.UserFromName(data.Name) 253 | if peer == nil { 254 | return errors.New("unregistered peer") 255 | } 256 | 257 | sm.Candidate = data.Candidate 258 | sm.Type = "candidate" 259 | out, err := json.Marshal(sm) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | if err = peer.Conn.WriteMessage(websocket.TextMessage, out); err != nil { 265 | return err 266 | } 267 | 268 | log.Infof("candidateEvent: candidate forwarded to %v", peer.Name) 269 | 270 | return nil 271 | } 272 | 273 | // leaveEvent terminates a connection (client closed the browser for example) 274 | func (ss *SignalingServer) leaveEvent(conn *websocket.Conn) error { 275 | defer conn.Close() 276 | 277 | log.Info("received terminated connection request") 278 | 279 | if peerConn := ss.PeerFromConn(conn); peerConn != nil { 280 | var out []byte 281 | out, err := json.Marshal(Leaving{Type: "leaving"}) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | err = peerConn.WriteMessage(websocket.TextMessage, out) 287 | if err != nil { 288 | return err 289 | } 290 | } 291 | 292 | return nil 293 | } 294 | 295 | // loginEvent 296 | func (ss *SignalingServer) loginEvent(conn *websocket.Conn, data SignalMessage) error { 297 | if author := ss.UserFromName(data.Name); author != nil { 298 | log.Debugf("loginEvent: %v tried to log in but was already registered: %v", data.Name, ss.AllUserNames()) 299 | return nil 300 | } 301 | 302 | ss.AddUser(conn, data.Name) 303 | out, err := json.Marshal(LoginResponse{Type: "login", Success: true}) 304 | if err != nil { 305 | return err 306 | } 307 | 308 | log.Infof("User %v registered in successfully", data.Name) 309 | 310 | if err = conn.WriteMessage(websocket.TextMessage, out); err != nil { 311 | return err 312 | } 313 | 314 | out, err = json.Marshal(Users{Type: "users", Users: ss.AllUserNames()}) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | ss.NotifyUsers(func(user *User) { 320 | user.Conn.WriteMessage(websocket.TextMessage, out) 321 | }) 322 | 323 | return nil 324 | } 325 | 326 | // unknownCommandEvent 327 | func unknownCommandEvent(conn *websocket.Conn, raw []byte) error { 328 | var out []byte 329 | var message SignalMessage 330 | 331 | err := json.Unmarshal(raw, &message) 332 | if err != nil { 333 | return err 334 | } 335 | 336 | log.Infof("unrecognized command %v", string(raw)) 337 | 338 | out, err = json.Marshal(DefaultError{Type: "error", Message: "Unrecognized command"}) 339 | if err != nil { 340 | return err 341 | } 342 | 343 | if err = conn.WriteMessage(websocket.TextMessage, out); err != nil { 344 | return err 345 | } 346 | 347 | return nil 348 | } 349 | 350 | func (ss *SignalingServer) connHandler(conn *websocket.Conn) error { 351 | var message SignalMessage 352 | 353 | _, raw, err := conn.ReadMessage() 354 | if err != nil { 355 | log.Errorf("connHandler.ReadMessage: %v", err) 356 | return err 357 | } 358 | 359 | if err := json.Unmarshal(raw, &message); err != nil { 360 | log.Errorf("connHandler.Unmarshal: %v", err) 361 | 362 | out, err := json.Marshal(DefaultError{Type: "error", Message: "Incorrect data format"}) 363 | if err != nil { 364 | log.Errorf("connHandler.Marshal: %v", err) 365 | return err 366 | } 367 | 368 | if err = conn.WriteMessage(websocket.TextMessage, out); err != nil { 369 | log.Errorf("connHandler.WriteMessage: %v", err) 370 | return err 371 | } 372 | 373 | return err 374 | } 375 | 376 | switch message.Type { 377 | case "login": 378 | err = ss.loginEvent(conn, message) 379 | case "offer": 380 | err = ss.offerEvent(conn, message) 381 | case "answer": 382 | err = ss.answerEvent(conn, message) 383 | case "candidate": 384 | err = ss.candidateEvent(conn, message) 385 | case "leave": 386 | err = ss.leaveEvent(conn) 387 | default: 388 | err = unknownCommandEvent(conn, raw) 389 | } 390 | 391 | if err != nil { 392 | log.Errorf("[%vEvent]: %v", message.Type, err) 393 | return err 394 | } 395 | 396 | return nil 397 | } 398 | 399 | func (ss *SignalingServer) Handler(w http.ResponseWriter, r *http.Request) { 400 | conn, err := ss.upgrader.Upgrade(w, r, nil) 401 | if err != nil { 402 | log.Error(err) 403 | http.Error(w, err.Error(), http.StatusInternalServerError) 404 | } 405 | 406 | log.Debugf("%v accesses the server", conn.RemoteAddr()) 407 | for { 408 | if err := ss.connHandler(conn); err != nil { 409 | if err.Error() == "websocket: close 1001 (going away)" { 410 | user := ss.UserFromConn(conn) 411 | if user == nil { 412 | log.Debugf("connection closed for %v", conn.RemoteAddr()) 413 | } else { 414 | ss.leaveEvent(conn) 415 | log.Debugf("connection closed for %v", user.Name) 416 | } 417 | } 418 | log.Error(err) 419 | } 420 | } 421 | 422 | } 423 | 424 | func init() { 425 | log.SetFormatter(&log.JSONFormatter{}) 426 | log.SetOutput(os.Stdout) 427 | } 428 | 429 | func main() { 430 | // Ugrade policty from http request to websocket 431 | // TODO: to be defined 432 | ss := SignalingServer{ 433 | users: []*User{}, 434 | upgrader: websocket.Upgrader{ 435 | ReadBufferSize: 1024, 436 | WriteBufferSize: 1024, 437 | CheckOrigin: func(r *http.Request) bool { 438 | return true 439 | }, 440 | }, 441 | } 442 | 443 | http.HandleFunc("/", ss.Handler) 444 | 445 | log.Info("Signaling Server started") 446 | 447 | err := http.ListenAndServe(":9090", nil) 448 | if err != nil { 449 | log.Panic(err) 450 | } 451 | } 452 | --------------------------------------------------------------------------------