blob: b370a3ee71ccb1aaa77e1c9afd3779fb4db65706 [file] [log] [blame]
// 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"
"golang.org/x/net/websocket"
"encoding/json"
"errors"
"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) {
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_RSA_WITH_AES_128_CBC_SHA,
},
PreferServerCipherSuites: true,
}
server := &http.Server{ Addr: pstr, Handler: nil, TLSConfig: config }
e = server.ListenAndServeTLS("/cert/cert.pem", "/cert/key.pem")
} 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: "+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)
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)
}