-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcollider.go
More file actions
217 lines (192 loc) · 6.16 KB
/
collider.go
File metadata and controls
217 lines (192 loc) · 6.16 KB
1
2
3
4
5
6
7
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
// Package collider implements a signaling server based on WebSocket.
package collider
import (
"crypto/tls"
"encoding/json"
"errors"
"golang.org/x/net/websocket"
"html"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"time"
)
const registerTimeoutSec = 10
// This is a temporary solution to avoid holding a zombie connection forever, by
// setting a 1 day timeout on reading from the WebSocket connection.
const wsReadTimeoutSec = 60 * 60 * 24
type Collider struct {
*roomTable
dash *dashboard
}
func NewCollider(rs string) *Collider {
return &Collider{
roomTable: newRoomTable(time.Second*registerTimeoutSec, rs),
dash: newDashboard(),
}
}
// Run starts the collider server and blocks the thread until the program exits.
func (c *Collider) Run(p int, useTls bool, certFile, keyFile string) {
http.Handle("/ws", websocket.Handler(c.wsHandler))
http.HandleFunc("/status", c.httpStatusHandler)
http.HandleFunc("/", c.httpHandler)
var e error
pstr := ":" + strconv.Itoa(p)
if useTls {
config := &tls.Config{
// Only allow ciphers that support forward secrecy for iOS9 compatibility:
// https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
},
PreferServerCipherSuites: true,
}
server := &http.Server{Addr: pstr, Handler: nil, TLSConfig: config}
e = server.ListenAndServeTLS(certFile, keyFile)
} else {
e = http.ListenAndServe(pstr, nil)
}
if e != nil {
log.Fatal("Run: " + e.Error())
}
}
// httpStatusHandler is a HTTP handler that handles GET requests to get the
// status of collider.
func (c *Collider) httpStatusHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "GET")
rp := c.dash.getReport(c.roomTable)
enc := json.NewEncoder(w)
if err := enc.Encode(rp); err != nil {
err = errors.New("Failed to encode to JSON: err=" + err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
c.dash.onHttpErr(err)
}
}
// httpHandler is a HTTP handler that handles GET/POST/DELETE requests.
// POST request to path "/$ROOMID/$CLIENTID" is used to send a message to the other client of the room.
// $CLIENTID is the source client ID.
// The request must have a form value "msg", which is the message to send.
// DELETE request to path "/$ROOMID/$CLIENTID" is used to delete all records of a client, including the queued message from the client.
// "OK" is returned if the request is valid.
func (c *Collider) httpHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Methods", "POST, DELETE")
p := strings.Split(r.URL.Path, "/")
if len(p) != 3 {
c.httpError("Invalid path: "+html.EscapeString(r.URL.Path), w)
return
}
rid, cid := p[1], p[2]
switch r.Method {
case "POST":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
c.httpError("Failed to read request body: "+err.Error(), w)
return
}
m := string(body)
if m == "" {
c.httpError("Empty request body", w)
return
}
if err := c.roomTable.send(rid, cid, m); err != nil {
c.httpError("Failed to send the message: "+err.Error(), w)
return
}
case "DELETE":
c.roomTable.remove(rid, cid)
default:
return
}
io.WriteString(w, "OK\n")
}
// wsHandler is a WebSocket server that handles requests from the WebSocket client in the form of:
// 1. { 'cmd': 'register', 'roomid': $ROOM, 'clientid': $CLIENT' },
// which binds the WebSocket client to a client ID and room ID.
// A client should send this message only once right after the connection is open.
// or
// 2. { 'cmd': 'send', 'msg': $MSG }, which sends the message to the other client of the room.
// It should be sent to the server only after 'regiser' has been sent.
// The message may be cached by the server if the other client has not joined.
//
// Unexpected messages will cause the WebSocket connection to be closed.
func (c *Collider) wsHandler(ws *websocket.Conn) {
var rid, cid string
registered := false
var msg wsClientMsg
loop:
for {
err := ws.SetReadDeadline(time.Now().Add(time.Duration(wsReadTimeoutSec) * time.Second))
if err != nil {
c.wsError("ws.SetReadDeadline error: "+err.Error(), ws)
break
}
err = websocket.JSON.Receive(ws, &msg)
log.Println("receive message", msg)
if err != nil {
if err.Error() != "EOF" {
c.wsError("websocket.JSON.Receive error: "+err.Error(), ws)
}
break
}
switch msg.Cmd {
case "register":
if registered {
c.wsError("Duplicated register request", ws)
break loop
}
if msg.RoomID == "" || msg.ClientID == "" {
c.wsError("Invalid register request: missing 'clientid' or 'roomid'", ws)
break loop
}
if err = c.roomTable.register(msg.RoomID, msg.ClientID, ws); err != nil {
c.wsError(err.Error(), ws)
break loop
}
registered, rid, cid = true, msg.RoomID, msg.ClientID
c.dash.incrWs()
defer c.roomTable.deregister(rid, cid)
break
case "send":
if !registered {
c.wsError("Client not registered", ws)
break loop
}
if msg.Msg == "" {
c.wsError("Invalid send request: missing 'msg'", ws)
break loop
}
c.roomTable.send(rid, cid, msg.Msg)
break
default:
c.wsError("Invalid message: unexpected 'cmd'", ws)
break
}
}
// This should be unnecessary but just be safe.
ws.Close()
}
func (c *Collider) httpError(msg string, w http.ResponseWriter) {
err := errors.New(msg)
http.Error(w, err.Error(), http.StatusInternalServerError)
c.dash.onHttpErr(err)
}
func (c *Collider) wsError(msg string, ws *websocket.Conn) {
err := errors.New(msg)
sendServerErr(ws, msg)
c.dash.onWsErr(err)
}