├── 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.*
## 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 | Login
19 |
20 |
21 |
22 |
23 |
24 | Chat
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Chat Room
35 |
36 |
40 |
41 |
42 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
Connected Users
80 |
81 |
85 |
86 |
87 |
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 |
--------------------------------------------------------------------------------