From a2116591bcd04687289bb9fbd57e9bab724ce972 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 15:59:58 -0700 Subject: [PATCH 01/29] Switch the socket server in development --- src/constants.js | 2 ++ src/socket.js | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/constants.js b/src/constants.js index dad381a7..470807b3 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,10 +1,12 @@ var SVGPATH = '', DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'), + WS_SERVER = DEBUG ? "localhost:8001" : "catbag.frankerfacez.com", SERVER = DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/"; module.exports = { DEBUG: DEBUG, SERVER: SERVER, + WS_SERVER: WS_SERVER, API_SERVER: "//api.frankerfacez.com/", API_SERVER_2: "//direct-api.frankerfacez.com/", diff --git a/src/socket.js b/src/socket.js index 6263c280..2eae3781 100644 --- a/src/socket.js +++ b/src/socket.js @@ -1,4 +1,5 @@ -var FFZ = window.FrankerFaceZ; +var FFZ = window.FrankerFaceZ, + constants = require('./constants'); FFZ.prototype._ws_open = false; FFZ.prototype._ws_delay = 0; @@ -12,6 +13,8 @@ FFZ.ws_on_close = []; // Socket Creation // ---------------- +// Attempt to authenticate to the socket server as a real browser by loading the root page. +// e.g. cloudflare ddos check FFZ.prototype.ws_iframe = function() { this._ws_last_iframe = Date.now(); var ifr = document.createElement('iframe'), @@ -36,7 +39,7 @@ FFZ.prototype.ws_create = function() { this._ws_pending = this._ws_pending || []; try { - ws = this._ws_sock = new WebSocket("ws://catbag.frankerfacez.com/"); + ws = this._ws_sock = new WebSocket("ws://" + constants.WS_SERVER + "/"); } catch(err) { this._ws_exists = false; return this.log("Error Creating WebSocket: " + err); From dfcd6bf413709d584b37efaf054a7b2a31a3f7bf Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 16:04:27 -0700 Subject: [PATCH 02/29] Cycle socket servers when connection fails --- src/constants.js | 4 ++-- src/socket.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/constants.js b/src/constants.js index 470807b3..2a31c5f8 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,12 +1,12 @@ var SVGPATH = '', DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'), - WS_SERVER = DEBUG ? "localhost:8001" : "catbag.frankerfacez.com", + WS_SERVERS = DEBUG ? ["localhost:8001", "catbag.frankerfacez.com"] : ["catbag.frankerfacez.com"], SERVER = DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/"; module.exports = { DEBUG: DEBUG, SERVER: SERVER, - WS_SERVER: WS_SERVER, + WS_SERVERS: WS_SERVERS, API_SERVER: "//api.frankerfacez.com/", API_SERVER_2: "//direct-api.frankerfacez.com/", diff --git a/src/socket.js b/src/socket.js index 2eae3781..30882bb0 100644 --- a/src/socket.js +++ b/src/socket.js @@ -4,6 +4,7 @@ var FFZ = window.FrankerFaceZ, FFZ.prototype._ws_open = false; FFZ.prototype._ws_delay = 0; FFZ.prototype._ws_last_iframe = 0; +FFZ.prototype._ws_host_idx = 0; FFZ.ws_commands = {}; FFZ.ws_on_close = []; @@ -39,7 +40,7 @@ FFZ.prototype.ws_create = function() { this._ws_pending = this._ws_pending || []; try { - ws = this._ws_sock = new WebSocket("ws://" + constants.WS_SERVER + "/"); + ws = this._ws_sock = new WebSocket("ws://" + constants.WS_SERVERs[this._ws_host_idx] + "/"); } catch(err) { this._ws_exists = false; return this.log("Error Creating WebSocket: " + err); @@ -119,7 +120,7 @@ FFZ.prototype.ws_create = function() { f._ws_open = false; // When the connection closes, run our callbacks. - for(var i=0; i < FFZ.ws_on_close.length; i++) { + for (var i=0; i < FFZ.ws_on_close.length; i++) { try { FFZ.ws_on_close[i].bind(f)(); } catch(err) { @@ -127,6 +128,9 @@ FFZ.prototype.ws_create = function() { } } + // Attempt to cycle to backup server + f._ws_host_idx = (f._ws_host_idx + 1) % constants.WS_SERVERS.length; + if ( f._ws_delay > 10000 ) { var ua = navigator.userAgent.toLowerCase(); if ( Date.now() - f._ws_last_iframe > 1800000 && !(ua.indexOf('chrome') === -1 && ua.indexOf('safari') !== -1) ) From 36ac3e576f8351f545099e13d33c5c261134dca4 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 19:59:34 -0700 Subject: [PATCH 03/29] Start writing socket server in Go --- .gitignore | 5 +- socketserver/cmd/socketserver/socketserver.go | 48 ++++ socketserver/lib/handlecore.go | 237 ++++++++++++++++++ socketserver/lib/handlecore_test.go | 37 +++ src/socket.js | 2 +- 5 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 socketserver/cmd/socketserver/socketserver.go create mode 100644 socketserver/lib/handlecore.go create mode 100644 socketserver/lib/handlecore_test.go diff --git a/.gitignore b/.gitignore index 96f9db49..a524bdc7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,8 @@ npm-debug.log build Extension Building .idea +*.iml script.js -script.min.js \ No newline at end of file +script.min.js + +/socketserver/cmd/socketserver/socketserver diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go new file mode 100644 index 00000000..b898617e --- /dev/null +++ b/socketserver/cmd/socketserver/socketserver.go @@ -0,0 +1,48 @@ +package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/socketserver" + +import ( + "flag" + "../../lib" + "log" + "net/http" +) + +var origin *string = flag.String("origin", "localhost:8001", "Client-visible origin of the socket server") +var bindAddress *string = flag.String("listen", "", "Address to bind to, if different from origin") +var certificateFile *string = flag.String("crt", "", "SSL certificate file") +var privateKeyFile *string = flag.String("key", "", "SSL private key file") + +func main() { + flag.Parse() + + if *origin == "" { + log.Fatalln("--origin argument required") + } + if *bindAddress == "" { + bindAddress = origin + } + if (*certificateFile == "") != (*privateKeyFile == "") { + log.Fatalln("Either both --crt and --key can be provided, or neither.") + } + + conf := &lib.Config { + SSLKeyFile: *privateKeyFile, + SSLCertificateFile: *certificateFile, + UseSSL: *certificateFile != "", + + Origin: *origin, + } + + lib.SetupServerAndHandle(conf) + + var err error + if conf.UseSSL { + err = http.ListenAndServeTLS(*bindAddress, *certificateFile, *privateKeyFile, nil) + } else { + err = http.ListenAndServe(*bindAddress, nil) + } + + if err != nil { + log.Fatal("ListenAndServe: ", err) + } +} diff --git a/socketserver/lib/handlecore.go b/socketserver/lib/handlecore.go new file mode 100644 index 00000000..4acf162a --- /dev/null +++ b/socketserver/lib/handlecore.go @@ -0,0 +1,237 @@ +package lib // import "bitbucket.org/stendec/frankerfacez/socketserver/lib" + +import ( + "net/http" + "golang.org/x/net/websocket" + "crypto/tls" + "log" + "strings" + "strconv" + "errors" + "encoding/json" + "fmt" +) + +const MAX_PACKET_SIZE = 1024 + +type Config struct { + SSLCertificateFile string + SSLKeyFile string + UseSSL bool + + Origin string +} + +// A command is how the client refers to a function on the server. It's just a string. +type Command string + +type ClientMessage struct { + MessageID int + // The command that the client wants from the server. + // When sent from the server, the literal string 'True' indicates success. + // Before sending, a blank Command will be converted into SuccessCommand. + Command Command + Arguments interface{} +} + +// Sent by the server in ClientMessage.Command to indicate success. +const SuccessCommand Command = "True" + +var FFZCodec websocket.Codec = websocket.Codec{ + Marshal: MarshalClientMessage, + Unmarshal: UnmarshalClientMessage, +} + +// Errors that get returned to the client. +var ProtocolError error = errors.New("FFZ Socket protocol error.") +var ExpectedSingleString = errors.New("Error: Expected single string as arguments.") +var ExpectedTwoStrings = errors.New("Error: Expected array of string, string as arguments.") +var ExpectedStringAndInt = errors.New("Error: Expected array of string, int as arguments.") +var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.") + +func SetupServer(config *Config) *websocket.Server { + sockConf, err := websocket.NewConfig("/", config.Origin) + if err != nil { + panic(err) + } + if config.UseSSL { + cert, err := tls.LoadX509KeyPair(config.SSLCertificateFile, config.SSLKeyFile) + if err != nil { + panic(err) + } + tlsConf := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ServerName: config.Origin, + } + tlsConf.BuildNameToCertificate() + sockConf.TlsConfig = tlsConf + } + + sockServer := &websocket.Server{} + sockServer.Config = *sockConf + sockServer.Handler = HandleSocketConnection + return sockServer +} + +// Set up a websocket listener and register it on /. +// (Uses http.DefaultServeMux .) +func SetupServerAndHandle(config *Config) { + sockServer := SetupServer(config) + + http.HandleFunc("/", sockServer.ServeHTTP) +} + +// Handle a new websocket connection from a FFZ client. +// This runs in a goroutine started by net/http. +func HandleSocketConnection(conn *websocket.Conn) { + // websocket.Conn is a ReadWriteCloser + var msg ClientMessage + var err error = nil + var abort bool + + log.Print("Got a connection from ", conn.RemoteAddr()) + + for ; err == nil || abort; err = FFZCodec.Receive(conn, &msg) { + log.Print(msg) + } + + if err != nil { + FFZCodec.Send(conn, ClientMessage{ + MessageID: -1, + Command: "error", + Arguments: err.Error(), + }) + } + conn.Close() +} + +// Unpack a message sent from the client into a ClientMessage. +func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err error) { + var spaceIdx int + + out := v.(*ClientMessage) + dataStr := string(data) + + // Message ID + spaceIdx = strings.IndexRune(dataStr, ' ') + if spaceIdx == -1 { + return ProtocolError + } + messageId, err := strconv.Atoi(dataStr[:spaceIdx]) + if messageId <= 0 { + return ProtocolError + } + + out.MessageID = messageId + dataStr = dataStr[spaceIdx + 1:] + + spaceIdx = strings.IndexRune(dataStr, ' ') + if spaceIdx == -1 { + out.Command = Command(dataStr) + out.Arguments = nil + return nil + } else { + out.Command = Command(dataStr[:spaceIdx]) + } + dataStr = dataStr[spaceIdx + 1:] + argumentsJson := dataStr + err = json.Unmarshal([]byte(argumentsJson), &out.Arguments) + if err != nil { + return + } + return nil +} + +func MarshalClientMessage(clientMessage interface{}) (data []byte, payloadType byte, err error) { + var msg ClientMessage + var ok bool + msg, ok = clientMessage.(ClientMessage) + if !ok { + pMsg, ok := clientMessage.(*ClientMessage) + if !ok { + panic("MarshalClientMessage: argument needs to be a ClientMessage") + } + msg = *pMsg + } + var dataStr string + + if msg.Command == "" { + msg.Command = SuccessCommand + } + + if msg.Arguments != nil { + argBytes, err := json.Marshal(msg.Arguments) + if err != nil { + return nil, 0, err + } + + dataStr = fmt.Sprintf("%d %s %s", msg.MessageID, msg.Command, string(argBytes)) + } else { + dataStr = fmt.Sprintf("%d %s", msg.MessageID, msg.Command) + } + + return []byte(dataStr), websocket.TextFrame, nil +} + + +// Convenience method: Parse the arguments of the ClientMessage as a single string. +func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { + var ok bool + string1, ok = cm.Arguments.(string) + if !ok { + err = ExpectedSingleString; return + } else { + return string1, nil + } +} + +// Convenience method: Parse the arguments of the ClientMessage as an array of two strings. +func (cm *ClientMessage) ArgumentsAsTwoStrings() (string1, string2 string, err error) { + var ok bool + var ary []interface{} + ary, ok = cm.Arguments.([]interface{}) + if !ok { + err = ExpectedTwoStrings; return + } else { + if len(ary) != 2 { + err = ExpectedTwoStrings; return + } + string1, ok = ary[0].(string) + if !ok { + err = ExpectedTwoStrings; return + } + string2, ok = ary[1].(string) + if !ok { + err = ExpectedTwoStrings; return + } + return string1, string2, nil + } +} + +// Convenience method: Parse the arguments of the ClientMessage as an array of a string and an int. +func (cm *ClientMessage) ArgumentsAsStringAndInt() (string1 string, int int64, err error) { + var ok bool + var ary []interface{} + ary, ok = cm.Arguments.([]interface{}) + if !ok { + err = ExpectedStringAndInt; return + } else { + if len(ary) != 2 { + err = ExpectedStringAndInt; return + } + string1, ok = ary[0].(string) + if !ok { + err = ExpectedStringAndInt; return + } + var num float64 + num, ok = ary[1].(float64) + if !ok { + err = ExpectedStringAndInt; return + } + int = int64(num) + if float64(int) != num { + err = ExpectedStringAndIntGotFloat; return + } + return string1, int, nil + } +} diff --git a/socketserver/lib/handlecore_test.go b/socketserver/lib/handlecore_test.go new file mode 100644 index 00000000..a96cdbfa --- /dev/null +++ b/socketserver/lib/handlecore_test.go @@ -0,0 +1,37 @@ +package lib + +import ( + "golang.org/x/net/websocket" + "fmt" +) + +func ExampleUnmarshalClientMessage() { + sourceData := []byte("100 hello [\"ffz_3.5.30\",\"898b5bfa-b577-47bb-afb4-252c703b67d6\"]") + var cm ClientMessage + err := UnmarshalClientMessage(sourceData, websocket.TextFrame, &cm) + fmt.Println(err) + fmt.Println(cm.MessageID) + fmt.Println(cm.Command) + fmt.Println(cm.Arguments) + // Output: + // + // 100 + // hello + // [ffz_3.5.30 898b5bfa-b577-47bb-afb4-252c703b67d6] +} + +func ExampleMarshalClientMessage() { + var cm ClientMessage = ClientMessage{ + MessageID: -1, + Command: "do_authorize", + Arguments: "1234567890", + } + data, payloadType, err := MarshalClientMessage(&cm) + fmt.Println(err) + fmt.Println(payloadType == websocket.TextFrame) + fmt.Println(string(data)) + // Output: + // + // true + // -1 do_authorize "1234567890" +} diff --git a/src/socket.js b/src/socket.js index 30882bb0..2354a3fe 100644 --- a/src/socket.js +++ b/src/socket.js @@ -40,7 +40,7 @@ FFZ.prototype.ws_create = function() { this._ws_pending = this._ws_pending || []; try { - ws = this._ws_sock = new WebSocket("ws://" + constants.WS_SERVERs[this._ws_host_idx] + "/"); + ws = this._ws_sock = new WebSocket("ws://" + constants.WS_SERVERS[this._ws_host_idx] + "/"); } catch(err) { this._ws_exists = false; return this.log("Error Creating WebSocket: " + err); From 61b4ef33e3dffb07ad792be46d7645a3a5068baf Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 20:40:49 -0700 Subject: [PATCH 04/29] Implement recieve loop --- socketserver/lib/handlecore.go | 127 +++++++++++++++++++++++++++++---- src/socket.js | 12 +--- 2 files changed, 113 insertions(+), 26 deletions(-) diff --git a/socketserver/lib/handlecore.go b/socketserver/lib/handlecore.go index 4acf162a..4f272faf 100644 --- a/socketserver/lib/handlecore.go +++ b/socketserver/lib/handlecore.go @@ -9,7 +9,9 @@ import ( "strconv" "errors" "encoding/json" + "github.com/satori/go.uuid" "fmt" + "sync" ) const MAX_PACKET_SIZE = 1024 @@ -26,17 +28,48 @@ type Config struct { type Command string type ClientMessage struct { + // Message ID. Increments by 1 for each message sent from the client. + // When replying to a command, the message ID must be echoed. + // When sending a server-initiated message, this is -1. MessageID int // The command that the client wants from the server. // When sent from the server, the literal string 'True' indicates success. // Before sending, a blank Command will be converted into SuccessCommand. Command Command + // Arguments interface{} } +type ClientInfo struct { + // The client ID. + // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. + ClientID uuid.UUID + + // The client's version. + // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. + Version string + + // This mutex protects writable data in this struct. + // If it seems to be a performance problem, we can split this. + Mutex sync.Mutex + + // The list of chats this client is currently in. + // Protected by Mutex + CurrentChannels []string + + // Server-initiated messages should be sent here + MessageChannel chan<- ClientMessage +} + +// A function that is called to respond to a Command. +type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) *ClientMessage + +var CommandHandlers = make(map[Command]CommandHandler) + // Sent by the server in ClientMessage.Command to indicate success. const SuccessCommand Command = "True" +// A websocket.Codec that translates the protocol into ClientMessage objects. var FFZCodec websocket.Codec = websocket.Codec{ Marshal: MarshalClientMessage, Unmarshal: UnmarshalClientMessage, @@ -49,6 +82,7 @@ var ExpectedTwoStrings = errors.New("Error: Expected array of string, string as var ExpectedStringAndInt = errors.New("Error: Expected array of string, int as arguments.") var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.") +// Create a websocket.Server with the options from the provided Config. func SetupServer(config *Config) *websocket.Server { sockConf, err := websocket.NewConfig("/", config.Origin) if err != nil { @@ -85,24 +119,71 @@ func SetupServerAndHandle(config *Config) { // This runs in a goroutine started by net/http. func HandleSocketConnection(conn *websocket.Conn) { // websocket.Conn is a ReadWriteCloser - var msg ClientMessage - var err error = nil - var abort bool - log.Print("Got a connection from ", conn.RemoteAddr()) + closer := sync.Once(func() { + conn.Close() + }) - for ; err == nil || abort; err = FFZCodec.Receive(conn, &msg) { - log.Print(msg) + defer func() { + closer() + }() + + log.Print("! Got a connection from ", conn.RemoteAddr()) + + _clientChan := make(chan ClientMessage) + _serverMessageChan := make(chan ClientMessage) + _errorChan := make(chan error) + + // Receive goroutine + go func(errorChan chan<- error, clientChan chan<- ClientMessage) { + var msg ClientMessage + var err error + for ; err == nil; err = FFZCodec.Receive(conn, &msg) { + if msg.MessageID == 0 { + continue + } + clientChan <- msg + } + errorChan <- err + close(errorChan) + close(clientChan) + // exit + }(_errorChan, _clientChan) + + var client ClientInfo + client.MessageChannel = _serverMessageChan + + var errorChan <-chan error = _errorChan + var clientChan <-chan ClientMessage = _clientChan + var serverMessageChan <-chan ClientMessage = _serverMessageChan + + RunLoop: + for { + select { + case err := <-errorChan: + FFZCodec.Send(conn, ClientMessage{ Command: "error", Arguments: err.Error() }) + break RunLoop + case cmsg := <-clientChan: + handler, ok := CommandHandlers[cmsg.Command] + if !ok { + log.Print("[!] Unknown command", cmsg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) + // TODO - after commands are implemented + // closer() + continue + } + + client.Mutex.Lock() + response := handler(conn, &client, cmsg) + if response != nil { + response.MessageID = cmsg.MessageID + FFZCodec.Send(conn, response) + } + client.Mutex.Unlock() + case smsg := <-serverMessageChan: + FFZCodec.Send(conn, smsg) + } } - - if err != nil { - FFZCodec.Send(conn, ClientMessage{ - MessageID: -1, - Command: "error", - Arguments: err.Error(), - }) - } - conn.Close() + // exit } // Unpack a message sent from the client into a ClientMessage. @@ -155,9 +236,16 @@ func MarshalClientMessage(clientMessage interface{}) (data []byte, payloadType b } var dataStr string + if msg.Command == "" && msg.MessageID == 0 { + panic("MarshalClientMessage: attempt to send an empty ClientMessage") + } + if msg.Command == "" { msg.Command = SuccessCommand } + if msg.MessageID == 0 { + msg.MessageID = -1 + } if msg.Arguments != nil { argBytes, err := json.Marshal(msg.Arguments) @@ -173,6 +261,15 @@ func MarshalClientMessage(clientMessage interface{}) (data []byte, payloadType b return []byte(dataStr), websocket.TextFrame, nil } +// Command handlers should use this to construct responses. +func NewClientMessage(arguments interface{}) ClientMessage { + return ClientMessage{ + MessageID: 0, // filled by the select loop + Command: SuccessCommand, + Arguments: arguments, + } +} + // Convenience method: Parse the arguments of the ClientMessage as a single string. func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { diff --git a/src/socket.js b/src/socket.js index 2354a3fe..bc03b154 100644 --- a/src/socket.js +++ b/src/socket.js @@ -54,17 +54,7 @@ FFZ.prototype.ws_create = function() { f._ws_last_iframe = Date.now(); f.log("Socket connected."); - // Check for incognito. We don't want to do a hello in incognito mode. - var fs = window.RequestFileSystem || window.webkitRequestFileSystem; - if (!fs) - // Assume not. - f.ws_send("hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)); - - else - fs(window.TEMPORARY, 100, - f.ws_send.bind(f, "hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)), - f.log.bind(f, "Operating in Incognito Mode.")); - + f.ws_send("hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)); var user = f.get_user(); if ( user ) From 9b75026422d01d6453edea05727a407a04bd08ba Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 21:33:31 -0700 Subject: [PATCH 05/29] Add client handling for "error" messages --- socketserver/lib/commands.go | 1 + src/socket.js | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 socketserver/lib/commands.go diff --git a/socketserver/lib/commands.go b/socketserver/lib/commands.go new file mode 100644 index 00000000..55c21f80 --- /dev/null +++ b/socketserver/lib/commands.go @@ -0,0 +1 @@ +package lib diff --git a/src/socket.js b/src/socket.js index bc03b154..3e3d2092 100644 --- a/src/socket.js +++ b/src/socket.js @@ -160,6 +160,11 @@ FFZ.prototype.ws_create = function() { else f.log("Invalid command: " + cmd, data, false, true); + } else if ( cmd === "error" ) { + f.log("Socket server reported error: " + data); + if (f._ws_callbacks[request] ) + delete f._ws_callbacks[request]; + } else { var success = cmd === 'True', has_callback = typeof f._ws_callbacks[request] === "function"; @@ -174,7 +179,7 @@ FFZ.prototype.ws_create = function() { f.error("Callback for " + request + ": " + err); } - f._ws_callbacks[request] = undefined; + delete f._ws_callbacks[request]; } } } From 6681c1d64b72ce4eb1b94e71ade383f0fb06848c Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 21:33:55 -0700 Subject: [PATCH 06/29] Stub all command handlers on server --- socketserver/lib/commands.go | 84 +++++++++++++++++++++++++++++++++ socketserver/lib/handlecore.go | 85 ++++++++++++++++++++++++++++------ 2 files changed, 154 insertions(+), 15 deletions(-) diff --git a/socketserver/lib/commands.go b/socketserver/lib/commands.go index 55c21f80..317e1305 100644 --- a/socketserver/lib/commands.go +++ b/socketserver/lib/commands.go @@ -1 +1,85 @@ package lib +import ( + "golang.org/x/net/websocket" + "github.com/satori/go.uuid" + "log" +) + +func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + version, clientId, err := msg.ArgumentsAsTwoStrings() + + client.Version = version + client.ClientID = uuid.FromStringOrNil(clientId) + if client.ClientID == uuid.Nil { + client.ClientID = uuid.NewV4() + } + + return ClientMessage{ + Arguments: client.ClientID.String(), + }, nil +} + +func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleChatHistory(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + // Ignore, send empty history + return ClientMessage{Arguments: []string{}}, nil +} + +func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + log.Println("Ignoring survey response from", client.ClientID) + return ClientMessage{}, nil +} + +func HandleUpdateFollowButtons(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleTwitchEmote(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleGetLink(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} + +func HandleGetDisplayName(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ClientMessage{}, nil +} diff --git a/socketserver/lib/handlecore.go b/socketserver/lib/handlecore.go index 4f272faf..5edbb61f 100644 --- a/socketserver/lib/handlecore.go +++ b/socketserver/lib/handlecore.go @@ -62,12 +62,28 @@ type ClientInfo struct { } // A function that is called to respond to a Command. -type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) *ClientMessage +type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error) -var CommandHandlers = make(map[Command]CommandHandler) +var CommandHandlers = map[Command]CommandHandler { + HelloCommand: HandleHello, + "get_display_name": HandleGetDisplayName, + "sub": HandleSub, + "unsub": HandleUnsub, + "chat_history": HandleChatHistory, + "sub_channel": HandleSubChannel, + "unsub_channel": HandleUnsubChannel, + "setuser": HandleSetUser, + "update_follow_buttons": HandleUpdateFollowButtons, + "track_follow": HandleTrackFollow, + "emoticon_uses": HandleEmoticonUses, + "twitch_emote": HandleTwitchEmote, + "get_link": HandleGetLink, + "survey": HandleSurvey, +} // Sent by the server in ClientMessage.Command to indicate success. const SuccessCommand Command = "True" +const HelloCommand Command = "hello" // A websocket.Codec that translates the protocol into ClientMessage objects. var FFZCodec websocket.Codec = websocket.Codec{ @@ -120,9 +136,12 @@ func SetupServerAndHandle(config *Config) { func HandleSocketConnection(conn *websocket.Conn) { // websocket.Conn is a ReadWriteCloser - closer := sync.Once(func() { - conn.Close() - }) + var _closer sync.Once + closer := func() { + _closer.Do(func() { + conn.Close() + }) + } defer func() { closer() @@ -161,24 +180,46 @@ func HandleSocketConnection(conn *websocket.Conn) { for { select { case err := <-errorChan: - FFZCodec.Send(conn, ClientMessage{ Command: "error", Arguments: err.Error() }) + // note - socket might not be open at this point + // don't care + FFZCodec.Send(conn, ClientMessage{ + MessageID: -1, + Command: "error", + Arguments: err.Error(), + }) break RunLoop - case cmsg := <-clientChan: - handler, ok := CommandHandlers[cmsg.Command] + case msg := <-clientChan: + if client.Version == "" && msg.Command != HelloCommand { + FFZCodec.Send(conn, ClientMessage{ + MessageID: msg.MessageID, + Command: "error", + Arguments: "Error - the first message sent must be a 'hello'", + }) + break RunLoop + } + + handler, ok := CommandHandlers[msg.Command] if !ok { - log.Print("[!] Unknown command", cmsg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) - // TODO - after commands are implemented + log.Print("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) + // uncomment after commands are implemented // closer() continue } client.Mutex.Lock() - response := handler(conn, &client, cmsg) - if response != nil { - response.MessageID = cmsg.MessageID - FFZCodec.Send(conn, response) - } + response, err := CallHandler(handler, conn, &client, msg) client.Mutex.Unlock() + + if err == nil { + response.MessageID = msg.MessageID + FFZCodec.Send(conn, response) + } else { + FFZCodec.Send(conn, ClientMessage{ + MessageID: msg.MessageID, + Command: "error", + Arguments: err.Error(), + }) + } case smsg := <-serverMessageChan: FFZCodec.Send(conn, smsg) } @@ -186,6 +227,20 @@ func HandleSocketConnection(conn *websocket.Conn) { // exit } +func CallHandler(handler CommandHandler, conn *websocket.Conn, client *ClientInfo, cmsg ClientMessage) (rmsg ClientMessage, err error) { + defer func() { + if r := recover(); r != nil { + var ok bool + fmt.Print("[!] Error executing command", cmsg.Command, "--", r) + err, ok = r.(error) + if !ok { + err = fmt.Errorf("command handler: %v", r) + } + } + }() + return handler(conn, client, cmsg) +} + // Unpack a message sent from the client into a ClientMessage. func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err error) { var spaceIdx int From 9a1d1b720d95ba02654117a481fca8b24f510f50 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 21:42:16 -0700 Subject: [PATCH 07/29] Add TwitchUsername to ClientInfo --- socketserver/lib/commands.go | 6 ++++++ socketserver/lib/handlecore.go | 30 ++++++++++++++++++------------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/socketserver/lib/commands.go b/socketserver/lib/commands.go index 317e1305..bdbe2b7b 100644 --- a/socketserver/lib/commands.go +++ b/socketserver/lib/commands.go @@ -5,8 +5,13 @@ import ( "log" ) +var EmptyClientMessage ClientMessage = ClientMessage{} + func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { version, clientId, err := msg.ArgumentsAsTwoStrings() + if err != nil { + return EmptyClientMessage, nil + } client.Version = version client.ClientID = uuid.FromStringOrNil(clientId) @@ -20,6 +25,7 @@ func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (r } func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + username, err := msg.ArgumentsAsString() return ClientMessage{}, nil } diff --git a/socketserver/lib/handlecore.go b/socketserver/lib/handlecore.go index 5edbb61f..4b9537b5 100644 --- a/socketserver/lib/handlecore.go +++ b/socketserver/lib/handlecore.go @@ -32,8 +32,8 @@ type ClientMessage struct { // When replying to a command, the message ID must be echoed. // When sending a server-initiated message, this is -1. MessageID int - // The command that the client wants from the server. - // When sent from the server, the literal string 'True' indicates success. + // The command that the client wants from the server. + // When sent from the server, the literal string 'True' indicates success. // Before sending, a blank Command will be converted into SuccessCommand. Command Command // @@ -43,28 +43,34 @@ type ClientMessage struct { type ClientInfo struct { // The client ID. // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - ClientID uuid.UUID + ClientID uuid.UUID // The client's version. // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - Version string + Version string // This mutex protects writable data in this struct. // If it seems to be a performance problem, we can split this. - Mutex sync.Mutex + Mutex sync.Mutex + + // The client's claimed username on Twitch. + TwitchUsername string + + // Whether or not the server has validated the client's claimed username. + UsernameValidated bool // The list of chats this client is currently in. // Protected by Mutex - CurrentChannels []string + CurrentChannels []string // Server-initiated messages should be sent here - MessageChannel chan<- ClientMessage + MessageChannel chan <- ClientMessage } // A function that is called to respond to a Command. type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error) -var CommandHandlers = map[Command]CommandHandler { +var CommandHandlers = map[Command]CommandHandler{ HelloCommand: HandleHello, "get_display_name": HandleGetDisplayName, "sub": HandleSub, @@ -154,7 +160,7 @@ func HandleSocketConnection(conn *websocket.Conn) { _errorChan := make(chan error) // Receive goroutine - go func(errorChan chan<- error, clientChan chan<- ClientMessage) { + go func(errorChan chan <- error, clientChan chan <- ClientMessage) { var msg ClientMessage var err error for ; err == nil; err = FFZCodec.Receive(conn, &msg) { @@ -180,13 +186,11 @@ func HandleSocketConnection(conn *websocket.Conn) { for { select { case err := <-errorChan: - // note - socket might not be open at this point - // don't care FFZCodec.Send(conn, ClientMessage{ MessageID: -1, Command: "error", Arguments: err.Error(), - }) + }) // note - socket might be closed, but don't care break RunLoop case msg := <-clientChan: if client.Version == "" && msg.Command != HelloCommand { @@ -206,6 +210,8 @@ func HandleSocketConnection(conn *websocket.Conn) { continue } + log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) + client.Mutex.Lock() response, err := CallHandler(handler, conn, &client, msg) client.Mutex.Unlock() From 93c3f6f6723badfdf5505cc91bf7f2a46db807d8 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sat, 24 Oct 2015 22:38:04 -0700 Subject: [PATCH 08/29] Enumerate possible get_link responses --- socketserver/broadcaster/subscriptions.go | 2 + socketserver/cmd/socketserver/socketserver.go | 6 +- socketserver/lib/commands.go | 91 -------- socketserver/lib/utils.go | 34 +++ socketserver/listener/commands.go | 194 ++++++++++++++++++ socketserver/{lib => listener}/handlecore.go | 14 +- .../{lib => listener}/handlecore_test.go | 2 +- src/tokenize.js | 2 +- 8 files changed, 247 insertions(+), 98 deletions(-) create mode 100644 socketserver/broadcaster/subscriptions.go delete mode 100644 socketserver/lib/commands.go create mode 100644 socketserver/lib/utils.go create mode 100644 socketserver/listener/commands.go rename socketserver/{lib => listener}/handlecore.go (94%) rename socketserver/{lib => listener}/handlecore_test.go (97%) diff --git a/socketserver/broadcaster/subscriptions.go b/socketserver/broadcaster/subscriptions.go new file mode 100644 index 00000000..ee810828 --- /dev/null +++ b/socketserver/broadcaster/subscriptions.go @@ -0,0 +1,2 @@ +package broadcaster + diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index b898617e..fb75c2e9 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -2,7 +2,7 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/sock import ( "flag" - "../../lib" + "../../listener" "log" "net/http" ) @@ -25,7 +25,7 @@ func main() { log.Fatalln("Either both --crt and --key can be provided, or neither.") } - conf := &lib.Config { + conf := &listener.Config { SSLKeyFile: *privateKeyFile, SSLCertificateFile: *certificateFile, UseSSL: *certificateFile != "", @@ -33,7 +33,7 @@ func main() { Origin: *origin, } - lib.SetupServerAndHandle(conf) + listener.SetupServerAndHandle(conf) var err error if conf.UseSSL { diff --git a/socketserver/lib/commands.go b/socketserver/lib/commands.go deleted file mode 100644 index bdbe2b7b..00000000 --- a/socketserver/lib/commands.go +++ /dev/null @@ -1,91 +0,0 @@ -package lib -import ( - "golang.org/x/net/websocket" - "github.com/satori/go.uuid" - "log" -) - -var EmptyClientMessage ClientMessage = ClientMessage{} - -func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - version, clientId, err := msg.ArgumentsAsTwoStrings() - if err != nil { - return EmptyClientMessage, nil - } - - client.Version = version - client.ClientID = uuid.FromStringOrNil(clientId) - if client.ClientID == uuid.Nil { - client.ClientID = uuid.NewV4() - } - - return ClientMessage{ - Arguments: client.ClientID.String(), - }, nil -} - -func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - username, err := msg.ArgumentsAsString() - - return ClientMessage{}, nil -} - -func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleChatHistory(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - // Ignore, send empty history - return ClientMessage{Arguments: []string{}}, nil -} - -func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - log.Println("Ignoring survey response from", client.ClientID) - return ClientMessage{}, nil -} - -func HandleUpdateFollowButtons(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleTwitchEmote(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleGetLink(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} - -func HandleGetDisplayName(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ClientMessage{}, nil -} diff --git a/socketserver/lib/utils.go b/socketserver/lib/utils.go new file mode 100644 index 00000000..1b9af86c --- /dev/null +++ b/socketserver/lib/utils.go @@ -0,0 +1,34 @@ +package lib + +import ( +) + +func AddToSliceS(ary *[]string, val string) { + slice := *ary + for _, v := range slice { + if v == val { + return + } + } + + slice = append(slice, val) + *ary = slice +} + +func RemoveFromSliceS(ary *[]string, val string) { + slice := *ary + var idx int = -1 + for i, v := range slice { + if v == val { + idx = i + break + } + } + if idx == -1 { + return + } + + slice[idx] = slice[len(slice) - 1] + slice = slice[:len(slice) - 1] + *ary = slice +} diff --git a/socketserver/listener/commands.go b/socketserver/listener/commands.go new file mode 100644 index 00000000..06595612 --- /dev/null +++ b/socketserver/listener/commands.go @@ -0,0 +1,194 @@ +package listener + +import ( + "golang.org/x/net/websocket" + "github.com/satori/go.uuid" + "log" + "../lib" +) + +var ResponseSuccess = ClientMessage{Command: SuccessCommand} +var ResponseFailure = ClientMessage{Command: "False"} + +func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + version, clientId, err := msg.ArgumentsAsTwoStrings() + if err != nil { + return + } + + client.Version = version + client.ClientID = uuid.FromStringOrNil(clientId) + if client.ClientID == uuid.Nil { + client.ClientID = uuid.NewV4() + } + + return ClientMessage{ + Arguments: client.ClientID.String(), + }, nil +} + +func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + username, err := msg.ArgumentsAsString() + if err != nil { + return + } + + client.TwitchUsername = username + client.UsernameValidated = false + + return ResponseSuccess, nil +} + +func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + lib.AddToSliceS(&client.CurrentChannels, channel) + + return ResponseSuccess, nil +} + +func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + lib.RemoveFromSliceS(&client.CurrentChannels, channel) + + return ResponseSuccess, nil +} + +func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + lib.AddToSliceS(&client.WatchingChannels, channel) + + return ResponseSuccess, nil +} + +func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + lib.RemoveFromSliceS(&client.WatchingChannels, channel) + + return ResponseSuccess, nil +} + +func HandleChatHistory(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, count, err := msg.ArgumentsAsStringAndInt() + + _ = channel + _ = count + + // Ignore, send empty history + return ClientMessage{Arguments: []string{}}, nil +} + +func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + log.Println("Ignoring survey response from", client.ClientID) + return ResponseSuccess, nil +} + +func HandleUpdateFollowButtons(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + // TODO + return ResponseFailure, nil +} + +func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +type EmoteData struct { + +} + +func HandleTwitchEmote(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +type LinkResponse struct { + // If true, the link will be colored red. + Unsafe bool `json:unsafe` + // If present, the provided HTML will be shown as the link tooltip. + TooltipHTML string `json:tooltip,omitempty` + + // Each of the LinkTypes have a special rendering on the client. + Type LinkType `json:type,omitempty` + + // A URL to an image to embed. + // Recognized by several LinkTypes, as well as the empty LinkType + Full string `json:full,omitempty` + + // For LinkTypeYoutube, LinkTypeStrawpoll, LinkTypeTwitchVod + Title string `json:title,omitempty` + // For LinkTypeYoutube + Channel string `json:channel,omitempty` + // For LinkTypeYoutube + // Seconds + Duration int `json:duration,omitempty` + // For LinkTypeYoutube, LinkTypeTwitch, LinkTypeTwitchVod + Views int `json:views,omitempty` + // For LinkTypeYoutube + Likes int `json:likes,omitempty` + // For LinkTypeStrawpoll + Items map[string]int `json:items,omitempty` + // For LinkTypeStrawpoll + Total int `json:total,omitempty` + // For LinkTypeStrawpoll + // TODO - what time format is this + Fetched string `json:fetched,omitempty` + // For LinkTypeTwitch, LinkTypeTwitchVod + DisplayName string `json:display_name,omitempty` + // For LinkTypeTwitch + // TODO - what time format is this + Since string `json:since,omitempty` + // For LinkTypeTwitch + Followers int `json:followers,omitempty` + // For LinkTypeTwitchVod + BroadcastType string `json:broadcast_type,omitempty` + // For LinkTypeTwitchVod + Game string `json:game,omitempty` + // For LinkTypeTwitchVod + // Seconds + Length int `json:length,omitempty` + // For LinkTypeTwitter + User string `json:user,omitempty` + // For LinkTypeTwitter + Tweet string `json:tweet,omitempty` + // For LinkTypeReputation + Trust int `json:trust,omitempty` + // For LinkTypeReputation + Safety int `json:safety,omitempty` +} + +type LinkType string + +const ( + LinkTypeYoutube = "youtube" + LinkTypeStrawpoll = "strawpoll" + LinkTypeTwitch = "twitch" + LinkTypeTwitchVod = "twitch_vod" + LinkTypeTwitter = "twitter" + LinkTypeReputation = "reputation" + LinkTypeShortened = "shortened" // proposed +) +const ( + BroadcastTypeHighlight = "highlight" + BroadcastTypeFull = "broadcast" +) + +func HandleGetLink(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + // TODO + return ResponseFailure, nil +} + +func HandleGetDisplayName(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + // TODO + return ResponseFailure, nil +} diff --git a/socketserver/lib/handlecore.go b/socketserver/listener/handlecore.go similarity index 94% rename from socketserver/lib/handlecore.go rename to socketserver/listener/handlecore.go index 4b9537b5..1bb1a1c3 100644 --- a/socketserver/lib/handlecore.go +++ b/socketserver/listener/handlecore.go @@ -1,4 +1,4 @@ -package lib // import "bitbucket.org/stendec/frankerfacez/socketserver/lib" +package listener // import "bitbucket.org/stendec/frankerfacez/socketserver/listener" import ( "net/http" @@ -63,6 +63,10 @@ type ClientInfo struct { // Protected by Mutex CurrentChannels []string + // This list of channels this client needs UI updates for. + // Protected by Mutex + WatchingChannels []string + // Server-initiated messages should be sent here MessageChannel chan <- ClientMessage } @@ -89,7 +93,11 @@ var CommandHandlers = map[Command]CommandHandler{ // Sent by the server in ClientMessage.Command to indicate success. const SuccessCommand Command = "True" +// This must be the first command sent by the client once the connection is established. const HelloCommand Command = "hello" +// A handler returning a ClientMessage with this Command will prevent replying to the client. +// It signals that the work has been handed off to a background goroutine. +const AsyncResponseCommand Command = "_async" // A websocket.Codec that translates the protocol into ClientMessage objects. var FFZCodec websocket.Codec = websocket.Codec{ @@ -219,6 +227,9 @@ func HandleSocketConnection(conn *websocket.Conn) { if err == nil { response.MessageID = msg.MessageID FFZCodec.Send(conn, response) + } else if response.Command == AsyncResponseCommand { + // Don't send anything + // The response will be delivered over client.MessageChannel / serverMessageChan } else { FFZCodec.Send(conn, ClientMessage{ MessageID: msg.MessageID, @@ -331,7 +342,6 @@ func NewClientMessage(arguments interface{}) ClientMessage { } } - // Convenience method: Parse the arguments of the ClientMessage as a single string. func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { var ok bool diff --git a/socketserver/lib/handlecore_test.go b/socketserver/listener/handlecore_test.go similarity index 97% rename from socketserver/lib/handlecore_test.go rename to socketserver/listener/handlecore_test.go index a96cdbfa..f5a0648f 100644 --- a/socketserver/lib/handlecore_test.go +++ b/socketserver/listener/handlecore_test.go @@ -1,4 +1,4 @@ -package lib +package listener import ( "golang.org/x/net/websocket" diff --git a/src/tokenize.js b/src/tokenize.js index e24c4b46..b702eb83 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -201,7 +201,7 @@ var FFZ = window.FrankerFaceZ, return; this._link_data[href] = data; - data.unsafe = false; + //data.unsafe = false; var tooltip = build_link_tooltip.bind(this)(href), links, no_trail = href.charAt(href.length-1) == "/" ? href.substr(0, href.length-1) : null; From 2a6c36bba5d2eb1a5ad3d3ec1d94393457c64989 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sun, 25 Oct 2015 00:44:25 -0700 Subject: [PATCH 09/29] rename listener package to internal/server --- socketserver/broadcaster/subscriptions.go | 2 - socketserver/cmd/socketserver/socketserver.go | 8 +- socketserver/internal/server/backend.go | 72 +++++++ socketserver/internal/server/commands.go | 134 ++++++++++++ .../server}/handlecore.go | 137 +++++-------- .../server}/handlecore_test.go | 22 +- socketserver/internal/server/types.go | 59 ++++++ .../{lib => internal/server}/utils.go | 2 +- socketserver/listener/commands.go | 194 ------------------ 9 files changed, 346 insertions(+), 284 deletions(-) delete mode 100644 socketserver/broadcaster/subscriptions.go create mode 100644 socketserver/internal/server/backend.go create mode 100644 socketserver/internal/server/commands.go rename socketserver/{listener => internal/server}/handlecore.go (75%) rename socketserver/{listener => internal/server}/handlecore_test.go (61%) create mode 100644 socketserver/internal/server/types.go rename socketserver/{lib => internal/server}/utils.go (96%) delete mode 100644 socketserver/listener/commands.go diff --git a/socketserver/broadcaster/subscriptions.go b/socketserver/broadcaster/subscriptions.go deleted file mode 100644 index ee810828..00000000 --- a/socketserver/broadcaster/subscriptions.go +++ /dev/null @@ -1,2 +0,0 @@ -package broadcaster - diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index fb75c2e9..0ab2a278 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -2,7 +2,7 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/sock import ( "flag" - "../../listener" + "../../internal/server" "log" "net/http" ) @@ -25,15 +25,15 @@ func main() { log.Fatalln("Either both --crt and --key can be provided, or neither.") } - conf := &listener.Config { + conf := &server.Config { SSLKeyFile: *privateKeyFile, SSLCertificateFile: *certificateFile, UseSSL: *certificateFile != "", - Origin: *origin, + SocketOrigin: *origin, } - listener.SetupServerAndHandle(conf) + server.SetupServerAndHandle(conf) var err error if conf.UseSSL { diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go new file mode 100644 index 00000000..ba8b4f06 --- /dev/null +++ b/socketserver/internal/server/backend.go @@ -0,0 +1,72 @@ +package server + +import ( + "net/http" + "time" + "fmt" + "net/url" + "github.com/pmylund/go-cache" + "strconv" + "io/ioutil" +) + +var httpClient http.Client +var backendUrl string +var responseCache *cache.Cache + +func SetupBackend(url string) { + httpClient.Timeout = 60 * time.Second + backendUrl = url + responseCache = cache.New(60 * time.Second, 120 * time.Second) +} + +func getCacheKey(remoteCommand, data string) string { + return fmt.Sprintf("%s/%s", remoteCommand, data) +} + +func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) { + cached, ok := responseCache.Get(getCacheKey(remoteCommand, data)) + if ok { + return cached.(string), nil + } + return RequestRemoteData(remoteCommand, data, auth) +} + +func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error) { + destUrl := fmt.Sprintf("%s/%s", backendUrl, remoteCommand) + var authKey string + if auth.UsernameValidated { + authKey = "usernameClaimed" + } else { + authKey = "username" + } + + formData := url.Values{ + "clientData": []string{data}, + authKey: []string{auth.TwitchUsername}, + } + + resp, err := httpClient.PostForm(destUrl, formData) + if err != nil { + return "", err + } + + respBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return "", err + } + + responseJson := string(respBytes) + + if resp.Header.Get("FFZ-Cache") != "" { + durSecs, err := strconv.ParseInt(resp.Header.Get("FFZ-Cache"), 10, 64) + if err != nil { + return "", fmt.Errorf("The RPC server returned a non-integer cache duration: %v", err) + } + duration := time.Duration(durSecs) * time.Second + responseCache.Set(getCacheKey(remoteCommand, data), responseJson, duration) + } + + return responseJson, nil +} \ No newline at end of file diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go new file mode 100644 index 00000000..2b4d0c43 --- /dev/null +++ b/socketserver/internal/server/commands.go @@ -0,0 +1,134 @@ +package server + +import ( + "golang.org/x/net/websocket" + "github.com/satori/go.uuid" + "log" +) + +var ResponseSuccess = ClientMessage{Command: SuccessCommand} +var ResponseFailure = ClientMessage{Command: "False"} + +func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) { + handler, ok := CommandHandlers[msg.Command] + if !ok { + log.Print("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) + // uncomment after commands are implemented + // closer() + return + } + + log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) + + client.Mutex.Lock() + response, err := CallHandler(handler, conn, client, msg) + client.Mutex.Unlock() + + if err == nil { + response.MessageID = msg.MessageID + FFZCodec.Send(conn, response) + } else if response.Command == AsyncResponseCommand { + // Don't send anything + // The response will be delivered over client.MessageChannel / serverMessageChan + } else { + FFZCodec.Send(conn, ClientMessage{ + MessageID: msg.MessageID, + Command: "error", + Arguments: err.Error(), + }) + } +} + +func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + version, clientId, err := msg.ArgumentsAsTwoStrings() + if err != nil { + return + } + + client.Version = version + client.ClientID = uuid.FromStringOrNil(clientId) + if client.ClientID == uuid.Nil { + client.ClientID = uuid.NewV4() + } + + return ClientMessage{ + Arguments: client.ClientID.String(), + }, nil +} + +func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + username, err := msg.ArgumentsAsString() + if err != nil { + return + } + + client.TwitchUsername = username + client.UsernameValidated = false + + return ResponseSuccess, nil +} + +func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + AddToSliceS(&client.CurrentChannels, channel) + + // TODO - get backlog + + return ResponseSuccess, nil +} + +func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + RemoveFromSliceS(&client.CurrentChannels, channel) + + return ResponseSuccess, nil +} + +func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + AddToSliceS(&client.WatchingChannels, channel) + + // TODO - get backlog + + return ResponseSuccess, nil +} + +func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, err := msg.ArgumentsAsString() + + RemoveFromSliceS(&client.WatchingChannels, channel) + + return ResponseSuccess, nil +} + +func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + log.Println("Ignoring survey response from", client.ClientID) + return ResponseSuccess, nil +} + +func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + + return ResponseSuccess, nil +} + +func HandleRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + go func(conn *websocket.Conn, msg ClientMessage, authInfo AuthInfo) { + resp, err := RequestRemoteDataCached(string(msg.Command), msg.origArguments, authInfo) + + if err != nil { + FFZCodec.Send(conn, ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: err.Error()}) + } else { + FFZCodec.Send(conn, ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand, origArguments: resp}) + } + }(conn, msg, client.AuthInfo) + + return ClientMessage{Command: AsyncResponseCommand}, nil +} diff --git a/socketserver/listener/handlecore.go b/socketserver/internal/server/handlecore.go similarity index 75% rename from socketserver/listener/handlecore.go rename to socketserver/internal/server/handlecore.go index 1bb1a1c3..07abc425 100644 --- a/socketserver/listener/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -1,4 +1,4 @@ -package listener // import "bitbucket.org/stendec/frankerfacez/socketserver/listener" +package server // import "bitbucket.org/stendec/frankerfacez/socketserver/server" import ( "net/http" @@ -9,7 +9,6 @@ import ( "strconv" "errors" "encoding/json" - "github.com/satori/go.uuid" "fmt" "sync" ) @@ -21,78 +20,39 @@ type Config struct { SSLKeyFile string UseSSL bool - Origin string + SocketOrigin string } // A command is how the client refers to a function on the server. It's just a string. type Command string -type ClientMessage struct { - // Message ID. Increments by 1 for each message sent from the client. - // When replying to a command, the message ID must be echoed. - // When sending a server-initiated message, this is -1. - MessageID int - // The command that the client wants from the server. - // When sent from the server, the literal string 'True' indicates success. - // Before sending, a blank Command will be converted into SuccessCommand. - Command Command - // - Arguments interface{} -} - -type ClientInfo struct { - // The client ID. - // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - ClientID uuid.UUID - - // The client's version. - // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - Version string - - // This mutex protects writable data in this struct. - // If it seems to be a performance problem, we can split this. - Mutex sync.Mutex - - // The client's claimed username on Twitch. - TwitchUsername string - - // Whether or not the server has validated the client's claimed username. - UsernameValidated bool - - // The list of chats this client is currently in. - // Protected by Mutex - CurrentChannels []string - - // This list of channels this client needs UI updates for. - // Protected by Mutex - WatchingChannels []string - - // Server-initiated messages should be sent here - MessageChannel chan <- ClientMessage -} - // A function that is called to respond to a Command. type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error) var CommandHandlers = map[Command]CommandHandler{ HelloCommand: HandleHello, - "get_display_name": HandleGetDisplayName, + "setuser": HandleSetUser, + "sub": HandleSub, "unsub": HandleUnsub, - "chat_history": HandleChatHistory, "sub_channel": HandleSubChannel, "unsub_channel": HandleUnsubChannel, - "setuser": HandleSetUser, - "update_follow_buttons": HandleUpdateFollowButtons, + "track_follow": HandleTrackFollow, "emoticon_uses": HandleEmoticonUses, - "twitch_emote": HandleTwitchEmote, - "get_link": HandleGetLink, "survey": HandleSurvey, + + "twitch_emote": HandleRemoteCommand, + "get_link": HandleRemoteCommand, + "get_display_name": HandleRemoteCommand, + "update_follow_buttons": HandleRemoteCommand, + "chat_history": HandleRemoteCommand, } // Sent by the server in ClientMessage.Command to indicate success. const SuccessCommand Command = "True" +// Sent by the server in ClientMessage.Command to indicate failure. +const ErrorCommand Command = "error" // This must be the first command sent by the client once the connection is established. const HelloCommand Command = "hello" // A handler returning a ClientMessage with this Command will prevent replying to the client. @@ -108,13 +68,15 @@ var FFZCodec websocket.Codec = websocket.Codec{ // Errors that get returned to the client. var ProtocolError error = errors.New("FFZ Socket protocol error.") var ExpectedSingleString = errors.New("Error: Expected single string as arguments.") +var ExpectedSingleInt = errors.New("Error: Expected single integer as arguments.") var ExpectedTwoStrings = errors.New("Error: Expected array of string, string as arguments.") var ExpectedStringAndInt = errors.New("Error: Expected array of string, int as arguments.") +var ExpectedStringAndBool = errors.New("Error: Expected array of string, bool as arguments.") var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.") // Create a websocket.Server with the options from the provided Config. func SetupServer(config *Config) *websocket.Server { - sockConf, err := websocket.NewConfig("/", config.Origin) + sockConf, err := websocket.NewConfig("/", config.SocketOrigin) if err != nil { panic(err) } @@ -125,7 +87,7 @@ func SetupServer(config *Config) *websocket.Server { } tlsConf := &tls.Config{ Certificates: []tls.Certificate{cert}, - ServerName: config.Origin, + ServerName: config.SocketOrigin, } tlsConf.BuildNameToCertificate() sockConf.TlsConfig = tlsConf @@ -210,33 +172,7 @@ func HandleSocketConnection(conn *websocket.Conn) { break RunLoop } - handler, ok := CommandHandlers[msg.Command] - if !ok { - log.Print("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) - // uncomment after commands are implemented - // closer() - continue - } - - log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) - - client.Mutex.Lock() - response, err := CallHandler(handler, conn, &client, msg) - client.Mutex.Unlock() - - if err == nil { - response.MessageID = msg.MessageID - FFZCodec.Send(conn, response) - } else if response.Command == AsyncResponseCommand { - // Don't send anything - // The response will be delivered over client.MessageChannel / serverMessageChan - } else { - FFZCodec.Send(conn, ClientMessage{ - MessageID: msg.MessageID, - Command: "error", - Arguments: err.Error(), - }) - } + HandleCommand(conn, &client, msg) case smsg := <-serverMessageChan: FFZCodec.Send(conn, smsg) } @@ -288,6 +224,7 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e } dataStr = dataStr[spaceIdx + 1:] argumentsJson := dataStr + out.origArguments = argumentsJson err = json.Unmarshal([]byte(argumentsJson), &out.Arguments) if err != nil { return @@ -353,6 +290,19 @@ func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { } } +// Convenience method: Parse the arguments of the ClientMessage as a single int. +func (cm *ClientMessage) ArgumentsAsInt() (int1 int, err error) { + var ok bool + var num float64 + num, ok = cm.Arguments.(float64) + if !ok { + err = ExpectedSingleInt; return + } else { + int1 = int(num) + return int1, nil + } +} + // Convenience method: Parse the arguments of the ClientMessage as an array of two strings. func (cm *ClientMessage) ArgumentsAsTwoStrings() (string1, string2 string, err error) { var ok bool @@ -403,3 +353,26 @@ func (cm *ClientMessage) ArgumentsAsStringAndInt() (string1 string, int int64, e return string1, int, nil } } + +// Convenience method: Parse the arguments of the ClientMessage as an array of a string and an int. +func (cm *ClientMessage) ArgumentsAsStringAndBool() (str string, flag bool, err error) { + var ok bool + var ary []interface{} + ary, ok = cm.Arguments.([]interface{}) + if !ok { + err = ExpectedStringAndBool; return + } else { + if len(ary) != 2 { + err = ExpectedStringAndBool; return + } + str, ok = ary[0].(string) + if !ok { + err = ExpectedStringAndBool; return + } + flag, ok = ary[1].(bool) + if !ok { + err = ExpectedStringAndBool; return + } + return str, flag, nil + } +} diff --git a/socketserver/listener/handlecore_test.go b/socketserver/internal/server/handlecore_test.go similarity index 61% rename from socketserver/listener/handlecore_test.go rename to socketserver/internal/server/handlecore_test.go index f5a0648f..0350a7e3 100644 --- a/socketserver/listener/handlecore_test.go +++ b/socketserver/internal/server/handlecore_test.go @@ -1,8 +1,9 @@ -package listener +package server import ( "golang.org/x/net/websocket" "fmt" + "testing" ) func ExampleUnmarshalClientMessage() { @@ -35,3 +36,22 @@ func ExampleMarshalClientMessage() { // true // -1 do_authorize "1234567890" } + +func TestArgumentsAsStringAndBool(t *testing.T) { + sourceData := []byte("1 foo [\"string\", false]") + var cm ClientMessage + err := UnmarshalClientMessage(sourceData, websocket.TextFrame, &cm) + if err != nil { + t.Fatal(err) + } + str, boolean, err := cm.ArgumentsAsStringAndBool() + if err != nil { + t.Fatal(err) + } + if str != "string" { + t.Error("Expected first array item to be 'string', got", str) + } + if boolean != false { + t.Error("Expected second array item to be false, got", boolean) + } +} \ No newline at end of file diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go new file mode 100644 index 00000000..3559de81 --- /dev/null +++ b/socketserver/internal/server/types.go @@ -0,0 +1,59 @@ +package server + +import ( + "github.com/satori/go.uuid" + "sync" +) + +type ClientMessage struct { + // Message ID. Increments by 1 for each message sent from the client. + // When replying to a command, the message ID must be echoed. + // When sending a server-initiated message, this is -1. + MessageID int + // The command that the client wants from the server. + // When sent from the server, the literal string 'True' indicates success. + // Before sending, a blank Command will be converted into SuccessCommand. + Command Command + // Result of json.Unmarshal on the third field send from the client + Arguments interface{} + + origArguments string +} + +type AuthInfo struct { + // The client's claimed username on Twitch. + TwitchUsername string + + // Whether or not the server has validated the client's claimed username. + UsernameValidated bool +} + +type ClientInfo struct { + // The client ID. + // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. + ClientID uuid.UUID + + // The client's version. + // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. + Version string + + // This mutex protects writable data in this struct. + // If it seems to be a performance problem, we can split this. + Mutex sync.Mutex + + AuthInfo + + // Username validation nonce. + ValidationNonce string + + // The list of chats this client is currently in. + // Protected by Mutex + CurrentChannels []string + + // This list of channels this client needs UI updates for. + // Protected by Mutex + WatchingChannels []string + + // Server-initiated messages should be sent here + MessageChannel chan <- ClientMessage +} \ No newline at end of file diff --git a/socketserver/lib/utils.go b/socketserver/internal/server/utils.go similarity index 96% rename from socketserver/lib/utils.go rename to socketserver/internal/server/utils.go index 1b9af86c..65fa9cae 100644 --- a/socketserver/lib/utils.go +++ b/socketserver/internal/server/utils.go @@ -1,4 +1,4 @@ -package lib +package server import ( ) diff --git a/socketserver/listener/commands.go b/socketserver/listener/commands.go deleted file mode 100644 index 06595612..00000000 --- a/socketserver/listener/commands.go +++ /dev/null @@ -1,194 +0,0 @@ -package listener - -import ( - "golang.org/x/net/websocket" - "github.com/satori/go.uuid" - "log" - "../lib" -) - -var ResponseSuccess = ClientMessage{Command: SuccessCommand} -var ResponseFailure = ClientMessage{Command: "False"} - -func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - version, clientId, err := msg.ArgumentsAsTwoStrings() - if err != nil { - return - } - - client.Version = version - client.ClientID = uuid.FromStringOrNil(clientId) - if client.ClientID == uuid.Nil { - client.ClientID = uuid.NewV4() - } - - return ClientMessage{ - Arguments: client.ClientID.String(), - }, nil -} - -func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - username, err := msg.ArgumentsAsString() - if err != nil { - return - } - - client.TwitchUsername = username - client.UsernameValidated = false - - return ResponseSuccess, nil -} - -func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.AddToSliceS(&client.CurrentChannels, channel) - - return ResponseSuccess, nil -} - -func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.RemoveFromSliceS(&client.CurrentChannels, channel) - - return ResponseSuccess, nil -} - -func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.AddToSliceS(&client.WatchingChannels, channel) - - return ResponseSuccess, nil -} - -func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - lib.RemoveFromSliceS(&client.WatchingChannels, channel) - - return ResponseSuccess, nil -} - -func HandleChatHistory(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, count, err := msg.ArgumentsAsStringAndInt() - - _ = channel - _ = count - - // Ignore, send empty history - return ClientMessage{Arguments: []string{}}, nil -} - -func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - log.Println("Ignoring survey response from", client.ClientID) - return ResponseSuccess, nil -} - -func HandleUpdateFollowButtons(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - // TODO - return ResponseFailure, nil -} - -func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ResponseSuccess, nil -} - -func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ResponseSuccess, nil -} - -type EmoteData struct { - -} - -func HandleTwitchEmote(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - return ResponseSuccess, nil -} - -type LinkResponse struct { - // If true, the link will be colored red. - Unsafe bool `json:unsafe` - // If present, the provided HTML will be shown as the link tooltip. - TooltipHTML string `json:tooltip,omitempty` - - // Each of the LinkTypes have a special rendering on the client. - Type LinkType `json:type,omitempty` - - // A URL to an image to embed. - // Recognized by several LinkTypes, as well as the empty LinkType - Full string `json:full,omitempty` - - // For LinkTypeYoutube, LinkTypeStrawpoll, LinkTypeTwitchVod - Title string `json:title,omitempty` - // For LinkTypeYoutube - Channel string `json:channel,omitempty` - // For LinkTypeYoutube - // Seconds - Duration int `json:duration,omitempty` - // For LinkTypeYoutube, LinkTypeTwitch, LinkTypeTwitchVod - Views int `json:views,omitempty` - // For LinkTypeYoutube - Likes int `json:likes,omitempty` - // For LinkTypeStrawpoll - Items map[string]int `json:items,omitempty` - // For LinkTypeStrawpoll - Total int `json:total,omitempty` - // For LinkTypeStrawpoll - // TODO - what time format is this - Fetched string `json:fetched,omitempty` - // For LinkTypeTwitch, LinkTypeTwitchVod - DisplayName string `json:display_name,omitempty` - // For LinkTypeTwitch - // TODO - what time format is this - Since string `json:since,omitempty` - // For LinkTypeTwitch - Followers int `json:followers,omitempty` - // For LinkTypeTwitchVod - BroadcastType string `json:broadcast_type,omitempty` - // For LinkTypeTwitchVod - Game string `json:game,omitempty` - // For LinkTypeTwitchVod - // Seconds - Length int `json:length,omitempty` - // For LinkTypeTwitter - User string `json:user,omitempty` - // For LinkTypeTwitter - Tweet string `json:tweet,omitempty` - // For LinkTypeReputation - Trust int `json:trust,omitempty` - // For LinkTypeReputation - Safety int `json:safety,omitempty` -} - -type LinkType string - -const ( - LinkTypeYoutube = "youtube" - LinkTypeStrawpoll = "strawpoll" - LinkTypeTwitch = "twitch" - LinkTypeTwitchVod = "twitch_vod" - LinkTypeTwitter = "twitter" - LinkTypeReputation = "reputation" - LinkTypeShortened = "shortened" // proposed -) -const ( - BroadcastTypeHighlight = "highlight" - BroadcastTypeFull = "broadcast" -) - -func HandleGetLink(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - // TODO - return ResponseFailure, nil -} - -func HandleGetDisplayName(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - - // TODO - return ResponseFailure, nil -} From d4afc3c4c76ff2a7219dcacd0f82bf6474dee5b8 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sun, 25 Oct 2015 00:58:05 -0700 Subject: [PATCH 10/29] store data from emoticon_uses command --- socketserver/internal/server/commands.go | 32 ++++++++++++++++++++++ socketserver/internal/server/handlecore.go | 3 ++ 2 files changed, 35 insertions(+) diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index 2b4d0c43..e8dabbc7 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -4,6 +4,8 @@ import ( "golang.org/x/net/websocket" "github.com/satori/go.uuid" "log" + "sync" + "strconv" ) var ResponseSuccess = ClientMessage{Command: SuccessCommand} @@ -114,7 +116,37 @@ func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessa return ResponseSuccess, nil } +var AggregateEmoteUsage map[int]map[string]int = make(map[int]map[string]int) +var AggregateEmoteUsageLock sync.Mutex + func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + // arguments is [1]map[EmoteId]map[RoomName]float64 + + mapRoot := msg.Arguments.([]interface{})[0].(map[string]interface{}) + + AggregateEmoteUsageLock.Lock() + defer AggregateEmoteUsageLock.Unlock() + + for strEmote, val1 := range mapRoot { + var emoteId int + emoteId, err = strconv.Atoi(strEmote) + if err != nil { + return + } + + destMapInner, ok := AggregateEmoteUsage[emoteId] + if !ok { + destMapInner = make(map[string]int) + AggregateEmoteUsage[emoteId] = destMapInner + } + + mapInner := val1.(map[string]interface{}) + for roomName, val2 := range mapInner { + var count int = int(val2.(float64)) + destMapInner[roomName] += count + } + } + return ResponseSuccess, nil } diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 07abc425..15ba4312 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -21,6 +21,7 @@ type Config struct { UseSSL bool SocketOrigin string + BackendUrl string } // A command is how the client refers to a function on the server. It's just a string. @@ -96,6 +97,8 @@ func SetupServer(config *Config) *websocket.Server { sockServer := &websocket.Server{} sockServer.Config = *sockConf sockServer.Handler = HandleSocketConnection + + SetupBackend(config.BackendUrl) return sockServer } From 401f66f15b9d8de39e7af797a88e37b68ca5f2d9 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sun, 25 Oct 2015 03:21:50 -0700 Subject: [PATCH 11/29] Work on pub/sub and peer cert setup --- socketserver/cmd/socketserver/socketserver.go | 22 ++- socketserver/internal/server/backend.go | 60 +++++- socketserver/internal/server/commands.go | 118 +++++++++++- socketserver/internal/server/handlecore.go | 82 +++++++-- socketserver/internal/server/publisher.go | 173 ++++++++++++++++++ socketserver/internal/server/types.go | 29 ++- socketserver/internal/server/utils.go | 42 ++++- 7 files changed, 478 insertions(+), 48 deletions(-) create mode 100644 socketserver/internal/server/publisher.go diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index 0ab2a278..1e41434a 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -9,8 +9,13 @@ import ( var origin *string = flag.String("origin", "localhost:8001", "Client-visible origin of the socket server") var bindAddress *string = flag.String("listen", "", "Address to bind to, if different from origin") -var certificateFile *string = flag.String("crt", "", "SSL certificate file") -var privateKeyFile *string = flag.String("key", "", "SSL private key file") +var usessl *bool = flag.Bool("ssl", false, "Enable the use of SSL for connecting clients and backend connections") +var certificateFile *string = flag.String("crt", "ssl.crt", "CA-signed SSL certificate file") +var privateKeyFile *string = flag.String("key", "ssl.key", "SSL private key file") +var backendRootFile *string = flag.String("peerroot", "backend_issuer.pem", "Root certificate that issued client certificates for backend servers") +var backendCertFile *string = flag.String("peercrt", "backend_cert.crt", "Backend-trusted certificate, for use as a client certificate") +var backendKeyFile *string = flag.String("peerkey", "backend_cert.key", "Private key for backend-trusted certificate, for use as a client certificate") +var basicAuthPwd *string = flag.String("password", "", "Password for HTTP Basic Auth") // TODO func main() { flag.Parse() @@ -29,17 +34,24 @@ func main() { SSLKeyFile: *privateKeyFile, SSLCertificateFile: *certificateFile, UseSSL: *certificateFile != "", + BackendRootCertFile: *backendRootFile, + BackendClientCertFile: *backendCertFile, + BackendClientKeyFile: *backendKeyFile, SocketOrigin: *origin, } - server.SetupServerAndHandle(conf) + httpServer := &http.Server{ + Addr: *bindAddress + } + + server.SetupServerAndHandle(conf, httpServer.TLSConfig) var err error if conf.UseSSL { - err = http.ListenAndServeTLS(*bindAddress, *certificateFile, *privateKeyFile, nil) + err = httpServer.ListenAndServeTLS(nil, nil) } else { - err = http.ListenAndServe(*bindAddress, nil) + err = httpServer.ListenAndServe() } if err != nil { diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index ba8b4f06..2366996f 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -8,16 +8,41 @@ import ( "github.com/pmylund/go-cache" "strconv" "io/ioutil" + "encoding/json" + "crypto/tls" + "crypto/x509" + "log" ) -var httpClient http.Client +var backendHttpClient http.Client var backendUrl string var responseCache *cache.Cache -func SetupBackend(url string) { - httpClient.Timeout = 60 * time.Second - backendUrl = url +var getBacklogUrl string + +func SetupBackend(config *Config) { + backendHttpClient.Timeout = 60 * time.Second + backendUrl = config.BackendUrl + if responseCache != nil { + responseCache.Flush() + } responseCache = cache.New(60 * time.Second, 120 * time.Second) + + getBacklogUrl = fmt.Sprintf("%s/backlog", backendUrl) +} + +func SetupBackendCertificates(config *Config, certPool x509.CertPool) { + myCert, err := tls.LoadX509KeyPair(config.BackendClientCertFile, config.BackendClientKeyFile) + if err != nil { + log.Fatal(err) + } + tlsConfig := tls.Config{ + Certificates: []tls.Certificate{myCert}, + RootCAs: certPool, + } + tlsConfig.BuildNameToCertificate() + transport := &http.Transport{TLSClientConfig: tlsConfig} + backendHttpClient.Transport = transport } func getCacheKey(remoteCommand, data string) string { @@ -33,7 +58,7 @@ func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, } func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error) { - destUrl := fmt.Sprintf("%s/%s", backendUrl, remoteCommand) + destUrl := fmt.Sprintf("%s/cmd/%s", backendUrl, remoteCommand) var authKey string if auth.UsernameValidated { authKey = "usernameClaimed" @@ -45,8 +70,11 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error "clientData": []string{data}, authKey: []string{auth.TwitchUsername}, } + if gconfig.BasicAuthPassword != "" { + formData["password"] = gconfig.BasicAuthPassword + } - resp, err := httpClient.PostForm(destUrl, formData) + resp, err := backendHttpClient.PostForm(destUrl, formData) if err != nil { return "", err } @@ -69,4 +97,24 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error } return responseJson, nil +} + +func FetchBacklogData(chatSubs, channelSubs []string) ([]ClientMessage, error) { + formData := url.Values{ + "chatSubs": chatSubs, + "channelSubs": channelSubs, + } + + resp, err := backendHttpClient.PostForm(getBacklogUrl, formData) + if err != nil { + return nil, err + } + dec := json.NewDecoder(resp.Body) + var messages []ClientMessage + err = dec.Decode(messages) + if err != nil { + return nil, err + } + + return messages, nil } \ No newline at end of file diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index e8dabbc7..52692254 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -6,11 +6,14 @@ import ( "log" "sync" "strconv" + "time" ) var ResponseSuccess = ClientMessage{Command: SuccessCommand} var ResponseFailure = ClientMessage{Command: "False"} +const ChannelInfoDelay = 2 * time.Second + func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) { handler, ok := CommandHandlers[msg.Command] if !ok { @@ -22,9 +25,7 @@ func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) - client.Mutex.Lock() response, err := CallHandler(handler, conn, client, msg) - client.Mutex.Unlock() if err == nil { response.MessageID = msg.MessageID @@ -64,8 +65,10 @@ func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) return } + client.Mutex.Lock() client.TwitchUsername = username client.UsernameValidated = false + client.Mutex.Unlock() return ResponseSuccess, nil } @@ -73,9 +76,22 @@ func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() - AddToSliceS(&client.CurrentChannels, channel) + client.Mutex.Lock() - // TODO - get backlog + AddToSliceS(&client.CurrentChannels, channel) + client.PendingChatBacklogs = append(client.PendingChatBacklogs, channel) + + if client.MakePendingRequests == nil { + client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) + } else { + if !client.MakePendingRequests.Reset(ChannelInfoDelay) { + client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) + } + } + + client.Mutex.Unlock() + + // note - pub/sub updating happens in GetSubscriptionBacklog return ResponseSuccess, nil } @@ -83,7 +99,11 @@ func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rms func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() + client.Mutex.Lock() RemoveFromSliceS(&client.CurrentChannels, channel) + client.Mutex.Unlock() + + UnsubscribeSingleChat(client, channel) return ResponseSuccess, nil } @@ -91,9 +111,22 @@ func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (r func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() - AddToSliceS(&client.WatchingChannels, channel) + client.Mutex.Lock() - // TODO - get backlog + AddToSliceS(&client.WatchingChannels, channel) + client.PendingStreamBacklogs = append(client.PendingStreamBacklogs, channel) + + if client.MakePendingRequests == nil { + client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) + } else { + if !client.MakePendingRequests.Reset(ChannelInfoDelay) { + client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) + } + } + + client.Mutex.Unlock() + + // note - pub/sub updating happens in GetSubscriptionBacklog return ResponseSuccess, nil } @@ -101,17 +134,88 @@ func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessag func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() + client.Mutex.Lock() RemoveFromSliceS(&client.WatchingChannels, channel) + client.Mutex.Unlock() + + UnsubscribeSingleChannel(client, channel) return ResponseSuccess, nil } +func GetSubscriptionBacklogFor(conn *websocket.Conn, client *ClientInfo) func() { + return func() { + GetSubscriptionBacklog(conn, client) + } +} + +// On goroutine +func GetSubscriptionBacklog(conn *websocket.Conn, client *ClientInfo) { + var chatSubs, channelSubs []string + + // Lock, grab the data, and reset it + client.Mutex.Lock() + chatSubs = client.PendingChatBacklogs + channelSubs = client.PendingStreamBacklogs + client.PendingChatBacklogs = nil + client.PendingStreamBacklogs = nil + client.MakePendingRequests = nil + client.Mutex.Unlock() + + if len(chatSubs) == 0 && len(channelSubs) == 0 { + return + } + + SubscribeBatch(client, chatSubs, channelSubs) + + messages, err := FetchBacklogData(chatSubs, channelSubs) + + if err != nil { + // Oh well. + log.Print("error in GetSubscriptionBacklog:", err) + return + } + + // Deliver to client + for _, msg := range messages { + client.MessageChannel <- msg + } +} + +type SurveySubmission struct { + User string + Json string +} +var SurveySubmissions []SurveySubmission +var SurveySubmissionLock sync.Mutex + func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - log.Println("Ignoring survey response from", client.ClientID) + SurveySubmissionLock.Lock() + SurveySubmissions = append(SurveySubmissions, SurveySubmission{client.TwitchUsername, msg.origArguments}) + SurveySubmissionLock.Unlock() + return ResponseSuccess, nil } +type FollowEvent struct { + User string + Channel string + NowFollowing bool + Timestamp time.Time +} +var FollowEvents []FollowEvent +var FollowEventsLock sync.Mutex + func HandleTrackFollow(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + channel, following, err := msg.ArgumentsAsStringAndBool() + if err != nil { + return + } + now := time.Now() + + FollowEventsLock.Lock() + FollowEvents = append(FollowEvents, FollowEvent{client.TwitchUsername, channel, following, now}) + FollowEventsLock.Unlock() return ResponseSuccess, nil } diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 15ba4312..ce7e0462 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -4,23 +4,34 @@ import ( "net/http" "golang.org/x/net/websocket" "crypto/tls" - "log" "strings" "strconv" "errors" "encoding/json" "fmt" "sync" + "crypto/x509" + "io/ioutil" ) const MAX_PACKET_SIZE = 1024 type Config struct { + // SSL SSLCertificateFile string SSLKeyFile string UseSSL bool + // CA for client validation (pub/sub commands only) + BackendRootCertFile string + BackendClientCertFile string + BackendClientKeyFile string + // Password for client validation (pub/sub commands only) + BasicAuthPassword string + + // Hostname of the socket server SocketOrigin string + // URL to the backend server BackendUrl string } @@ -75,39 +86,58 @@ var ExpectedStringAndInt = errors.New("Error: Expected array of string, int as a var ExpectedStringAndBool = errors.New("Error: Expected array of string, bool as arguments.") var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.") +var gconfig *Config + // Create a websocket.Server with the options from the provided Config. -func SetupServer(config *Config) *websocket.Server { +func setupServer(config *Config, tlsConfig *tls.Config) *websocket.Server { + gconfig = config sockConf, err := websocket.NewConfig("/", config.SocketOrigin) if err != nil { panic(err) } + + SetupBackend(config) + if config.UseSSL { cert, err := tls.LoadX509KeyPair(config.SSLCertificateFile, config.SSLKeyFile) if err != nil { panic(err) } - tlsConf := &tls.Config{ - Certificates: []tls.Certificate{cert}, - ServerName: config.SocketOrigin, + tlsConfig.Certificates = []tls.Certificate{cert} + tlsConfig.ServerName = config.SocketOrigin + tlsConfig.BuildNameToCertificate() + sockConf.TlsConfig = tlsConfig + + certBytes, err := ioutil.ReadFile(config.BackendRootCertFile) + if err != nil { + panic(err) } - tlsConf.BuildNameToCertificate() - sockConf.TlsConfig = tlsConf + clientCA, err := x509.ParseCertificate(certBytes) + if err != nil { + panic(err) + } + certPool := x509.NewCertPool() + certPool.AddCert(clientCA) + tlsConfig.ClientCAs = certPool + SetupBackendCertificates(config, certPool) } sockServer := &websocket.Server{} sockServer.Config = *sockConf sockServer.Handler = HandleSocketConnection - SetupBackend(config.BackendUrl) + go deadChannelReaper() + return sockServer } // Set up a websocket listener and register it on /. // (Uses http.DefaultServeMux .) -func SetupServerAndHandle(config *Config) { - sockServer := SetupServer(config) +func SetupServerAndHandle(config *Config, tlsConfig *tls.Config) { + sockServer := setupServer(config, tlsConfig) http.HandleFunc("/", sockServer.ServeHTTP) + http.HandleFunc("/pub", HandlePublishRequest) } // Handle a new websocket connection from a FFZ client. @@ -122,17 +152,14 @@ func HandleSocketConnection(conn *websocket.Conn) { }) } - defer func() { - closer() - }() - - log.Print("! Got a connection from ", conn.RemoteAddr()) + // Close the connection when we're done. + defer closer() _clientChan := make(chan ClientMessage) _serverMessageChan := make(chan ClientMessage) _errorChan := make(chan error) - // Receive goroutine + // Launch receiver goroutine go func(errorChan chan <- error, clientChan chan <- ClientMessage) { var msg ClientMessage var err error @@ -148,13 +175,15 @@ func HandleSocketConnection(conn *websocket.Conn) { // exit }(_errorChan, _clientChan) - var client ClientInfo - client.MessageChannel = _serverMessageChan - var errorChan <-chan error = _errorChan var clientChan <-chan ClientMessage = _clientChan var serverMessageChan <-chan ClientMessage = _serverMessageChan + var client ClientInfo + client.MessageChannel = _serverMessageChan + + // All set up, now enter the work loop + RunLoop: for { select { @@ -180,7 +209,20 @@ func HandleSocketConnection(conn *websocket.Conn) { FFZCodec.Send(conn, smsg) } } - // exit + + // Exit + + // Launch message draining goroutine - we aren't out of the pub/sub records + go func() { + for _ := range _serverMessageChan {} + }() + + // Stop getting messages... + UnsubscribeAll(&client) + + // And finished. + // Close the channel so the draining goroutine can finish, too. + close(_serverMessageChan) } func CallHandler(handler CommandHandler, conn *websocket.Conn, client *ClientInfo, cmsg ClientMessage) (rmsg ClientMessage, err error) { diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go new file mode 100644 index 00000000..571da17b --- /dev/null +++ b/socketserver/internal/server/publisher.go @@ -0,0 +1,173 @@ +package server + +// This is the scariest code I've written yet for the server. +// If I screwed up the locking, I won't know until it's too late. + +import ( + "sync" + "time" + "net/http" +) + +type SubscriberList struct { + sync.RWMutex + Members []chan <- ClientMessage +} + +var ChatSubscriptionInfo map[string]*SubscriberList +var ChatSubscriptionLock sync.RWMutex +var WatchingSubscriptionInfo map[string]*SubscriberList +var WatchingSubscriptionLock sync.RWMutex + +func PublishToChat(channel string, msg ClientMessage) { + ChatSubscriptionLock.RLock() + list := ChatSubscriptionInfo[channel] + if list != nil { + list.RLock() + for _, ch := range list.Members { + ch <- msg + } + list.RUnlock() + } + ChatSubscriptionLock.RUnlock() +} + +func PublishToWatchers(channel string, msg ClientMessage) { + WatchingSubscriptionLock.RLock() + list := WatchingSubscriptionInfo[channel] + if list != nil { + list.RLock() + for _, ch := range list.Members { + ch <- msg + } + list.RUnlock() + } + WatchingSubscriptionLock.RUnlock() +} + +func HandlePublishRequest(w http.ResponseWriter, r *http.Request) { + if r.TLS { + PeerCertificates + } +} + +// Add a channel to the subscriptions while holding a read-lock to the map. +// Locks: +// - ALREADY HOLDING a read-lock to the 'which' top-level map via the rlocker object +// - possible write lock to the 'which' top-level map via the wlocker object +// - write lock to SubscriptionInfo (if not creating new) +func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string, value chan <- ClientMessage, rlocker sync.Locker, wlocker sync.Locker) { + list := which[channelName] + if list == nil { + // Not found, so create it + rlocker.Unlock() + wlocker.Lock() + list = &SubscriberList{} + list.Members = &[]chan <- ClientMessage{value} // Create it populated, to avoid reaper + which[channelName] = list + wlocker.Unlock() + rlocker.Lock() + } else { + list.Lock() + AddToSliceC(&list.Members, value) + list.Unlock() + } +} + +// Locks: +// - read lock to top-level maps +// - possible write lock to top-level maps +// - write lock to SubscriptionInfos +func SubscribeBatch(client *ClientInfo, chatSubs, channelSubs []string) { + mchan := client.MessageChannel + if len(chatSubs) > 0 { + rlocker := ChatSubscriptionLock.RLocker() + rlocker.Lock() + for _, v := range chatSubs { + _subscribeWhileRlocked(ChatSubscriptionInfo, v, mchan, rlocker, ChatSubscriptionLock) + } + rlocker.Unlock() + } + if len(channelSubs) > 0 { + rlocker := WatchingSubscriptionLock.RLocker() + rlocker.Lock() + for _, v := range channelSubs { + _subscribeWhileRlocked(WatchingSubscriptionInfo, v, mchan, rlocker, WatchingSubscriptionLock) + } + rlocker.Unlock() + } +} + +// Unsubscribe the client from all channels, AND clear the CurrentChannels / WatchingChannels fields. +// Locks: +// - read lock to top-level maps +// - write lock to SubscriptionInfos +// - write lock to ClientInfo +func UnsubscribeAll(client *ClientInfo) { + ChatSubscriptionLock.RLock() + client.Mutex.Lock() + for _, v := range client.CurrentChannels { + list := ChatSubscriptionInfo[v] + list.Lock() + RemoveFromSliceC(&list.Members, client.MessageChannel) + list.Unlock() + } + client.CurrentChannels = nil + client.Mutex.Unlock() + ChatSubscriptionLock.RUnlock() + + WatchingSubscriptionLock.RLock() + client.Mutex.Lock() + for _, v := range client.WatchingChannels { + list := WatchingSubscriptionInfo[v] + list.Lock() + RemoveFromSliceC(&list.Members, client.MessageChannel) + list.Unlock() + } + client.WatchingChannels = nil + client.Mutex.Unlock() + WatchingSubscriptionLock.RUnlock() +} + +func UnsubscribeSingleChat(client *ClientInfo, channelName string) { + ChatSubscriptionLock.RLock() + list := ChatSubscriptionInfo[channelName] + list.Lock() + RemoveFromSliceC(&list.Members, client.MessageChannel) + list.Unlock() + ChatSubscriptionLock.RUnlock() +} + +func UnsubscribeSingleChannel(client *ClientInfo, channelName string) { + WatchingSubscriptionLock.RLock() + list := WatchingSubscriptionInfo[channelName] + list.Lock() + RemoveFromSliceC(&list.Members, client.MessageChannel) + list.Unlock() + WatchingSubscriptionLock.RUnlock() +} + +const ReapingDelay = 120 * time.Minute + +// Checks each of ChatSubscriptionInfo / WatchingSubscriptionInfo +// for entries with no subscribers every ReapingDelay. +// Started from SetupServer(). +func deadChannelReaper() { + for { + time.Sleep(ReapingDelay / 2) + ChatSubscriptionLock.Lock() + for key, val := range ChatSubscriptionInfo { + if len(val.Members) == 0 { + ChatSubscriptionInfo[key] = nil + } + } + ChatSubscriptionLock.Unlock() + time.Sleep(ReapingDelay / 2) + WatchingSubscriptionLock.Lock() + for key, val := range WatchingSubscriptionInfo { + if len(val.Members) == 0 { + WatchingSubscriptionInfo[key] = nil + } + } + } +} \ No newline at end of file diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 3559de81..9c94c7a1 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -3,19 +3,20 @@ package server import ( "github.com/satori/go.uuid" "sync" + "time" ) type ClientMessage struct { // Message ID. Increments by 1 for each message sent from the client. // When replying to a command, the message ID must be echoed. // When sending a server-initiated message, this is -1. - MessageID int + MessageID int `json:_` // The command that the client wants from the server. // When sent from the server, the literal string 'True' indicates success. // Before sending, a blank Command will be converted into SuccessCommand. - Command Command + Command Command `json:cmd` // Result of json.Unmarshal on the third field send from the client - Arguments interface{} + Arguments interface{} `json:data` origArguments string } @@ -41,19 +42,35 @@ type ClientInfo struct { // If it seems to be a performance problem, we can split this. Mutex sync.Mutex + // TODO(riking) - does this need to be protected cross-thread? AuthInfo // Username validation nonce. ValidationNonce string // The list of chats this client is currently in. - // Protected by Mutex + // Protected by Mutex. CurrentChannels []string // This list of channels this client needs UI updates for. - // Protected by Mutex + // Protected by Mutex. WatchingChannels []string + // List of channels that we have not yet checked current chat-related channel info for. + // This lets us batch the backlog requests. + // Protected by Mutex. + PendingChatBacklogs []string + + // List of channels that we have not yet checked current stream-related channel info for. + // This lets us batch the backlog requests. + // Protected by Mutex. + PendingStreamBacklogs []string + + // A timer that, when fired, will make the pending backlog requests. + // Usually nil. Protected by Mutex. + MakePendingRequests *time.Timer + // Server-initiated messages should be sent here + // Never nil. MessageChannel chan <- ClientMessage -} \ No newline at end of file +} diff --git a/socketserver/internal/server/utils.go b/socketserver/internal/server/utils.go index 65fa9cae..639eac5c 100644 --- a/socketserver/internal/server/utils.go +++ b/socketserver/internal/server/utils.go @@ -3,19 +3,20 @@ package server import ( ) -func AddToSliceS(ary *[]string, val string) { +func AddToSliceS(ary *[]string, val string) bool { slice := *ary for _, v := range slice { if v == val { - return + return false } } slice = append(slice, val) *ary = slice + return true } -func RemoveFromSliceS(ary *[]string, val string) { +func RemoveFromSliceS(ary *[]string, val string) bool { slice := *ary var idx int = -1 for i, v := range slice { @@ -25,10 +26,43 @@ func RemoveFromSliceS(ary *[]string, val string) { } } if idx == -1 { - return + return false } slice[idx] = slice[len(slice) - 1] slice = slice[:len(slice) - 1] *ary = slice + return true } + +func AddToSliceC(ary *[]chan <- ClientMessage, val chan <- ClientMessage) bool { + slice := *ary + for _, v := range slice { + if v == val { + return false + } + } + + slice = append(slice, val) + *ary = slice + return true +} + +func RemoveFromSliceC(ary *[]chan <- ClientMessage, val chan <- ClientMessage) bool { + slice := *ary + var idx int = -1 + for i, v := range slice { + if v == val { + idx = i + break + } + } + if idx == -1 { + return false + } + + slice[idx] = slice[len(slice) - 1] + slice = slice[:len(slice) - 1] + *ary = slice + return true +} \ No newline at end of file From c6a3c120c67962639b8e6807e65b061e505df972 Mon Sep 17 00:00:00 2001 From: Kane York Date: Sun, 25 Oct 2015 12:40:07 -0700 Subject: [PATCH 12/29] Switch to nacl cryptobox --- socketserver/cmd/socketserver/socketserver.go | 36 +++++-- socketserver/internal/server/backend.go | 101 ++++++++++++++---- socketserver/internal/server/handlecore.go | 31 ++---- socketserver/internal/server/publisher.go | 10 +- socketserver/internal/server/types.go | 9 ++ socketserver/internal/server/utils.go | 17 +++ 6 files changed, 144 insertions(+), 60 deletions(-) diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index 1e41434a..49db8872 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -2,9 +2,9 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/sock import ( "flag" - "../../internal/server" "log" "net/http" + "../../internal/server" ) var origin *string = flag.String("origin", "localhost:8001", "Client-visible origin of the socket server") @@ -12,14 +12,18 @@ var bindAddress *string = flag.String("listen", "", "Address to bind to, if diff var usessl *bool = flag.Bool("ssl", false, "Enable the use of SSL for connecting clients and backend connections") var certificateFile *string = flag.String("crt", "ssl.crt", "CA-signed SSL certificate file") var privateKeyFile *string = flag.String("key", "ssl.key", "SSL private key file") -var backendRootFile *string = flag.String("peerroot", "backend_issuer.pem", "Root certificate that issued client certificates for backend servers") -var backendCertFile *string = flag.String("peercrt", "backend_cert.crt", "Backend-trusted certificate, for use as a client certificate") -var backendKeyFile *string = flag.String("peerkey", "backend_cert.key", "Private key for backend-trusted certificate, for use as a client certificate") -var basicAuthPwd *string = flag.String("password", "", "Password for HTTP Basic Auth") // TODO + +var naclKeysFile *string = flag.String("naclkey", "naclkeys.json", "Keypairs for the NaCl crypto library, for communicating with the backend.") +var generateKeys *bool = flag.Bool("genkeys", false, "Generate NaCl keys instead of serving requests.\nArguments: [int serverId] [base64 backendPublic]\nThe backend public key can either be specified in base64 on the command line, or put in the json file later.") func main() { flag.Parse() + if *generateKeys { + GenerateKeys(*naclKeysFile) + return + } + if *origin == "" { log.Fatalln("--origin argument required") } @@ -33,23 +37,21 @@ func main() { conf := &server.Config { SSLKeyFile: *privateKeyFile, SSLCertificateFile: *certificateFile, - UseSSL: *certificateFile != "", - BackendRootCertFile: *backendRootFile, - BackendClientCertFile: *backendCertFile, - BackendClientKeyFile: *backendKeyFile, + UseSSL: *usessl, + NaclKeysFile: *naclKeysFile, SocketOrigin: *origin, } httpServer := &http.Server{ - Addr: *bindAddress + Addr: *bindAddress, } server.SetupServerAndHandle(conf, httpServer.TLSConfig) var err error if conf.UseSSL { - err = httpServer.ListenAndServeTLS(nil, nil) + err = httpServer.ListenAndServeTLS(*certificateFile, *privateKeyFile) } else { err = httpServer.ListenAndServe() } @@ -58,3 +60,15 @@ func main() { log.Fatal("ListenAndServe: ", err) } } + + +func GenerateKeys(outputFile string) { + if flag.NArg() < 1 { + log.Fatal("The server ID must be specified") + } + if flag.NArg() >= 2 { + server.GenerateKeys(outputFile, flag.Arg(0), flag.Arg(1)) + } else { + server.GenerateKeys(outputFile, flag.Arg(0), "") + } +} \ No newline at end of file diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 2366996f..24766025 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -1,6 +1,7 @@ package server import ( + "golang.org/x/crypto/nacl/box" "net/http" "time" "fmt" @@ -9,9 +10,12 @@ import ( "strconv" "io/ioutil" "encoding/json" - "crypto/tls" - "crypto/x509" + "sync" "log" + "os" + "crypto/rand" + "encoding/base64" + "strings" ) var backendHttpClient http.Client @@ -20,6 +24,10 @@ var responseCache *cache.Cache var getBacklogUrl string +var backendSharedKey [32]byte + +var messageBufferPool sync.Pool + func SetupBackend(config *Config) { backendHttpClient.Timeout = 60 * time.Second backendUrl = config.BackendUrl @@ -29,26 +37,45 @@ func SetupBackend(config *Config) { responseCache = cache.New(60 * time.Second, 120 * time.Second) getBacklogUrl = fmt.Sprintf("%s/backlog", backendUrl) -} -func SetupBackendCertificates(config *Config, certPool x509.CertPool) { - myCert, err := tls.LoadX509KeyPair(config.BackendClientCertFile, config.BackendClientKeyFile) + messageBufferPool.New = NewByteBuffer + + var keys CryptoKeysBuf + file, err := os.Open(config.NaclKeysFile) if err != nil { log.Fatal(err) } - tlsConfig := tls.Config{ - Certificates: []tls.Certificate{myCert}, - RootCAs: certPool, + dec := json.NewDecoder(file) + err = dec.Decode(&keys) + if err != nil { + log.Fatal(err) } - tlsConfig.BuildNameToCertificate() - transport := &http.Transport{TLSClientConfig: tlsConfig} - backendHttpClient.Transport = transport + + box.Precompute(&backendSharedKey, &keys.TheirPublicKey, &keys.OurPrivateKey) } func getCacheKey(remoteCommand, data string) string { return fmt.Sprintf("%s/%s", remoteCommand, data) } +func SealRequest(form url.Values) ([]byte, error) { + asString := form.Encode() + var nonce [24]byte + var err error + + err = FillCryptoRandom(nonce[:]) + if err != nil { + return nil, err + } + + message := []byte(asString) + out := make([]byte, len(message) + box.Overhead) + box.SealAfterPrecomputation(out, message, &nonce, &backendSharedKey) + + // TODO + return nil, nil +} + func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) { cached, ok := responseCache.Get(getCacheKey(remoteCommand, data)) if ok { @@ -57,7 +84,7 @@ func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, return RequestRemoteData(remoteCommand, data, auth) } -func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error) { +func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr string, err error) { destUrl := fmt.Sprintf("%s/cmd/%s", backendUrl, remoteCommand) var authKey string if auth.UsernameValidated { @@ -70,9 +97,6 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error "clientData": []string{data}, authKey: []string{auth.TwitchUsername}, } - if gconfig.BasicAuthPassword != "" { - formData["password"] = gconfig.BasicAuthPassword - } resp, err := backendHttpClient.PostForm(destUrl, formData) if err != nil { @@ -85,7 +109,7 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error return "", err } - responseJson := string(respBytes) + responseStr = string(respBytes) if resp.Header.Get("FFZ-Cache") != "" { durSecs, err := strconv.ParseInt(resp.Header.Get("FFZ-Cache"), 10, 64) @@ -93,10 +117,10 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (string, error return "", fmt.Errorf("The RPC server returned a non-integer cache duration: %v", err) } duration := time.Duration(durSecs) * time.Second - responseCache.Set(getCacheKey(remoteCommand, data), responseJson, duration) + responseCache.Set(getCacheKey(remoteCommand, data), responseStr, duration) } - return responseJson, nil + return } func FetchBacklogData(chatSubs, channelSubs []string) ([]ClientMessage, error) { @@ -117,4 +141,43 @@ func FetchBacklogData(chatSubs, channelSubs []string) ([]ClientMessage, error) { } return messages, nil -} \ No newline at end of file +} + +func GenerateKeys(outputFile, serverId, theirPublicStr string) { + var err error + output := CryptoKeysBuf{} + + output.ServerId, err = strconv.Atoi(serverId) + if err != nil { + log.Fatal(err) + } + + ourPublic, ourPrivate, err := box.GenerateKey(rand.Reader) + if err != nil { + log.Fatal(err) + } + output.OurPublicKey, output.OurPrivateKey = *ourPublic, *ourPrivate + + if theirPublicStr != "" { + reader := base64.NewDecoder(base64.RawURLEncoding, strings.NewReader(theirPublicStr)) + theirPublic, err := ioutil.ReadAll(reader) + if err != nil { + log.Fatal(err) + } + copy(output.TheirPublicKey[:], theirPublic) + } + + file, err := os.Create(outputFile) + if err != nil { + log.Fatal(err) + } + enc := json.NewEncoder(file) + err = enc.Encode(output) + if err != nil { + log.Fatal(err) + } + err = file.Close() + if err != nil { + log.Fatal(err) + } +} diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index ce7e0462..b6161db7 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -10,24 +10,19 @@ import ( "encoding/json" "fmt" "sync" - "crypto/x509" - "io/ioutil" + "log" ) const MAX_PACKET_SIZE = 1024 type Config struct { - // SSL + // SSL/TLS SSLCertificateFile string SSLKeyFile string UseSSL bool - // CA for client validation (pub/sub commands only) - BackendRootCertFile string - BackendClientCertFile string - BackendClientKeyFile string - // Password for client validation (pub/sub commands only) - BasicAuthPassword string + // NaCl keys for backend messages + NaclKeysFile string // Hostname of the socket server SocketOrigin string @@ -93,7 +88,7 @@ func setupServer(config *Config, tlsConfig *tls.Config) *websocket.Server { gconfig = config sockConf, err := websocket.NewConfig("/", config.SocketOrigin) if err != nil { - panic(err) + log.Fatal(err) } SetupBackend(config) @@ -101,25 +96,13 @@ func setupServer(config *Config, tlsConfig *tls.Config) *websocket.Server { if config.UseSSL { cert, err := tls.LoadX509KeyPair(config.SSLCertificateFile, config.SSLKeyFile) if err != nil { - panic(err) + log.Fatal(err) } tlsConfig.Certificates = []tls.Certificate{cert} tlsConfig.ServerName = config.SocketOrigin tlsConfig.BuildNameToCertificate() sockConf.TlsConfig = tlsConfig - certBytes, err := ioutil.ReadFile(config.BackendRootCertFile) - if err != nil { - panic(err) - } - clientCA, err := x509.ParseCertificate(certBytes) - if err != nil { - panic(err) - } - certPool := x509.NewCertPool() - certPool.AddCert(clientCA) - tlsConfig.ClientCAs = certPool - SetupBackendCertificates(config, certPool) } sockServer := &websocket.Server{} @@ -214,7 +197,7 @@ func HandleSocketConnection(conn *websocket.Conn) { // Launch message draining goroutine - we aren't out of the pub/sub records go func() { - for _ := range _serverMessageChan {} + for _ = range _serverMessageChan {} }() // Stop getting messages... diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index 571da17b..61d521d8 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -46,9 +46,7 @@ func PublishToWatchers(channel string, msg ClientMessage) { } func HandlePublishRequest(w http.ResponseWriter, r *http.Request) { - if r.TLS { - PeerCertificates - } + // TODO - box.Open() } // Add a channel to the subscriptions while holding a read-lock to the map. @@ -63,7 +61,7 @@ func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string rlocker.Unlock() wlocker.Lock() list = &SubscriberList{} - list.Members = &[]chan <- ClientMessage{value} // Create it populated, to avoid reaper + list.Members = []chan <- ClientMessage{value} // Create it populated, to avoid reaper which[channelName] = list wlocker.Unlock() rlocker.Lock() @@ -84,7 +82,7 @@ func SubscribeBatch(client *ClientInfo, chatSubs, channelSubs []string) { rlocker := ChatSubscriptionLock.RLocker() rlocker.Lock() for _, v := range chatSubs { - _subscribeWhileRlocked(ChatSubscriptionInfo, v, mchan, rlocker, ChatSubscriptionLock) + _subscribeWhileRlocked(ChatSubscriptionInfo, v, mchan, rlocker, &ChatSubscriptionLock) } rlocker.Unlock() } @@ -92,7 +90,7 @@ func SubscribeBatch(client *ClientInfo, chatSubs, channelSubs []string) { rlocker := WatchingSubscriptionLock.RLocker() rlocker.Lock() for _, v := range channelSubs { - _subscribeWhileRlocked(WatchingSubscriptionInfo, v, mchan, rlocker, WatchingSubscriptionLock) + _subscribeWhileRlocked(WatchingSubscriptionInfo, v, mchan, rlocker, &WatchingSubscriptionLock) } rlocker.Unlock() } diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 9c94c7a1..046afcca 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -6,6 +6,15 @@ import ( "time" ) +const CryptoBoxKeyLength = 32 + +type CryptoKeysBuf struct { + OurPrivateKey [CryptoBoxKeyLength]byte + OurPublicKey [CryptoBoxKeyLength]byte + TheirPublicKey [CryptoBoxKeyLength]byte + ServerId int +} + type ClientMessage struct { // Message ID. Increments by 1 for each message sent from the client. // When replying to a command, the message ID must be echoed. diff --git a/socketserver/internal/server/utils.go b/socketserver/internal/server/utils.go index 639eac5c..1d6d437a 100644 --- a/socketserver/internal/server/utils.go +++ b/socketserver/internal/server/utils.go @@ -1,8 +1,25 @@ package server import ( + "crypto/rand" ) +func FillCryptoRandom(buf []byte) error { + remaining := len(buf) + for remaining > 0 { + count, err := rand.Read(buf) + if err != nil { + return err + } + remaining -= count + } + return nil +} + +func NewByteBuffer() interface{} { + return make([]byte, 1024) +} + func AddToSliceS(ary *[]string, val string) bool { slice := *ary for _, v := range slice { From 8a24ac37ab444f8aa4ed82db3baa0eee1aed759c Mon Sep 17 00:00:00 2001 From: Kane York Date: Sun, 25 Oct 2015 14:06:56 -0700 Subject: [PATCH 13/29] Finish Seal/UnsealRequest functions --- socketserver/internal/server/backend.go | 35 +++------ socketserver/internal/server/backend_test.go | 40 ++++++++++ socketserver/internal/server/types.go | 6 +- socketserver/internal/server/utils.go | 80 +++++++++++++++++++- 4 files changed, 133 insertions(+), 28 deletions(-) create mode 100644 socketserver/internal/server/backend_test.go diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 24766025..af4668cf 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -25,6 +25,7 @@ var responseCache *cache.Cache var getBacklogUrl string var backendSharedKey [32]byte +var serverId int var messageBufferPool sync.Pool @@ -38,7 +39,7 @@ func SetupBackend(config *Config) { getBacklogUrl = fmt.Sprintf("%s/backlog", backendUrl) - messageBufferPool.New = NewByteBuffer + messageBufferPool.New = New4KByteBuffer var keys CryptoKeysBuf file, err := os.Open(config.NaclKeysFile) @@ -51,31 +52,18 @@ func SetupBackend(config *Config) { log.Fatal(err) } - box.Precompute(&backendSharedKey, &keys.TheirPublicKey, &keys.OurPrivateKey) + var theirPublic, ourPrivate [32]byte + copy(theirPublic[:], keys.TheirPublicKey) + copy(ourPrivate[:], keys.OurPrivateKey) + serverId = keys.ServerId + + box.Precompute(&backendSharedKey, &theirPublic, &ourPrivate) } func getCacheKey(remoteCommand, data string) string { return fmt.Sprintf("%s/%s", remoteCommand, data) } -func SealRequest(form url.Values) ([]byte, error) { - asString := form.Encode() - var nonce [24]byte - var err error - - err = FillCryptoRandom(nonce[:]) - if err != nil { - return nil, err - } - - message := []byte(asString) - out := make([]byte, len(message) + box.Overhead) - box.SealAfterPrecomputation(out, message, &nonce, &backendSharedKey) - - // TODO - return nil, nil -} - func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) { cached, ok := responseCache.Get(getCacheKey(remoteCommand, data)) if ok { @@ -156,15 +144,16 @@ func GenerateKeys(outputFile, serverId, theirPublicStr string) { if err != nil { log.Fatal(err) } - output.OurPublicKey, output.OurPrivateKey = *ourPublic, *ourPrivate + output.OurPublicKey, output.OurPrivateKey = ourPublic[:], ourPrivate[:] if theirPublicStr != "" { - reader := base64.NewDecoder(base64.RawURLEncoding, strings.NewReader(theirPublicStr)) + reader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(theirPublicStr)) theirPublic, err := ioutil.ReadAll(reader) if err != nil { log.Fatal(err) } - copy(output.TheirPublicKey[:], theirPublic) + log.Print(theirPublic) + output.TheirPublicKey = theirPublic } file, err := os.Create(outputFile) diff --git a/socketserver/internal/server/backend_test.go b/socketserver/internal/server/backend_test.go new file mode 100644 index 00000000..3a2d5a7b --- /dev/null +++ b/socketserver/internal/server/backend_test.go @@ -0,0 +1,40 @@ +package server +import ( + "testing" + "net/url" + "golang.org/x/crypto/nacl/box" + "crypto/rand" +) + +func TestSealRequest(t *testing.T) { + senderPublic, senderPrivate, err := box.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + receiverPublic, receiverPrivate, err := box.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + messageBufferPool.New = New4KByteBuffer + + values := url.Values{ + "QuickBrownFox": []string{"LazyDog"}, + } + + box.Precompute(&backendSharedKey, receiverPublic, senderPrivate) + sealedValues, err := SealRequest(values) + if err != nil { + t.Fatal(err) + } + + box.Precompute(&backendSharedKey, senderPublic, receiverPrivate) + unsealedValues, err := UnsealRequest(sealedValues) + if err != nil { + t.Fatal(err) + } + + if unsealedValues.Get("QuickBrownFox") != "LazyDog" { + t.Errorf("Failed to round-trip, got back %v", unsealedValues) + } +} diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 046afcca..be3145e1 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -9,9 +9,9 @@ import ( const CryptoBoxKeyLength = 32 type CryptoKeysBuf struct { - OurPrivateKey [CryptoBoxKeyLength]byte - OurPublicKey [CryptoBoxKeyLength]byte - TheirPublicKey [CryptoBoxKeyLength]byte + OurPrivateKey []byte + OurPublicKey []byte + TheirPublicKey []byte ServerId int } diff --git a/socketserver/internal/server/utils.go b/socketserver/internal/server/utils.go index 1d6d437a..a3853cd0 100644 --- a/socketserver/internal/server/utils.go +++ b/socketserver/internal/server/utils.go @@ -2,6 +2,14 @@ package server import ( "crypto/rand" + "net/url" + "golang.org/x/crypto/nacl/box" + "bytes" + "encoding/base64" + "strconv" + "strings" + "errors" + "log" ) func FillCryptoRandom(buf []byte) error { @@ -16,8 +24,76 @@ func FillCryptoRandom(buf []byte) error { return nil } -func NewByteBuffer() interface{} { - return make([]byte, 1024) +func New4KByteBuffer() interface{} { + return make([]byte, 0, 4096) +} + +func SealRequest(form url.Values) (url.Values, error) { + var nonce [24]byte + var err error + + err = FillCryptoRandom(nonce[:]) + if err != nil { + return nil, err + } + + cipherMsg := box.SealAfterPrecomputation(nil, []byte(form.Encode()), &nonce, &backendSharedKey) + + bufMessage := new(bytes.Buffer) + enc := base64.NewEncoder(base64.URLEncoding, bufMessage) + enc.Write(cipherMsg) + enc.Close() + cipherString := bufMessage.String() + + bufNonce := new(bytes.Buffer) + enc = base64.NewEncoder(base64.URLEncoding, bufNonce) + enc.Write(nonce[:]) + enc.Close() + nonceString := bufNonce.String() + + retval := url.Values{ + "nonce": []string{nonceString}, + "msg": []string{cipherString}, + "id": []string{strconv.Itoa(serverId)}, + } + + return retval, nil +} + +var ErrorShortNonce = errors.New("Nonce too short.") +var ErrorInvalidSignature = errors.New("Invalid signature or contents") + +func UnsealRequest(form url.Values) (url.Values, error) { + var nonce [24]byte + + nonceString := form.Get("nonce") + dec := base64.NewDecoder(base64.URLEncoding, strings.NewReader(nonceString)) + count, err := dec.Read(nonce[:]) + if err != nil { + return nil, err + } + if count != 24 { + return nil, ErrorShortNonce + } + + cipherString := form.Get("msg") + dec = base64.NewDecoder(base64.URLEncoding, strings.NewReader(cipherString)) + cipherBuffer := new(bytes.Buffer) + cipherBuffer.ReadFrom(dec) + + message, ok := box.OpenAfterPrecomputation(nil, cipherBuffer.Bytes(), &nonce, &backendSharedKey) + if !ok { + return nil, ErrorInvalidSignature + } + + retValues, err := url.ParseQuery(string(message)) + if err != nil { + // Assume that the signature was accidentally correct but the contents were garbage + log.Print(err) + return nil, ErrorInvalidSignature + } + + return retValues, nil } func AddToSliceS(ary *[]string, val string) bool { From 0c9e7bb97d41f6e15136e36fec6f03b481fb368f Mon Sep 17 00:00:00 2001 From: Kane York Date: Sun, 25 Oct 2015 20:17:17 -0700 Subject: [PATCH 14/29] Add tests, fix some bugs uncovered by the tests --- socketserver/cmd/socketserver/socketserver.go | 2 +- socketserver/internal/server/backend.go | 15 +- socketserver/internal/server/backend_test.go | 13 +- socketserver/internal/server/commands.go | 27 +++- socketserver/internal/server/handlecore.go | 14 +- socketserver/internal/server/publisher.go | 79 ++++++++-- .../internal/server/publisher_test.go | 141 ++++++++++++++++++ 7 files changed, 261 insertions(+), 30 deletions(-) create mode 100644 socketserver/internal/server/publisher_test.go diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index 49db8872..463d8774 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -47,7 +47,7 @@ func main() { Addr: *bindAddress, } - server.SetupServerAndHandle(conf, httpServer.TLSConfig) + server.SetupServerAndHandle(conf, httpServer.TLSConfig, nil) var err error if conf.UseSSL { diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index af4668cf..6c95c261 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -86,7 +86,12 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s authKey: []string{auth.TwitchUsername}, } - resp, err := backendHttpClient.PostForm(destUrl, formData) + sealedForm, err := SealRequest(formData) + if err != nil { + return "", err + } + + resp, err := backendHttpClient.PostForm(destUrl, sealedForm) if err != nil { return "", err } @@ -117,7 +122,12 @@ func FetchBacklogData(chatSubs, channelSubs []string) ([]ClientMessage, error) { "channelSubs": channelSubs, } - resp, err := backendHttpClient.PostForm(getBacklogUrl, formData) + sealedForm, err := SealRequest(formData) + if err != nil { + return nil, err + } + + resp, err := backendHttpClient.PostForm(getBacklogUrl, sealedForm) if err != nil { return nil, err } @@ -152,7 +162,6 @@ func GenerateKeys(outputFile, serverId, theirPublicStr string) { if err != nil { log.Fatal(err) } - log.Print(theirPublic) output.TheirPublicKey = theirPublic } diff --git a/socketserver/internal/server/backend_test.go b/socketserver/internal/server/backend_test.go index 3a2d5a7b..e02d6c0e 100644 --- a/socketserver/internal/server/backend_test.go +++ b/socketserver/internal/server/backend_test.go @@ -6,29 +6,32 @@ import ( "crypto/rand" ) -func TestSealRequest(t *testing.T) { - senderPublic, senderPrivate, err := box.GenerateKey(rand.Reader) +func SetupRandomKeys(t testing.TB) { + _, senderPrivate, err := box.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } - receiverPublic, receiverPrivate, err := box.GenerateKey(rand.Reader) + receiverPublic, _, err := box.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } + box.Precompute(&backendSharedKey, receiverPublic, senderPrivate) messageBufferPool.New = New4KByteBuffer +} + +func TestSealRequest(t *testing.T) { + SetupRandomKeys(t) values := url.Values{ "QuickBrownFox": []string{"LazyDog"}, } - box.Precompute(&backendSharedKey, receiverPublic, senderPrivate) sealedValues, err := SealRequest(values) if err != nil { t.Fatal(err) } - box.Precompute(&backendSharedKey, senderPublic, receiverPrivate) unsealedValues, err := UnsealRequest(sealedValues) if err != nil { t.Fatal(err) diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index 52692254..e30b543f 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -23,7 +23,7 @@ func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) return } - log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) +// log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) response, err := CallHandler(handler, conn, client, msg) @@ -76,6 +76,10 @@ func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() + if err != nil { + return + } + client.Mutex.Lock() AddToSliceS(&client.CurrentChannels, channel) @@ -91,7 +95,7 @@ func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rms client.Mutex.Unlock() - // note - pub/sub updating happens in GetSubscriptionBacklog + SubscribeChat(client, channel) return ResponseSuccess, nil } @@ -99,6 +103,10 @@ func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rms func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() + if err != nil { + return + } + client.Mutex.Lock() RemoveFromSliceS(&client.CurrentChannels, channel) client.Mutex.Unlock() @@ -111,6 +119,10 @@ func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (r func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() + if err != nil { + return + } + client.Mutex.Lock() AddToSliceS(&client.WatchingChannels, channel) @@ -126,7 +138,7 @@ func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessag client.Mutex.Unlock() - // note - pub/sub updating happens in GetSubscriptionBacklog + SubscribeWatching(client, channel) return ResponseSuccess, nil } @@ -134,6 +146,10 @@ func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessag func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { channel, err := msg.ArgumentsAsString() + if err != nil { + return + } + client.Mutex.Lock() RemoveFromSliceS(&client.WatchingChannels, channel) client.Mutex.Unlock() @@ -166,8 +182,9 @@ func GetSubscriptionBacklog(conn *websocket.Conn, client *ClientInfo) { return } - SubscribeBatch(client, chatSubs, channelSubs) - + if backendUrl == "" { + return // for testing runs + } messages, err := FetchBacklogData(chatSubs, channelSubs) if err != nil { diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index b6161db7..7ef4e533 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -74,6 +74,7 @@ var FFZCodec websocket.Codec = websocket.Codec{ // Errors that get returned to the client. var ProtocolError error = errors.New("FFZ Socket protocol error.") +var ProtocolErrorNegativeID error = errors.New("FFZ Socket protocol error: negative or zero message ID.") var ExpectedSingleString = errors.New("Error: Expected single string as arguments.") var ExpectedSingleInt = errors.New("Error: Expected single integer as arguments.") var ExpectedTwoStrings = errors.New("Error: Expected array of string, string as arguments.") @@ -116,11 +117,14 @@ func setupServer(config *Config, tlsConfig *tls.Config) *websocket.Server { // Set up a websocket listener and register it on /. // (Uses http.DefaultServeMux .) -func SetupServerAndHandle(config *Config, tlsConfig *tls.Config) { +func SetupServerAndHandle(config *Config, tlsConfig *tls.Config, serveMux *http.ServeMux) { sockServer := setupServer(config, tlsConfig) - http.HandleFunc("/", sockServer.ServeHTTP) - http.HandleFunc("/pub", HandlePublishRequest) + if serveMux == nil { + serveMux = http.DefaultServeMux + } + serveMux.HandleFunc("/", sockServer.ServeHTTP) + serveMux.HandleFunc("/pub", HandlePublishRequest) } // Handle a new websocket connection from a FFZ client. @@ -235,8 +239,8 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e return ProtocolError } messageId, err := strconv.Atoi(dataStr[:spaceIdx]) - if messageId <= 0 { - return ProtocolError + if messageId < -1 || messageId == 0 { + return ProtocolErrorNegativeID } out.MessageID = messageId diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index 61d521d8..eae5488e 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -7,6 +7,7 @@ import ( "sync" "time" "net/http" + "fmt" ) type SubscriberList struct { @@ -14,39 +15,65 @@ type SubscriberList struct { Members []chan <- ClientMessage } -var ChatSubscriptionInfo map[string]*SubscriberList +var ChatSubscriptionInfo map[string]*SubscriberList = make(map[string]*SubscriberList) var ChatSubscriptionLock sync.RWMutex -var WatchingSubscriptionInfo map[string]*SubscriberList +var WatchingSubscriptionInfo map[string]*SubscriberList = make(map[string]*SubscriberList) var WatchingSubscriptionLock sync.RWMutex -func PublishToChat(channel string, msg ClientMessage) { +func PublishToChat(channel string, msg ClientMessage) (count int) { ChatSubscriptionLock.RLock() list := ChatSubscriptionInfo[channel] if list != nil { list.RLock() for _, ch := range list.Members { ch <- msg + count++ } list.RUnlock() } ChatSubscriptionLock.RUnlock() + return } -func PublishToWatchers(channel string, msg ClientMessage) { +func PublishToWatchers(channel string, msg ClientMessage) (count int) { WatchingSubscriptionLock.RLock() list := WatchingSubscriptionInfo[channel] if list != nil { list.RLock() for _, ch := range list.Members { ch <- msg + count++ } list.RUnlock() } WatchingSubscriptionLock.RUnlock() + return } func HandlePublishRequest(w http.ResponseWriter, r *http.Request) { - // TODO - box.Open() + formData, err := UnsealRequest(r.Form) + if err != nil { + w.WriteHeader(403) + fmt.Fprintf(w, "Error: %v", err) + return + } + + cmd := formData.Get("cmd") + json := formData.Get("args") + chat := formData.Get("chat") + watchChannel := formData.Get("channel") + cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} + var count int + if chat != "" { + count = PublishToChat(chat, cm) + } else if watchChannel != "" { + count = PublishToWatchers(watchChannel, cm) + } else { + w.WriteHeader(400) + fmt.Fprint(w, "Need to specify either chat or channel") + return + } + fmt.Fprint(w, count) } // Add a channel to the subscriptions while holding a read-lock to the map. @@ -72,6 +99,18 @@ func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string } } +func SubscribeChat(client *ClientInfo, channelName string) { + ChatSubscriptionLock.RLock() + _subscribeWhileRlocked(ChatSubscriptionInfo, channelName, client.MessageChannel, ChatSubscriptionLock.RLocker(), &ChatSubscriptionLock) + ChatSubscriptionLock.RUnlock() +} + +func SubscribeWatching(client *ClientInfo, channelName string) { + WatchingSubscriptionLock.RLock() + _subscribeWhileRlocked(WatchingSubscriptionInfo, channelName, client.MessageChannel, WatchingSubscriptionLock.RLocker(), &WatchingSubscriptionLock) + WatchingSubscriptionLock.RUnlock() +} + // Locks: // - read lock to top-level maps // - possible write lock to top-level maps @@ -102,13 +141,20 @@ func SubscribeBatch(client *ClientInfo, chatSubs, channelSubs []string) { // - write lock to SubscriptionInfos // - write lock to ClientInfo func UnsubscribeAll(client *ClientInfo) { + client.Mutex.Lock() + client.PendingChatBacklogs = nil + client.PendingStreamBacklogs = nil + client.Mutex.Unlock() + ChatSubscriptionLock.RLock() client.Mutex.Lock() for _, v := range client.CurrentChannels { list := ChatSubscriptionInfo[v] - list.Lock() - RemoveFromSliceC(&list.Members, client.MessageChannel) - list.Unlock() + if list != nil { + list.Lock() + RemoveFromSliceC(&list.Members, client.MessageChannel) + list.Unlock() + } } client.CurrentChannels = nil client.Mutex.Unlock() @@ -118,15 +164,26 @@ func UnsubscribeAll(client *ClientInfo) { client.Mutex.Lock() for _, v := range client.WatchingChannels { list := WatchingSubscriptionInfo[v] - list.Lock() - RemoveFromSliceC(&list.Members, client.MessageChannel) - list.Unlock() + if list != nil { + list.Lock() + RemoveFromSliceC(&list.Members, client.MessageChannel) + list.Unlock() + } } client.WatchingChannels = nil client.Mutex.Unlock() WatchingSubscriptionLock.RUnlock() } +func unsubscribeAllClients() { + ChatSubscriptionLock.Lock() + ChatSubscriptionInfo = make(map[string]*SubscriberList) + ChatSubscriptionLock.Unlock() + WatchingSubscriptionLock.Lock() + WatchingSubscriptionInfo = make(map[string]*SubscriberList) + WatchingSubscriptionLock.Unlock() +} + func UnsubscribeSingleChat(client *ClientInfo, channelName string) { ChatSubscriptionLock.RLock() list := ChatSubscriptionInfo[channelName] diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go new file mode 100644 index 00000000..ec4855c3 --- /dev/null +++ b/socketserver/internal/server/publisher_test.go @@ -0,0 +1,141 @@ +package server +import ( + "testing" + "net/http/httptest" + "net/http" + "sync" + "golang.org/x/net/websocket" + "github.com/satori/go.uuid" + "fmt" + "syscall" + "os" + "io/ioutil" +) + +func CountOpenFDs() uint64 { + ary, _ := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", os.Getpid())) + return uint64(len(ary)) +} + +func BenchmarkThousandUserSubscription(b *testing.B) { + var doneWg sync.WaitGroup + var readyWg sync.WaitGroup + + const TestChannelName = "testchannel" + const TestCommand = "testdata" + + GenerateKeys("/tmp/test_naclkeys.json", "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") + conf := &Config{ + UseSSL: false, + NaclKeysFile: "/tmp/test_naclkeys.json", + SocketOrigin: "localhost:2002", + } + serveMux := http.NewServeMux() + SetupServerAndHandle(conf, nil, serveMux) + + server := httptest.NewUnstartedServer(serveMux) + server.Start() + + wsUrl := fmt.Sprintf("ws://%s/", server.Listener.Addr().String()) + originUrl := fmt.Sprintf("http://%s", server.Listener.Addr().String()) + + message := ClientMessage{MessageID: -1, Command: "testdata", Arguments: "123456789"} + + fmt.Println() + fmt.Println(b.N) + + var limit syscall.Rlimit + syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit) + + limit.Cur = CountOpenFDs() + uint64(b.N) * 2 + 100 + + if limit.Cur > limit.Max { + b.Skip("Open file limit too low") + return + } + + syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + conn, err := websocket.Dial(wsUrl, "", originUrl) + if err != nil { + b.Error(err) + break + } + doneWg.Add(1) + readyWg.Add(1) + go func(i int, conn *websocket.Conn) { + var err error + var msg ClientMessage + err = FFZCodec.Send(conn, ClientMessage{MessageID: 1, Command: HelloCommand, Arguments: []interface{}{"ffz_test", uuid.NewV4().String()}}) + if err != nil { + b.Error(err) + } + err = FFZCodec.Send(conn, ClientMessage{MessageID: 2, Command: "sub", Arguments: TestChannelName}) + if err != nil { + b.Error(err) + } + err = FFZCodec.Receive(conn, &msg) + if err != nil { + b.Error(err) + } + if msg.MessageID != 1 { + b.Error("Got out-of-order message ID", msg) + } + if msg.Command != SuccessCommand { + b.Error("Command was not a success", msg) + } + err = FFZCodec.Receive(conn, &msg) + if err != nil { + b.Error(err) + } + if msg.MessageID != 2 { + b.Error("Got out-of-order message ID", msg) + } + if msg.Command != SuccessCommand { + b.Error("Command was not a success", msg) + } + + fmt.Println(i, " ready") + readyWg.Done() + + err = FFZCodec.Receive(conn, &msg) + if err != nil { + b.Error(err) + } + if msg.MessageID != -1 { + fmt.Println(msg) + b.Error("Client did not get expected messageID of -1") + } + if msg.Command != TestCommand { + fmt.Println(msg) + b.Error("Client did not get expected command") + } + str, err := msg.ArgumentsAsString() + if err != nil { + b.Error(err) + } + if str != "123456789" { + fmt.Println(msg) + b.Error("Client did not get expected data") + } + conn.Close() + doneWg.Done() + }(i, conn) + } + + readyWg.Wait() + + fmt.Println("publishing...") + if PublishToChat(TestChannelName, message) != b.N { + b.Error("not enough sent") + b.FailNow() + } + doneWg.Wait() + + b.StopTimer() + server.Close() + unsubscribeAllClients() + server.CloseClientConnections() +} From df7d60755646a1f282dacae824ab2f10840317b5 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 08:58:04 -0700 Subject: [PATCH 15/29] Move HandlePublishRequest to backend.go --- socketserver/internal/server/backend.go | 26 ++++++++++++++++++++ socketserver/internal/server/backend_test.go | 2 ++ socketserver/internal/server/publisher.go | 26 -------------------- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 6c95c261..e3bd77d5 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -64,6 +64,32 @@ func getCacheKey(remoteCommand, data string) string { return fmt.Sprintf("%s/%s", remoteCommand, data) } +func HandlePublishRequest(w http.ResponseWriter, r *http.Request) { + formData, err := UnsealRequest(r.Form) + if err != nil { + w.WriteHeader(403) + fmt.Fprintf(w, "Error: %v", err) + return + } + + cmd := formData.Get("cmd") + json := formData.Get("args") + chat := formData.Get("chat") + watchChannel := formData.Get("channel") + cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} + var count int + if chat != "" { + count = PublishToChat(chat, cm) + } else if watchChannel != "" { + count = PublishToWatchers(watchChannel, cm) + } else { + w.WriteHeader(400) + fmt.Fprint(w, "Need to specify either chat or channel") + return + } + fmt.Fprint(w, count) +} + func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) { cached, ok := responseCache.Get(getCacheKey(remoteCommand, data)) if ok { diff --git a/socketserver/internal/server/backend_test.go b/socketserver/internal/server/backend_test.go index e02d6c0e..0efea329 100644 --- a/socketserver/internal/server/backend_test.go +++ b/socketserver/internal/server/backend_test.go @@ -31,6 +31,8 @@ func TestSealRequest(t *testing.T) { if err != nil { t.Fatal(err) } + // sealedValues.Encode() + // id=0&msg=KKtbng49dOLLyjeuX5AnXiEe6P0uZwgeP_7mMB5vhP-wMAAPZw%3D%3D&nonce=-wRbUnifscisWUvhm3gBEXHN5QzrfzgV unsealedValues, err := UnsealRequest(sealedValues) if err != nil { diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index eae5488e..e3b57378 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -50,32 +50,6 @@ func PublishToWatchers(channel string, msg ClientMessage) (count int) { return } -func HandlePublishRequest(w http.ResponseWriter, r *http.Request) { - formData, err := UnsealRequest(r.Form) - if err != nil { - w.WriteHeader(403) - fmt.Fprintf(w, "Error: %v", err) - return - } - - cmd := formData.Get("cmd") - json := formData.Get("args") - chat := formData.Get("chat") - watchChannel := formData.Get("channel") - cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} - var count int - if chat != "" { - count = PublishToChat(chat, cm) - } else if watchChannel != "" { - count = PublishToWatchers(watchChannel, cm) - } else { - w.WriteHeader(400) - fmt.Fprint(w, "Need to specify either chat or channel") - return - } - fmt.Fprint(w, count) -} - // Add a channel to the subscriptions while holding a read-lock to the map. // Locks: // - ALREADY HOLDING a read-lock to the 'which' top-level map via the rlocker object From 8918b9ac3a2bffe88b1a4e1acf784063fec0e96e Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 10:06:45 -0700 Subject: [PATCH 16/29] Reformat and add global message subscriptions --- socketserver/cmd/socketserver/socketserver.go | 13 +-- socketserver/internal/server/backend.go | 32 +++--- socketserver/internal/server/backend_test.go | 7 +- socketserver/internal/server/backlog.go | 1 + socketserver/internal/server/commands.go | 17 +-- socketserver/internal/server/handlecore.go | 107 +++++++++++------- .../internal/server/handlecore_test.go | 6 +- socketserver/internal/server/publisher.go | 81 +++++++------ .../internal/server/publisher_test.go | 21 ++-- socketserver/internal/server/types.go | 27 +++-- socketserver/internal/server/utils.go | 28 ++--- 11 files changed, 182 insertions(+), 158 deletions(-) create mode 100644 socketserver/internal/server/backlog.go diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index 463d8774..0b0e052d 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -1,10 +1,10 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/socketserver" import ( + "../../internal/server" "flag" "log" "net/http" - "../../internal/server" ) var origin *string = flag.String("origin", "localhost:8001", "Client-visible origin of the socket server") @@ -34,11 +34,11 @@ func main() { log.Fatalln("Either both --crt and --key can be provided, or neither.") } - conf := &server.Config { - SSLKeyFile: *privateKeyFile, + conf := &server.Config{ + SSLKeyFile: *privateKeyFile, SSLCertificateFile: *certificateFile, - UseSSL: *usessl, - NaclKeysFile: *naclKeysFile, + UseSSL: *usessl, + NaclKeysFile: *naclKeysFile, SocketOrigin: *origin, } @@ -61,7 +61,6 @@ func main() { } } - func GenerateKeys(outputFile string) { if flag.NArg() < 1 { log.Fatal("The server ID must be specified") @@ -71,4 +70,4 @@ func GenerateKeys(outputFile string) { } else { server.GenerateKeys(outputFile, flag.Arg(0), "") } -} \ No newline at end of file +} diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index e3bd77d5..d02baaeb 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -1,21 +1,21 @@ package server import ( - "golang.org/x/crypto/nacl/box" - "net/http" - "time" - "fmt" - "net/url" - "github.com/pmylund/go-cache" - "strconv" - "io/ioutil" - "encoding/json" - "sync" - "log" - "os" "crypto/rand" "encoding/base64" + "encoding/json" + "fmt" + "github.com/pmylund/go-cache" + "golang.org/x/crypto/nacl/box" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strconv" "strings" + "sync" + "time" ) var backendHttpClient http.Client @@ -35,7 +35,7 @@ func SetupBackend(config *Config) { if responseCache != nil { responseCache.Flush() } - responseCache = cache.New(60 * time.Second, 120 * time.Second) + responseCache = cache.New(60*time.Second, 120*time.Second) getBacklogUrl = fmt.Sprintf("%s/backlog", backendUrl) @@ -64,7 +64,7 @@ func getCacheKey(remoteCommand, data string) string { return fmt.Sprintf("%s/%s", remoteCommand, data) } -func HandlePublishRequest(w http.ResponseWriter, r *http.Request) { +func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { formData, err := UnsealRequest(r.Form) if err != nil { w.WriteHeader(403) @@ -109,7 +109,7 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s formData := url.Values{ "clientData": []string{data}, - authKey: []string{auth.TwitchUsername}, + authKey: []string{auth.TwitchUsername}, } sealedForm, err := SealRequest(formData) @@ -144,7 +144,7 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s func FetchBacklogData(chatSubs, channelSubs []string) ([]ClientMessage, error) { formData := url.Values{ - "chatSubs": chatSubs, + "chatSubs": chatSubs, "channelSubs": channelSubs, } diff --git a/socketserver/internal/server/backend_test.go b/socketserver/internal/server/backend_test.go index 0efea329..7043d9f3 100644 --- a/socketserver/internal/server/backend_test.go +++ b/socketserver/internal/server/backend_test.go @@ -1,9 +1,10 @@ package server + import ( - "testing" - "net/url" - "golang.org/x/crypto/nacl/box" "crypto/rand" + "golang.org/x/crypto/nacl/box" + "net/url" + "testing" ) func SetupRandomKeys(t testing.TB) { diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go new file mode 100644 index 00000000..abb4e431 --- /dev/null +++ b/socketserver/internal/server/backlog.go @@ -0,0 +1 @@ +package server diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index e30b543f..ac5cae42 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -1,11 +1,11 @@ package server import ( - "golang.org/x/net/websocket" "github.com/satori/go.uuid" + "golang.org/x/net/websocket" "log" - "sync" "strconv" + "sync" "time" ) @@ -23,7 +23,7 @@ func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) return } -// log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) + // log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) response, err := CallHandler(handler, conn, client, msg) @@ -36,7 +36,7 @@ func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) } else { FFZCodec.Send(conn, ClientMessage{ MessageID: msg.MessageID, - Command: "error", + Command: "error", Arguments: err.Error(), }) } @@ -203,6 +203,7 @@ type SurveySubmission struct { User string Json string } + var SurveySubmissions []SurveySubmission var SurveySubmissionLock sync.Mutex @@ -215,11 +216,12 @@ func HandleSurvey(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) ( } type FollowEvent struct { - User string - Channel string + User string + Channel string NowFollowing bool - Timestamp time.Time + Timestamp time.Time } + var FollowEvents []FollowEvent var FollowEventsLock sync.Mutex @@ -268,7 +270,6 @@ func HandleEmoticonUses(conn *websocket.Conn, client *ClientInfo, msg ClientMess } } - return ResponseSuccess, nil } diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 7ef4e533..910bb4d6 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -1,16 +1,16 @@ package server // import "bitbucket.org/stendec/frankerfacez/socketserver/server" import ( - "net/http" - "golang.org/x/net/websocket" "crypto/tls" - "strings" - "strconv" - "errors" "encoding/json" + "errors" "fmt" - "sync" + "golang.org/x/net/websocket" "log" + "net/http" + "strconv" + "strings" + "sync" ) const MAX_PACKET_SIZE = 1024 @@ -22,12 +22,12 @@ type Config struct { UseSSL bool // NaCl keys for backend messages - NaclKeysFile string + NaclKeysFile string // Hostname of the socket server - SocketOrigin string + SocketOrigin string // URL to the backend server - BackendUrl string + BackendUrl string } // A command is how the client refers to a function on the server. It's just a string. @@ -38,37 +38,40 @@ type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMes var CommandHandlers = map[Command]CommandHandler{ HelloCommand: HandleHello, - "setuser": HandleSetUser, + "setuser": HandleSetUser, - "sub": HandleSub, - "unsub": HandleUnsub, - "sub_channel": HandleSubChannel, + "sub": HandleSub, + "unsub": HandleUnsub, + "sub_channel": HandleSubChannel, "unsub_channel": HandleUnsubChannel, - "track_follow": HandleTrackFollow, + "track_follow": HandleTrackFollow, "emoticon_uses": HandleEmoticonUses, - "survey": HandleSurvey, + "survey": HandleSurvey, - "twitch_emote": HandleRemoteCommand, - "get_link": HandleRemoteCommand, - "get_display_name": HandleRemoteCommand, + "twitch_emote": HandleRemoteCommand, + "get_link": HandleRemoteCommand, + "get_display_name": HandleRemoteCommand, "update_follow_buttons": HandleRemoteCommand, - "chat_history": HandleRemoteCommand, + "chat_history": HandleRemoteCommand, } // Sent by the server in ClientMessage.Command to indicate success. const SuccessCommand Command = "True" + // Sent by the server in ClientMessage.Command to indicate failure. const ErrorCommand Command = "error" + // This must be the first command sent by the client once the connection is established. const HelloCommand Command = "hello" + // A handler returning a ClientMessage with this Command will prevent replying to the client. // It signals that the work has been handed off to a background goroutine. const AsyncResponseCommand Command = "_async" // A websocket.Codec that translates the protocol into ClientMessage objects. var FFZCodec websocket.Codec = websocket.Codec{ - Marshal: MarshalClientMessage, + Marshal: MarshalClientMessage, Unmarshal: UnmarshalClientMessage, } @@ -124,7 +127,7 @@ func SetupServerAndHandle(config *Config, tlsConfig *tls.Config, serveMux *http. serveMux = http.DefaultServeMux } serveMux.HandleFunc("/", sockServer.ServeHTTP) - serveMux.HandleFunc("/pub", HandlePublishRequest) + serveMux.HandleFunc("/pub", HBackendPublishRequest) } // Handle a new websocket connection from a FFZ client. @@ -147,7 +150,7 @@ func HandleSocketConnection(conn *websocket.Conn) { _errorChan := make(chan error) // Launch receiver goroutine - go func(errorChan chan <- error, clientChan chan <- ClientMessage) { + go func(errorChan chan<- error, clientChan chan<- ClientMessage) { var msg ClientMessage var err error for ; err == nil; err = FFZCodec.Receive(conn, &msg) { @@ -171,13 +174,13 @@ func HandleSocketConnection(conn *websocket.Conn) { // All set up, now enter the work loop - RunLoop: +RunLoop: for { select { case err := <-errorChan: FFZCodec.Send(conn, ClientMessage{ MessageID: -1, - Command: "error", + Command: "error", Arguments: err.Error(), }) // note - socket might be closed, but don't care break RunLoop @@ -185,7 +188,7 @@ func HandleSocketConnection(conn *websocket.Conn) { if client.Version == "" && msg.Command != HelloCommand { FFZCodec.Send(conn, ClientMessage{ MessageID: msg.MessageID, - Command: "error", + Command: "error", Arguments: "Error - the first message sent must be a 'hello'", }) break RunLoop @@ -201,7 +204,8 @@ func HandleSocketConnection(conn *websocket.Conn) { // Launch message draining goroutine - we aren't out of the pub/sub records go func() { - for _ = range _serverMessageChan {} + for _ = range _serverMessageChan { + } }() // Stop getting messages... @@ -244,7 +248,7 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e } out.MessageID = messageId - dataStr = dataStr[spaceIdx + 1:] + dataStr = dataStr[spaceIdx+1:] spaceIdx = strings.IndexRune(dataStr, ' ') if spaceIdx == -1 { @@ -254,7 +258,7 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e } else { out.Command = Command(dataStr[:spaceIdx]) } - dataStr = dataStr[spaceIdx + 1:] + dataStr = dataStr[spaceIdx+1:] argumentsJson := dataStr out.origArguments = argumentsJson err = json.Unmarshal([]byte(argumentsJson), &out.Arguments) @@ -306,7 +310,7 @@ func MarshalClientMessage(clientMessage interface{}) (data []byte, payloadType b func NewClientMessage(arguments interface{}) ClientMessage { return ClientMessage{ MessageID: 0, // filled by the select loop - Command: SuccessCommand, + Command: SuccessCommand, Arguments: arguments, } } @@ -316,7 +320,8 @@ func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { var ok bool string1, ok = cm.Arguments.(string) if !ok { - err = ExpectedSingleString; return + err = ExpectedSingleString + return } else { return string1, nil } @@ -328,7 +333,8 @@ func (cm *ClientMessage) ArgumentsAsInt() (int1 int, err error) { var num float64 num, ok = cm.Arguments.(float64) if !ok { - err = ExpectedSingleInt; return + err = ExpectedSingleInt + return } else { int1 = int(num) return int1, nil @@ -341,18 +347,22 @@ func (cm *ClientMessage) ArgumentsAsTwoStrings() (string1, string2 string, err e var ary []interface{} ary, ok = cm.Arguments.([]interface{}) if !ok { - err = ExpectedTwoStrings; return + err = ExpectedTwoStrings + return } else { if len(ary) != 2 { - err = ExpectedTwoStrings; return + err = ExpectedTwoStrings + return } string1, ok = ary[0].(string) if !ok { - err = ExpectedTwoStrings; return + err = ExpectedTwoStrings + return } string2, ok = ary[1].(string) if !ok { - err = ExpectedTwoStrings; return + err = ExpectedTwoStrings + return } return string1, string2, nil } @@ -364,23 +374,28 @@ func (cm *ClientMessage) ArgumentsAsStringAndInt() (string1 string, int int64, e var ary []interface{} ary, ok = cm.Arguments.([]interface{}) if !ok { - err = ExpectedStringAndInt; return + err = ExpectedStringAndInt + return } else { if len(ary) != 2 { - err = ExpectedStringAndInt; return + err = ExpectedStringAndInt + return } string1, ok = ary[0].(string) if !ok { - err = ExpectedStringAndInt; return + err = ExpectedStringAndInt + return } var num float64 num, ok = ary[1].(float64) if !ok { - err = ExpectedStringAndInt; return + err = ExpectedStringAndInt + return } int = int64(num) if float64(int) != num { - err = ExpectedStringAndIntGotFloat; return + err = ExpectedStringAndIntGotFloat + return } return string1, int, nil } @@ -392,18 +407,22 @@ func (cm *ClientMessage) ArgumentsAsStringAndBool() (str string, flag bool, err var ary []interface{} ary, ok = cm.Arguments.([]interface{}) if !ok { - err = ExpectedStringAndBool; return + err = ExpectedStringAndBool + return } else { if len(ary) != 2 { - err = ExpectedStringAndBool; return + err = ExpectedStringAndBool + return } str, ok = ary[0].(string) if !ok { - err = ExpectedStringAndBool; return + err = ExpectedStringAndBool + return } flag, ok = ary[1].(bool) if !ok { - err = ExpectedStringAndBool; return + err = ExpectedStringAndBool + return } return str, flag, nil } diff --git a/socketserver/internal/server/handlecore_test.go b/socketserver/internal/server/handlecore_test.go index 0350a7e3..161b5921 100644 --- a/socketserver/internal/server/handlecore_test.go +++ b/socketserver/internal/server/handlecore_test.go @@ -1,8 +1,8 @@ package server import ( - "golang.org/x/net/websocket" "fmt" + "golang.org/x/net/websocket" "testing" ) @@ -24,7 +24,7 @@ func ExampleUnmarshalClientMessage() { func ExampleMarshalClientMessage() { var cm ClientMessage = ClientMessage{ MessageID: -1, - Command: "do_authorize", + Command: "do_authorize", Arguments: "1234567890", } data, payloadType, err := MarshalClientMessage(&cm) @@ -54,4 +54,4 @@ func TestArgumentsAsStringAndBool(t *testing.T) { if boolean != false { t.Error("Expected second array item to be false, got", boolean) } -} \ No newline at end of file +} diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index e3b57378..659a3038 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -4,29 +4,30 @@ package server // If I screwed up the locking, I won't know until it's too late. import ( + "fmt" + "net/http" "sync" "time" - "net/http" - "fmt" ) type SubscriberList struct { sync.RWMutex - Members []chan <- ClientMessage + Members []chan<- ClientMessage } var ChatSubscriptionInfo map[string]*SubscriberList = make(map[string]*SubscriberList) var ChatSubscriptionLock sync.RWMutex var WatchingSubscriptionInfo map[string]*SubscriberList = make(map[string]*SubscriberList) var WatchingSubscriptionLock sync.RWMutex +var GlobalSubscriptionInfo SubscriberList func PublishToChat(channel string, msg ClientMessage) (count int) { ChatSubscriptionLock.RLock() list := ChatSubscriptionInfo[channel] if list != nil { list.RLock() - for _, ch := range list.Members { - ch <- msg + for _, msgChan := range list.Members { + msgChan <- msg count++ } list.RUnlock() @@ -40,8 +41,8 @@ func PublishToWatchers(channel string, msg ClientMessage) (count int) { list := WatchingSubscriptionInfo[channel] if list != nil { list.RLock() - for _, ch := range list.Members { - ch <- msg + for _, msgChan := range list.Members { + msgChan <- msg count++ } list.RUnlock() @@ -50,19 +51,28 @@ func PublishToWatchers(channel string, msg ClientMessage) (count int) { return } +func PublishToAll(msg ClientMessage) (count int) { + GlobalSubscriptionInfo.RLock() + for _, msgChan := range GlobalSubscriptionInfo.Members { + msgChan <- msg + count++ + } + GlobalSubscriptionInfo.RUnlock() +} + // Add a channel to the subscriptions while holding a read-lock to the map. // Locks: // - ALREADY HOLDING a read-lock to the 'which' top-level map via the rlocker object // - possible write lock to the 'which' top-level map via the wlocker object // - write lock to SubscriptionInfo (if not creating new) -func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string, value chan <- ClientMessage, rlocker sync.Locker, wlocker sync.Locker) { +func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string, value chan<- ClientMessage, rlocker sync.Locker, wlocker sync.Locker) { list := which[channelName] if list == nil { // Not found, so create it rlocker.Unlock() wlocker.Lock() list = &SubscriberList{} - list.Members = []chan <- ClientMessage{value} // Create it populated, to avoid reaper + list.Members = []chan<- ClientMessage{value} // Create it populated, to avoid reaper which[channelName] = list wlocker.Unlock() rlocker.Lock() @@ -73,6 +83,12 @@ func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string } } +func SubscribeGlobal(client *ClientInfo) { + GlobalSubscriptionInfo.Lock() + AddToSliceC(&GlobalSubscriptionInfo.Members, client.MessageChannel) + GlobalSubscriptionInfo.Unlock() +} + func SubscribeChat(client *ClientInfo, channelName string) { ChatSubscriptionLock.RLock() _subscribeWhileRlocked(ChatSubscriptionInfo, channelName, client.MessageChannel, ChatSubscriptionLock.RLocker(), &ChatSubscriptionLock) @@ -85,28 +101,16 @@ func SubscribeWatching(client *ClientInfo, channelName string) { WatchingSubscriptionLock.RUnlock() } -// Locks: -// - read lock to top-level maps -// - possible write lock to top-level maps -// - write lock to SubscriptionInfos -func SubscribeBatch(client *ClientInfo, chatSubs, channelSubs []string) { - mchan := client.MessageChannel - if len(chatSubs) > 0 { - rlocker := ChatSubscriptionLock.RLocker() - rlocker.Lock() - for _, v := range chatSubs { - _subscribeWhileRlocked(ChatSubscriptionInfo, v, mchan, rlocker, &ChatSubscriptionLock) - } - rlocker.Unlock() - } - if len(channelSubs) > 0 { - rlocker := WatchingSubscriptionLock.RLocker() - rlocker.Lock() - for _, v := range channelSubs { - _subscribeWhileRlocked(WatchingSubscriptionInfo, v, mchan, rlocker, &WatchingSubscriptionLock) - } - rlocker.Unlock() - } +func unsubscribeAllClients() { + GlobalSubscriptionInfo.Lock() + GlobalSubscriptionInfo.Members = nil + GlobalSubscriptionInfo.Unlock() + ChatSubscriptionLock.Lock() + ChatSubscriptionInfo = make(map[string]*SubscriberList) + ChatSubscriptionLock.Unlock() + WatchingSubscriptionLock.Lock() + WatchingSubscriptionInfo = make(map[string]*SubscriberList) + WatchingSubscriptionLock.Unlock() } // Unsubscribe the client from all channels, AND clear the CurrentChannels / WatchingChannels fields. @@ -120,6 +124,10 @@ func UnsubscribeAll(client *ClientInfo) { client.PendingStreamBacklogs = nil client.Mutex.Unlock() + GlobalSubscriptionInfo.Lock() + RemoveFromSliceC(&GlobalSubscriptionInfo.Members, client.MessageChannel) + GlobalSubscriptionInfo.Unlock() + ChatSubscriptionLock.RLock() client.Mutex.Lock() for _, v := range client.CurrentChannels { @@ -149,15 +157,6 @@ func UnsubscribeAll(client *ClientInfo) { WatchingSubscriptionLock.RUnlock() } -func unsubscribeAllClients() { - ChatSubscriptionLock.Lock() - ChatSubscriptionInfo = make(map[string]*SubscriberList) - ChatSubscriptionLock.Unlock() - WatchingSubscriptionLock.Lock() - WatchingSubscriptionInfo = make(map[string]*SubscriberList) - WatchingSubscriptionLock.Unlock() -} - func UnsubscribeSingleChat(client *ClientInfo, channelName string) { ChatSubscriptionLock.RLock() list := ChatSubscriptionInfo[channelName] @@ -199,4 +198,4 @@ func deadChannelReaper() { } } } -} \ No newline at end of file +} diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index ec4855c3..f25235a1 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -1,15 +1,16 @@ package server + import ( - "testing" - "net/http/httptest" - "net/http" - "sync" - "golang.org/x/net/websocket" - "github.com/satori/go.uuid" "fmt" - "syscall" - "os" + "github.com/satori/go.uuid" + "golang.org/x/net/websocket" "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "sync" + "syscall" + "testing" ) func CountOpenFDs() uint64 { @@ -26,7 +27,7 @@ func BenchmarkThousandUserSubscription(b *testing.B) { GenerateKeys("/tmp/test_naclkeys.json", "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") conf := &Config{ - UseSSL: false, + UseSSL: false, NaclKeysFile: "/tmp/test_naclkeys.json", SocketOrigin: "localhost:2002", } @@ -47,7 +48,7 @@ func BenchmarkThousandUserSubscription(b *testing.B) { var limit syscall.Rlimit syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit) - limit.Cur = CountOpenFDs() + uint64(b.N) * 2 + 100 + limit.Cur = CountOpenFDs() + uint64(b.N)*2 + 100 if limit.Cur > limit.Max { b.Skip("Open file limit too low") diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index be3145e1..1c052ca1 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -1,6 +1,9 @@ package server import ( + "encoding/json" + "errors" + "fmt" "github.com/satori/go.uuid" "sync" "time" @@ -9,30 +12,30 @@ import ( const CryptoBoxKeyLength = 32 type CryptoKeysBuf struct { - OurPrivateKey []byte - OurPublicKey []byte + OurPrivateKey []byte + OurPublicKey []byte TheirPublicKey []byte - ServerId int + ServerId int } type ClientMessage struct { // Message ID. Increments by 1 for each message sent from the client. // When replying to a command, the message ID must be echoed. // When sending a server-initiated message, this is -1. - MessageID int `json:_` + MessageID int `json:_` // The command that the client wants from the server. // When sent from the server, the literal string 'True' indicates success. // Before sending, a blank Command will be converted into SuccessCommand. - Command Command `json:cmd` + Command Command `json:cmd` // Result of json.Unmarshal on the third field send from the client - Arguments interface{} `json:data` + Arguments interface{} `json:data` origArguments string } type AuthInfo struct { // The client's claimed username on Twitch. - TwitchUsername string + TwitchUsername string // Whether or not the server has validated the client's claimed username. UsernameValidated bool @@ -41,25 +44,25 @@ type AuthInfo struct { type ClientInfo struct { // The client ID. // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - ClientID uuid.UUID + ClientID uuid.UUID // The client's version. // This must be written once by the owning goroutine before the struct is passed off to any other goroutines. - Version string + Version string // This mutex protects writable data in this struct. // If it seems to be a performance problem, we can split this. - Mutex sync.Mutex + Mutex sync.Mutex // TODO(riking) - does this need to be protected cross-thread? AuthInfo // Username validation nonce. - ValidationNonce string + ValidationNonce string // The list of chats this client is currently in. // Protected by Mutex. - CurrentChannels []string + CurrentChannels []string // This list of channels this client needs UI updates for. // Protected by Mutex. diff --git a/socketserver/internal/server/utils.go b/socketserver/internal/server/utils.go index a3853cd0..8dbff0f4 100644 --- a/socketserver/internal/server/utils.go +++ b/socketserver/internal/server/utils.go @@ -1,15 +1,15 @@ package server import ( - "crypto/rand" - "net/url" - "golang.org/x/crypto/nacl/box" "bytes" + "crypto/rand" "encoding/base64" + "errors" + "golang.org/x/crypto/nacl/box" + "log" + "net/url" "strconv" "strings" - "errors" - "log" ) func FillCryptoRandom(buf []byte) error { @@ -53,8 +53,8 @@ func SealRequest(form url.Values) (url.Values, error) { retval := url.Values{ "nonce": []string{nonceString}, - "msg": []string{cipherString}, - "id": []string{strconv.Itoa(serverId)}, + "msg": []string{cipherString}, + "id": []string{strconv.Itoa(serverId)}, } return retval, nil @@ -122,13 +122,13 @@ func RemoveFromSliceS(ary *[]string, val string) bool { return false } - slice[idx] = slice[len(slice) - 1] - slice = slice[:len(slice) - 1] + slice[idx] = slice[len(slice)-1] + slice = slice[:len(slice)-1] *ary = slice return true } -func AddToSliceC(ary *[]chan <- ClientMessage, val chan <- ClientMessage) bool { +func AddToSliceC(ary *[]chan<- ClientMessage, val chan<- ClientMessage) bool { slice := *ary for _, v := range slice { if v == val { @@ -141,7 +141,7 @@ func AddToSliceC(ary *[]chan <- ClientMessage, val chan <- ClientMessage) bool { return true } -func RemoveFromSliceC(ary *[]chan <- ClientMessage, val chan <- ClientMessage) bool { +func RemoveFromSliceC(ary *[]chan<- ClientMessage, val chan<- ClientMessage) bool { slice := *ary var idx int = -1 for i, v := range slice { @@ -154,8 +154,8 @@ func RemoveFromSliceC(ary *[]chan <- ClientMessage, val chan <- ClientMessage) b return false } - slice[idx] = slice[len(slice) - 1] - slice = slice[:len(slice) - 1] + slice[idx] = slice[len(slice)-1] + slice = slice[:len(slice)-1] *ary = slice return true -} \ No newline at end of file +} From ae1306387e4eeb2f1fcbfc5795578b21a732f4de Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 10:07:15 -0700 Subject: [PATCH 17/29] Let's start caching some of the backlog data on the server --- socketserver/internal/server/backend.go | 14 +-- socketserver/internal/server/backlog.go | 88 +++++++++++++++++ socketserver/internal/server/commands.go | 2 + socketserver/internal/server/types.go | 117 ++++++++++++++++++++++- 4 files changed, 214 insertions(+), 7 deletions(-) diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index d02baaeb..70c826b2 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -74,14 +74,16 @@ func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { cmd := formData.Get("cmd") json := formData.Get("args") - chat := formData.Get("chat") - watchChannel := formData.Get("channel") + channel := formData.Get("channel") + scope := formData.Get("scope") cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} var count int - if chat != "" { - count = PublishToChat(chat, cm) - } else if watchChannel != "" { - count = PublishToWatchers(watchChannel, cm) + if scope == "chat" { + count = PublishToChat(channel, cm) + } else if scope == "channel" { + count = PublishToWatchers(channel, cm) + } else if scope == "global" { + count = PublishToAll(cm) } else { w.WriteHeader(400) fmt.Fprint(w, "Need to specify either chat or channel") diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index abb4e431..82fd500c 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -1 +1,89 @@ package server + +import ( + "errors" + "fmt" + "net/http" +) + +// this value is just docs right now +var ServerInitiatedCommands = []string{ + /// Global updates & notices + "update_news", // timecache:global + "message", // timecache:global + "reload_ff", // timecache:global + + /// Emote updates + "reload_badges", // timecache:global + "set_badge", // timecache:multichat + "reload_set", // timecache:multichat + "load_set", // TODO what are the semantics of this? + + /// User auth + "do_authorize", // nocache:single + + /// Channel data + // extra emote sets included in the chat + "follow_sets", // mustcache:chat + // extra follow buttons below the stream + "follow_buttons", // mustcache:watching + // SRL race data + "srl_race", // cachelast:watching + + /// Chatter/viewer counts + "chatters", // cachelast:watching + "viewers", // cachelast:watching +} +var _ = ServerInitiatedCommands + +type BacklogCacheType int + +const ( + // This is not a cache type. + CacheTypeInvalid BacklogCacheType = iota + // This message cannot be cached. + CacheTypeNever + // Save the last 24 hours of this message. + // If a client indicates that it has reconnected, replay the messages sent after the disconnect. + CacheTypeTimestamps + // Save only the last copy of this message, and always send it when the backlog is requested. + CacheTypeLastOnly + // Save this backlog data to disk with its timestamp. + // Send it when the backlog is requested, or after a reconnect if it was updated. + CacheTypePersistent +) + +type MessageTargetType int + +const ( + // This is not a message target. + MsgTargetTypeInvalid MessageTargetType = iota + // This message is targeted to a single TODO(user or connection) + MsgTargetTypeSingle + // This message is targeted to all users in a chat + MsgTargetTypeChat + // This message is targeted to all users in multiple chats + MsgTargetTypeMultichat + // This message is targeted to all users watching a stream + MsgTargetTypeWatching + // This message is sent to all FFZ users. + MsgTargetTypeGlobal +) + +// Returned by BacklogCacheType.UnmarshalJSON() +var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype") + +// Returned by MessageTargetType.UnmarshalJSON() +var ErrorUnrecognizedTargetType = errors.New("Invalid value for message target") + +// note: see types.go for methods on these + +func HBackendSaveBacklog(w http.ResponseWriter, r *http.Request) { + formData, err := UnsealRequest(r.Form) + if err != nil { + w.WriteHeader(403) + fmt.Fprintf(w, "Error: %v", err) + return + } + +} diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index ac5cae42..abbf01ff 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -54,6 +54,8 @@ func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (r client.ClientID = uuid.NewV4() } + SubscribeGlobal(client) + return ClientMessage{ Arguments: client.ClientID.String(), }, nil diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 1c052ca1..5ce92c81 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -84,5 +84,120 @@ type ClientInfo struct { // Server-initiated messages should be sent here // Never nil. - MessageChannel chan <- ClientMessage + MessageChannel chan<- ClientMessage +} + +func (bct BacklogCacheType) Name() string { + switch bct { + case CacheTypeInvalid: + return "" + case CacheTypeNever: + return "never" + case CacheTypeTimestamps: + return "timed" + case CacheTypeLastOnly: + return "last" + case CacheTypePersistent: + return "persist" + } + panic("Invalid BacklogCacheType value") +} + +var CacheTypesByName = map[string]BacklogCacheType{ + "never": CacheTypeNever, + "timed": CacheTypeTimestamps, + "last": CacheTypeLastOnly, + "persist": CacheTypePersistent, +} + +func BacklogCacheTypeByName(name string) (bct BacklogCacheType) { + // CacheTypeInvalid is the zero value so it doesn't matter + bct, _ = CacheTypesByName[name] + return +} + +// Implements Stringer +func (bct BacklogCacheType) String() string { return bct.Name() } + +// Implements json.Marshaler +func (bct BacklogCacheType) MarshalJSON() ([]byte, error) { + return json.Marshal(bct.Name()) +} + +// Implements json.Unmarshaler +func (pbct *BacklogCacheType) UnmarshalJSON(data []byte) error { + var str string + err := json.Unmarshal(data, &str) + if err != nil { + return err + } + if str == "" { + *pbct = CacheTypeInvalid + return nil + } + val := BacklogCacheTypeByName(str) + if val != CacheTypeInvalid { + *pbct = val + return nil + } + return ErrorUnrecognizedCacheType +} + +func (mtt MessageTargetType) Name() string { + switch mtt { + case MsgTargetTypeInvalid: + return "" + case MsgTargetTypeSingle: + return "single" + case MsgTargetTypeChat: + return "chat" + case MsgTargetTypeMultichat: + return "multichat" + case MsgTargetTypeWatching: + return "channel" + case MsgTargetTypeGlobal: + return "global" + } + panic("Invalid MessageTargetType value") +} + +var TargetTypesByName = map[string]MessageTargetType{ + "single": MsgTargetTypeSingle, + "chat": MsgTargetTypeChat, + "multichat": MsgTargetTypeMultichat, + "channel": MsgTargetTypeWatching, + "global": MsgTargetTypeGlobal, +} + +func MessageTargetTypeByName(name string) (mtt MessageTargetType) { + // MsgTargetTypeInvalid is the zero value so it doesn't matter + mtt, _ = TargetTypesByName[name] + return +} + +// Implements Stringer +func (mtt MessageTargetType) String() string { return mtt.Name() } + +// Implements json.Marshaler +func (mtt MessageTargetType) MarshalJSON() ([]byte, error) { + return json.Marshal(mtt.Name()) +} + +// Implements json.Unmarshaler +func (pmtt *MessageTargetType) UnmarshalJSON(data []byte) error { + var str string + err := json.Unmarshal(data, &str) + if err != nil { + return err + } + if str == "" { + *pmtt = MsgTargetTypeInvalid + return nil + } + mtt := MessageTargetTypeByName(str) + if mtt != MsgTargetTypeInvalid { + *pmtt = mtt + return nil + } + return ErrorUnrecognizedTargetType } From 898057cc20edf918211074fcd81416494ed37db3 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 10:23:53 -0700 Subject: [PATCH 18/29] rework /pubmsg to use MessageTargetType --- socketserver/internal/server/backend.go | 33 ++++++++++++--- socketserver/internal/server/backlog.go | 49 +++++++++++++--------- socketserver/internal/server/handlecore.go | 3 +- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 70c826b2..00a55293 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -76,17 +76,38 @@ func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { json := formData.Get("args") channel := formData.Get("channel") scope := formData.Get("scope") + + target := MessageTargetTypeByName(scope) + + if cmd == "" { + w.WriteHeader(422) + fmt.Fprintf(w, "Error: cmd cannot be blank") + return + } + if channel == "" && (target == MsgTargetTypeChat || target == MsgTargetTypeMultichat || target == MsgTargetTypeWatching) { + w.WriteHeader(422) + fmt.Fprintf(w, "Error: channel must be specified") + return + } + cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} var count int - if scope == "chat" { + + switch target { + case MsgTargetTypeSingle: + // TODO + case MsgTargetTypeChat: count = PublishToChat(channel, cm) - } else if scope == "channel" { + case MsgTargetTypeMultichat: + // TODO + case MsgTargetTypeWatching: count = PublishToWatchers(channel, cm) - } else if scope == "global" { + case MsgTargetTypeGlobal: count = PublishToAll(cm) - } else { - w.WriteHeader(400) - fmt.Fprint(w, "Need to specify either chat or channel") + case MsgTargetTypeInvalid: + default: + w.WriteHeader(422) + fmt.Fprint(w, "Invalid 'scope'. must be single, chat, multichat, channel, or global") return } fmt.Fprint(w, count) diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index 82fd500c..24e0b6be 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -6,33 +6,37 @@ import ( "net/http" ) +type PushCommandCacheInfo struct { + Caching BacklogCacheType + Target MessageTargetType +} + // this value is just docs right now -var ServerInitiatedCommands = []string{ +var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ /// Global updates & notices - "update_news", // timecache:global - "message", // timecache:global - "reload_ff", // timecache:global + "update_news": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global + "message": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global + "reload_ff": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global /// Emote updates - "reload_badges", // timecache:global - "set_badge", // timecache:multichat - "reload_set", // timecache:multichat - "load_set", // TODO what are the semantics of this? + "reload_badges": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global + "set_badge": {CacheTypeTimestamps, MsgTargetTypeMultichat}, // timecache:multichat + "reload_set": {CacheTypeTimestamps, MsgTargetTypeMultichat}, // timecache:multichat + "load_set": {}, // TODO what are the semantics of this? /// User auth - "do_authorize", // nocache:single + "do_authorize": {CacheTypeNever, MsgTargetTypeSingle}, // nocache:single /// Channel data - // extra emote sets included in the chat - "follow_sets", // mustcache:chat - // extra follow buttons below the stream - "follow_buttons", // mustcache:watching - // SRL race data - "srl_race", // cachelast:watching + // follow_sets: extra emote sets included in the chat + // follow_buttons: extra follow buttons below the stream + "follow_sets": {CacheTypePersistent, MsgTargetTypeChat}, // mustcache:chat + "follow_buttons": {CacheTypePersistent, MsgTargetTypeWatching}, // mustcache:watching + "srl_race": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching /// Chatter/viewer counts - "chatters", // cachelast:watching - "viewers", // cachelast:watching + "chatters": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching + "viewers": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching } var _ = ServerInitiatedCommands @@ -70,15 +74,15 @@ const ( MsgTargetTypeGlobal ) +// note: see types.go for methods on these + // Returned by BacklogCacheType.UnmarshalJSON() var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype") // Returned by MessageTargetType.UnmarshalJSON() var ErrorUnrecognizedTargetType = errors.New("Invalid value for message target") -// note: see types.go for methods on these - -func HBackendSaveBacklog(w http.ResponseWriter, r *http.Request) { +func HBackendUpdateAndPublish(w http.ResponseWriter, r *http.Request) { formData, err := UnsealRequest(r.Form) if err != nil { w.WriteHeader(403) @@ -86,4 +90,9 @@ func HBackendSaveBacklog(w http.ResponseWriter, r *http.Request) { return } + cmd := formData.Get("command") + cacheinfo, ok := ServerInitiatedCommands[cmd] + if !ok { + w.WriteHeader(422) + } } diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 910bb4d6..f4b40653 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -127,7 +127,8 @@ func SetupServerAndHandle(config *Config, tlsConfig *tls.Config, serveMux *http. serveMux = http.DefaultServeMux } serveMux.HandleFunc("/", sockServer.ServeHTTP) - serveMux.HandleFunc("/pub", HBackendPublishRequest) + serveMux.HandleFunc("/pubmsg", HBackendPublishRequest) + serveMux.HandleFunc("/updatepub", HBackendUpdateAndPublish) } // Handle a new websocket connection from a FFZ client. From 730ce39f721440126a2f1149f6bf72ba2188cf0f Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 10:24:28 -0700 Subject: [PATCH 19/29] rename http targets --- socketserver/internal/server/handlecore.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index f4b40653..dd4c3f04 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -127,8 +127,8 @@ func SetupServerAndHandle(config *Config, tlsConfig *tls.Config, serveMux *http. serveMux = http.DefaultServeMux } serveMux.HandleFunc("/", sockServer.ServeHTTP) - serveMux.HandleFunc("/pubmsg", HBackendPublishRequest) - serveMux.HandleFunc("/updatepub", HBackendUpdateAndPublish) + serveMux.HandleFunc("/pub_msg", HBackendPublishRequest) + serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish) } // Handle a new websocket connection from a FFZ client. From 0be3693c99dcddf3351b4805851866d179881250 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 11:22:06 -0700 Subject: [PATCH 20/29] Add test that posts to /pub_msg, test infrastructure --- socketserver/internal/server/backend.go | 4 + socketserver/internal/server/backlog.go | 47 ++++- socketserver/internal/server/handlecore.go | 12 +- socketserver/internal/server/publisher.go | 3 +- .../internal/server/publisher_test.go | 187 +++++++++++++----- socketserver/internal/server/types.go | 2 - 6 files changed, 193 insertions(+), 62 deletions(-) diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 00a55293..c1073370 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -64,7 +64,10 @@ func getCacheKey(remoteCommand, data string) string { return fmt.Sprintf("%s/%s", remoteCommand, data) } +// Publish a message to clients with no caching. +// The scope must be specified because no attempt is made to recognize the command. func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { + r.ParseForm() formData, err := UnsealRequest(r.Form) if err != nil { w.WriteHeader(403) @@ -91,6 +94,7 @@ func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { } cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json} + cm.parseOrigArguments() var count int switch target { diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index 24e0b6be..d16b3cf5 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "time" ) type PushCommandCacheInfo struct { @@ -82,7 +83,22 @@ var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype") // Returned by MessageTargetType.UnmarshalJSON() var ErrorUnrecognizedTargetType = errors.New("Invalid value for message target") -func HBackendUpdateAndPublish(w http.ResponseWriter, r *http.Request) { +type PersistentCachedData struct { + Timestamp time.Time + Channel string + Watching bool + Data string +} + +// map command -> channel -> data +var CachedDataLast map[Command]map[string]string + +func DumpCache() { + CachedDataLast = make(map[Command]map[string]string) +} + +func HBackendDumpCache(w http.ResponseWriter, r *http.Request) { + r.ParseForm() formData, err := UnsealRequest(r.Form) if err != nil { w.WriteHeader(403) @@ -90,9 +106,36 @@ func HBackendUpdateAndPublish(w http.ResponseWriter, r *http.Request) { return } - cmd := formData.Get("command") + confirm := formData.Get("confirm") + if confirm == "1" { + DumpCache() + } +} + +// Publish a message to clients, and update the in-server cache for the message. +// notes: +// `scope` is implicit in the command +func HBackendUpdateAndPublish(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + formData, err := UnsealRequest(r.Form) + if err != nil { + w.WriteHeader(403) + fmt.Fprintf(w, "Error: %v", err) + return + } + + cmd := formData.Get("cmd") + json := formData.Get("args") + channel := formData.Get("channel") + cacheinfo, ok := ServerInitiatedCommands[cmd] if !ok { w.WriteHeader(422) + fmt.Fprintf(w, "Caching semantics unknown for command '%s'. Post to /addcachedcommand first.") + return } + + _ = cacheinfo + _ = json + _ = channel } diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index dd4c3f04..1842a0ee 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -1,4 +1,4 @@ -package server // import "bitbucket.org/stendec/frankerfacez/socketserver/server" +package server // import "bitbucket.org/stendec/frankerfacez/socketserver/internal/server" import ( "crypto/tls" @@ -262,13 +262,21 @@ func UnmarshalClientMessage(data []byte, payloadType byte, v interface{}) (err e dataStr = dataStr[spaceIdx+1:] argumentsJson := dataStr out.origArguments = argumentsJson - err = json.Unmarshal([]byte(argumentsJson), &out.Arguments) + err = out.parseOrigArguments() if err != nil { return } return nil } +func (cm *ClientMessage) parseOrigArguments() error { + err := json.Unmarshal([]byte(cm.origArguments), &cm.Arguments) + if err != nil { + return err + } + return nil +} + func MarshalClientMessage(clientMessage interface{}) (data []byte, payloadType byte, err error) { var msg ClientMessage var ok bool diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index 659a3038..65aaf96a 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -4,8 +4,6 @@ package server // If I screwed up the locking, I won't know until it's too late. import ( - "fmt" - "net/http" "sync" "time" ) @@ -58,6 +56,7 @@ func PublishToAll(msg ClientMessage) (count int) { count++ } GlobalSubscriptionInfo.RUnlock() + return } // Add a channel to the subscriptions while holding a read-lock to the map. diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index f25235a1..4d416ce4 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -1,31 +1,152 @@ package server import ( + "encoding/json" "fmt" "github.com/satori/go.uuid" "golang.org/x/net/websocket" "io/ioutil" "net/http" "net/http/httptest" + "net/url" "os" "sync" "syscall" "testing" ) -func CountOpenFDs() uint64 { +func TCountOpenFDs() uint64 { ary, _ := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", os.Getpid())) return uint64(len(ary)) } +const IgnoreReceivedArguments = 1+2i +func TReceiveExpectedMessage(tb testing.TB, conn *websocket.Conn, messageId int, command Command, arguments interface{}) (ClientMessage, bool) { + var msg ClientMessage + var fail bool + err := FFZCodec.Receive(conn, &msg) + if err != nil { + tb.Error(err) + return msg, false + } + if msg.MessageID != messageId { + tb.Error("Message ID was wrong. Expected", messageId, ", got", msg.MessageID, ":", msg) + fail = true + } + if msg.Command != command { + tb.Error("Command was wrong. Expected", command, ", got", msg.Command, ":", msg) + fail = true + } + if arguments != IgnoreReceivedArguments { + if msg.Arguments != arguments { + tb.Error("Arguments are wrong. Expected", arguments, ", got", msg.Arguments, ":", msg) + } + } + return msg, !fail +} + +func TSendMessage(tb testing.TB, conn *websocket.Conn, messageId int, command Command, arguments interface{}) bool { + err := FFZCodec.Send(conn, ClientMessage{MessageID: messageId, Command: command, Arguments: arguments}) + if err != nil { + tb.Error(err) + } + return err == nil +} + +func TestSubscriptionAndPublish(t *testing.T) { + var doneWg sync.WaitGroup + var readyWg sync.WaitGroup + + const TestChannelName = "testchannel" + const TestCommand = "testdata" + const TestData = "123456789" + + GenerateKeys("/tmp/test_naclkeys.json", "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") + DumpCache() + conf := &Config{ + UseSSL: false, + NaclKeysFile: "/tmp/test_naclkeys.json", + SocketOrigin: "localhost:2002", + } + serveMux := http.NewServeMux() + SetupServerAndHandle(conf, nil, serveMux) + + server := httptest.NewUnstartedServer(serveMux) + server.Start() + + wsUrl := fmt.Sprintf("ws://%s/", server.Listener.Addr().String()) + originUrl := fmt.Sprintf("http://%s", server.Listener.Addr().String()) + publishUrl := fmt.Sprintf("http://%s/pub_msg", server.Listener.Addr().String()) + + conn, err := websocket.Dial(wsUrl, "", originUrl) + if err != nil { + t.Error(err) + return + } + doneWg.Add(1) + readyWg.Add(1) + + go func(conn *websocket.Conn) { + TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()}) + TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments) + TSendMessage(t, conn, 2, "sub", TestChannelName) + TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil) + + readyWg.Done() + + TReceiveExpectedMessage(t, conn, -1, TestCommand, TestData) + + conn.Close() + doneWg.Done() + }(conn) + + readyWg.Wait() + + form := url.Values{} + form.Set("cmd", TestCommand) + argsBytes, _ := json.Marshal(TestData) + form.Set("args", string(argsBytes)) + form.Set("channel", TestChannelName) + form.Set("scope", MsgTargetTypeChat.Name()) + + sealedForm, err := SealRequest(form) + if err != nil { + t.Error(err) + server.CloseClientConnections() + panic("halting test") + } + + resp, err := http.PostForm(publishUrl, sealedForm) + if err != nil { + t.Error(err) + server.CloseClientConnections() + panic("halting test") + } + + respBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + respStr := string(respBytes) + + if resp.StatusCode != 200 { + t.Error("Publish failed: ", resp.StatusCode, respStr) + server.CloseClientConnections() + panic("halting test") + } + + doneWg.Wait() + server.Close() +} + func BenchmarkThousandUserSubscription(b *testing.B) { var doneWg sync.WaitGroup var readyWg sync.WaitGroup const TestChannelName = "testchannel" const TestCommand = "testdata" + const TestData = "123456789" GenerateKeys("/tmp/test_naclkeys.json", "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") + DumpCache() conf := &Config{ UseSSL: false, NaclKeysFile: "/tmp/test_naclkeys.json", @@ -40,7 +161,7 @@ func BenchmarkThousandUserSubscription(b *testing.B) { wsUrl := fmt.Sprintf("ws://%s/", server.Listener.Addr().String()) originUrl := fmt.Sprintf("http://%s", server.Listener.Addr().String()) - message := ClientMessage{MessageID: -1, Command: "testdata", Arguments: "123456789"} + message := ClientMessage{MessageID: -1, Command: "testdata", Arguments: TestData} fmt.Println() fmt.Println(b.N) @@ -48,7 +169,7 @@ func BenchmarkThousandUserSubscription(b *testing.B) { var limit syscall.Rlimit syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit) - limit.Cur = CountOpenFDs() + uint64(b.N)*2 + 100 + limit.Cur = TCountOpenFDs() + uint64(b.N)*2 + 100 if limit.Cur > limit.Max { b.Skip("Open file limit too low") @@ -67,60 +188,17 @@ func BenchmarkThousandUserSubscription(b *testing.B) { doneWg.Add(1) readyWg.Add(1) go func(i int, conn *websocket.Conn) { - var err error - var msg ClientMessage - err = FFZCodec.Send(conn, ClientMessage{MessageID: 1, Command: HelloCommand, Arguments: []interface{}{"ffz_test", uuid.NewV4().String()}}) - if err != nil { - b.Error(err) - } - err = FFZCodec.Send(conn, ClientMessage{MessageID: 2, Command: "sub", Arguments: TestChannelName}) - if err != nil { - b.Error(err) - } - err = FFZCodec.Receive(conn, &msg) - if err != nil { - b.Error(err) - } - if msg.MessageID != 1 { - b.Error("Got out-of-order message ID", msg) - } - if msg.Command != SuccessCommand { - b.Error("Command was not a success", msg) - } - err = FFZCodec.Receive(conn, &msg) - if err != nil { - b.Error(err) - } - if msg.MessageID != 2 { - b.Error("Got out-of-order message ID", msg) - } - if msg.Command != SuccessCommand { - b.Error("Command was not a success", msg) - } + TSendMessage(b, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()}) + TSendMessage(b, conn, 2, "sub", TestChannelName) + + TReceiveExpectedMessage(b, conn, 1, SuccessCommand, IgnoreReceivedArguments) + TReceiveExpectedMessage(b, conn, 2, SuccessCommand, nil) fmt.Println(i, " ready") readyWg.Done() - err = FFZCodec.Receive(conn, &msg) - if err != nil { - b.Error(err) - } - if msg.MessageID != -1 { - fmt.Println(msg) - b.Error("Client did not get expected messageID of -1") - } - if msg.Command != TestCommand { - fmt.Println(msg) - b.Error("Client did not get expected command") - } - str, err := msg.ArgumentsAsString() - if err != nil { - b.Error(err) - } - if str != "123456789" { - fmt.Println(msg) - b.Error("Client did not get expected data") - } + TReceiveExpectedMessage(b, conn, -1, TestCommand, TestData) + conn.Close() doneWg.Done() }(i, conn) @@ -131,7 +209,8 @@ func BenchmarkThousandUserSubscription(b *testing.B) { fmt.Println("publishing...") if PublishToChat(TestChannelName, message) != b.N { b.Error("not enough sent") - b.FailNow() + server.CloseClientConnections() + panic("halting test instead of waiting") } doneWg.Wait() diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 5ce92c81..32cc940d 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -2,8 +2,6 @@ package server import ( "encoding/json" - "errors" - "fmt" "github.com/satori/go.uuid" "sync" "time" From 812c6f25574499037691ab43cc22bd6bfb382de0 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 11:25:17 -0700 Subject: [PATCH 21/29] More test infrastructure --- .../internal/server/publisher_test.go | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index 4d416ce4..25641cf2 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -53,6 +53,21 @@ func TSendMessage(tb testing.TB, conn *websocket.Conn, messageId int, command Co return err == nil } +type TURLs struct { + Websocket string + Origin string + PubMsg string +} + +func TGetUrls(testserver httptest.Server) TURLs { + addr := testserver.Listener.Addr().String() + return TURLs{ + Websocket: fmt.Sprintf("ws://%s/", addr), + Origin: fmt.Sprintf("http://%s"), + PubMsg: fmt.Sprintf("http://%s/pub_msg"), + } +} + func TestSubscriptionAndPublish(t *testing.T) { var doneWg sync.WaitGroup var readyWg sync.WaitGroup @@ -74,11 +89,9 @@ func TestSubscriptionAndPublish(t *testing.T) { server := httptest.NewUnstartedServer(serveMux) server.Start() - wsUrl := fmt.Sprintf("ws://%s/", server.Listener.Addr().String()) - originUrl := fmt.Sprintf("http://%s", server.Listener.Addr().String()) - publishUrl := fmt.Sprintf("http://%s/pub_msg", server.Listener.Addr().String()) + urls := TGetUrls(server) - conn, err := websocket.Dial(wsUrl, "", originUrl) + conn, err := websocket.Dial(urls.Websocket, "", urls.Origin) if err != nil { t.Error(err) return @@ -116,7 +129,7 @@ func TestSubscriptionAndPublish(t *testing.T) { panic("halting test") } - resp, err := http.PostForm(publishUrl, sealedForm) + resp, err := http.PostForm(urls.PubMsg, sealedForm) if err != nil { t.Error(err) server.CloseClientConnections() @@ -158,8 +171,7 @@ func BenchmarkThousandUserSubscription(b *testing.B) { server := httptest.NewUnstartedServer(serveMux) server.Start() - wsUrl := fmt.Sprintf("ws://%s/", server.Listener.Addr().String()) - originUrl := fmt.Sprintf("http://%s", server.Listener.Addr().String()) + urls := TGetUrls(server) message := ClientMessage{MessageID: -1, Command: "testdata", Arguments: TestData} @@ -180,7 +192,7 @@ func BenchmarkThousandUserSubscription(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - conn, err := websocket.Dial(wsUrl, "", originUrl) + conn, err := websocket.Dial(urls.Websocket, "", urls.Origin) if err != nil { b.Error(err) break From 69676bf28739809b5a707edcb09a0dbbba5105ad Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 11:56:03 -0700 Subject: [PATCH 22/29] backlog data types, test stuff --- socketserver/internal/server/backlog.go | 20 ++++- .../internal/server/publisher_test.go | 75 ++++++++++--------- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index d16b3cf5..057ff9b2 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -39,7 +39,6 @@ var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ "chatters": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching "viewers": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching } -var _ = ServerInitiatedCommands type BacklogCacheType int @@ -50,6 +49,7 @@ const ( CacheTypeNever // Save the last 24 hours of this message. // If a client indicates that it has reconnected, replay the messages sent after the disconnect. + // Do not replay if the client indicates that this is a firstload. CacheTypeTimestamps // Save only the last copy of this message, and always send it when the backlog is requested. CacheTypeLastOnly @@ -83,13 +83,29 @@ var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype") // Returned by MessageTargetType.UnmarshalJSON() var ErrorUnrecognizedTargetType = errors.New("Invalid value for message target") -type PersistentCachedData struct { +type PersistentCachedMessage struct { Timestamp time.Time Channel string Watching bool Data string } +type TimestampedGlobalMessage struct { + Timestamp time.Time + Data string +} + +type TimestampedMultichatMessage struct { + Timestamp time.Time + Channels string + Data string +} + +type LastSavedMessage struct { + Timestamp time.Time + Data string +} + // map command -> channel -> data var CachedDataLast map[Command]map[string]string diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index 25641cf2..2f84f4ac 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -59,12 +59,38 @@ type TURLs struct { PubMsg string } -func TGetUrls(testserver httptest.Server) TURLs { +func TGetUrls(testserver *httptest.Server) TURLs { addr := testserver.Listener.Addr().String() return TURLs{ Websocket: fmt.Sprintf("ws://%s/", addr), - Origin: fmt.Sprintf("http://%s"), - PubMsg: fmt.Sprintf("http://%s/pub_msg"), + Origin: fmt.Sprintf("http://%s", addr), + PubMsg: fmt.Sprintf("http://%s/pub_msg", addr), + } +} + +const TNaclKeysLocation = "/tmp/test_naclkeys.json" + +func TSetup(testserver **httptest.Server, urls *TURLs) { + if backendSharedKey[0] == 0 { + GenerateKeys(TNaclKeysLocation, "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") + } + DumpCache() + + if testserver != nil { + conf := &Config{ + UseSSL: false, + NaclKeysFile: TNaclKeysLocation, + SocketOrigin: "localhost:2002", + } + serveMux := http.NewServeMux() + SetupServerAndHandle(conf, nil, serveMux) + + tserv := httptest.NewUnstartedServer(serveMux) + *testserver = tserv + tserv.Start() + if urls != nil { + *urls = TGetUrls(tserv) + } } } @@ -76,20 +102,10 @@ func TestSubscriptionAndPublish(t *testing.T) { const TestCommand = "testdata" const TestData = "123456789" - GenerateKeys("/tmp/test_naclkeys.json", "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") - DumpCache() - conf := &Config{ - UseSSL: false, - NaclKeysFile: "/tmp/test_naclkeys.json", - SocketOrigin: "localhost:2002", - } - serveMux := http.NewServeMux() - SetupServerAndHandle(conf, nil, serveMux) - - server := httptest.NewUnstartedServer(serveMux) - server.Start() - - urls := TGetUrls(server) + var server *httptest.Server + var urls TURLs + TSetup(&server, &urls) + defer unsubscribeAllClients() conn, err := websocket.Dial(urls.Websocket, "", urls.Origin) if err != nil { @@ -150,7 +166,7 @@ func TestSubscriptionAndPublish(t *testing.T) { server.Close() } -func BenchmarkThousandUserSubscription(b *testing.B) { +func BenchmarkUserSubscriptionSinglePublish(b *testing.B) { var doneWg sync.WaitGroup var readyWg sync.WaitGroup @@ -158,21 +174,6 @@ func BenchmarkThousandUserSubscription(b *testing.B) { const TestCommand = "testdata" const TestData = "123456789" - GenerateKeys("/tmp/test_naclkeys.json", "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") - DumpCache() - conf := &Config{ - UseSSL: false, - NaclKeysFile: "/tmp/test_naclkeys.json", - SocketOrigin: "localhost:2002", - } - serveMux := http.NewServeMux() - SetupServerAndHandle(conf, nil, serveMux) - - server := httptest.NewUnstartedServer(serveMux) - server.Start() - - urls := TGetUrls(server) - message := ClientMessage{MessageID: -1, Command: "testdata", Arguments: TestData} fmt.Println() @@ -190,6 +191,11 @@ func BenchmarkThousandUserSubscription(b *testing.B) { syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit) + var server *httptest.Server + var urls TURLs + TSetup(&server, &urls) + defer unsubscribeAllClients() + b.ResetTimer() for i := 0; i < b.N; i++ { conn, err := websocket.Dial(urls.Websocket, "", urls.Origin) @@ -206,7 +212,6 @@ func BenchmarkThousandUserSubscription(b *testing.B) { TReceiveExpectedMessage(b, conn, 1, SuccessCommand, IgnoreReceivedArguments) TReceiveExpectedMessage(b, conn, 2, SuccessCommand, nil) - fmt.Println(i, " ready") readyWg.Done() TReceiveExpectedMessage(b, conn, -1, TestCommand, TestData) @@ -225,9 +230,9 @@ func BenchmarkThousandUserSubscription(b *testing.B) { panic("halting test instead of waiting") } doneWg.Wait() + fmt.Println("...done.") b.StopTimer() server.Close() - unsubscribeAllClients() server.CloseClientConnections() } From 85d261afb3e319af1464d04fb2eb8858fdae07d2 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 12:13:28 -0700 Subject: [PATCH 23/29] client: Move room/channel distinction into name (room.trihex) Also, have the client send a "ready" message when it has sent all its initial requests. --- socketserver/internal/server/backend.go | 9 +-- socketserver/internal/server/backlog.go | 12 ++- socketserver/internal/server/commands.go | 57 ++----------- socketserver/internal/server/handlecore.go | 2 - socketserver/internal/server/publisher.go | 81 +++---------------- .../internal/server/publisher_test.go | 4 +- socketserver/internal/server/types.go | 14 +--- src/ember/channel.js | 8 +- src/ember/room.js | 4 +- src/socket.js | 33 ++++++-- 10 files changed, 62 insertions(+), 162 deletions(-) diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index c1073370..80eb3f64 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -87,7 +87,7 @@ func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Error: cmd cannot be blank") return } - if channel == "" && (target == MsgTargetTypeChat || target == MsgTargetTypeMultichat || target == MsgTargetTypeWatching) { + if channel == "" && (target == MsgTargetTypeChat || target == MsgTargetTypeMultichat) { w.WriteHeader(422) fmt.Fprintf(w, "Error: channel must be specified") return @@ -104,8 +104,6 @@ func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) { count = PublishToChat(channel, cm) case MsgTargetTypeMultichat: // TODO - case MsgTargetTypeWatching: - count = PublishToWatchers(channel, cm) case MsgTargetTypeGlobal: count = PublishToAll(cm) case MsgTargetTypeInvalid: @@ -169,10 +167,9 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s return } -func FetchBacklogData(chatSubs, channelSubs []string) ([]ClientMessage, error) { +func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) { formData := url.Values{ - "chatSubs": chatSubs, - "channelSubs": channelSubs, + "subs": chatSubs, } sealedForm, err := SealRequest(formData) diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index 057ff9b2..261bf2a6 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -22,7 +22,7 @@ var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ /// Emote updates "reload_badges": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global "set_badge": {CacheTypeTimestamps, MsgTargetTypeMultichat}, // timecache:multichat - "reload_set": {CacheTypeTimestamps, MsgTargetTypeMultichat}, // timecache:multichat + "reload_set": {}, // timecache:multichat "load_set": {}, // TODO what are the semantics of this? /// User auth @@ -32,12 +32,12 @@ var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ // follow_sets: extra emote sets included in the chat // follow_buttons: extra follow buttons below the stream "follow_sets": {CacheTypePersistent, MsgTargetTypeChat}, // mustcache:chat - "follow_buttons": {CacheTypePersistent, MsgTargetTypeWatching}, // mustcache:watching - "srl_race": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching + "follow_buttons": {CacheTypePersistent, MsgTargetTypeChat}, // mustcache:watching + "srl_race": {CacheTypeLastOnly, MsgTargetTypeChat}, // cachelast:watching /// Chatter/viewer counts - "chatters": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching - "viewers": {CacheTypeLastOnly, MsgTargetTypeWatching}, // cachelast:watching + "chatters": {CacheTypeLastOnly, MsgTargetTypeChat}, // cachelast:watching + "viewers": {CacheTypeLastOnly, MsgTargetTypeChat}, // cachelast:watching } type BacklogCacheType int @@ -69,8 +69,6 @@ const ( MsgTargetTypeChat // This message is targeted to all users in multiple chats MsgTargetTypeMultichat - // This message is targeted to all users watching a stream - MsgTargetTypeWatching // This message is sent to all FFZ users. MsgTargetTypeGlobal ) diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index abbf01ff..5fa82577 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -85,7 +85,7 @@ func HandleSub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rms client.Mutex.Lock() AddToSliceS(&client.CurrentChannels, channel) - client.PendingChatBacklogs = append(client.PendingChatBacklogs, channel) + client.PendingSubscriptionsBacklog = append(client.PendingSubscriptionsBacklog, channel) if client.MakePendingRequests == nil { client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) @@ -118,49 +118,6 @@ func HandleUnsub(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (r return ResponseSuccess, nil } -func HandleSubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - if err != nil { - return - } - - client.Mutex.Lock() - - AddToSliceS(&client.WatchingChannels, channel) - client.PendingStreamBacklogs = append(client.PendingStreamBacklogs, channel) - - if client.MakePendingRequests == nil { - client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) - } else { - if !client.MakePendingRequests.Reset(ChannelInfoDelay) { - client.MakePendingRequests = time.AfterFunc(ChannelInfoDelay, GetSubscriptionBacklogFor(conn, client)) - } - } - - client.Mutex.Unlock() - - SubscribeWatching(client, channel) - - return ResponseSuccess, nil -} - -func HandleUnsubChannel(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { - channel, err := msg.ArgumentsAsString() - - if err != nil { - return - } - - client.Mutex.Lock() - RemoveFromSliceS(&client.WatchingChannels, channel) - client.Mutex.Unlock() - - UnsubscribeSingleChannel(client, channel) - - return ResponseSuccess, nil -} - func GetSubscriptionBacklogFor(conn *websocket.Conn, client *ClientInfo) func() { return func() { GetSubscriptionBacklog(conn, client) @@ -169,25 +126,23 @@ func GetSubscriptionBacklogFor(conn *websocket.Conn, client *ClientInfo) func() // On goroutine func GetSubscriptionBacklog(conn *websocket.Conn, client *ClientInfo) { - var chatSubs, channelSubs []string + var subs []string // Lock, grab the data, and reset it client.Mutex.Lock() - chatSubs = client.PendingChatBacklogs - channelSubs = client.PendingStreamBacklogs - client.PendingChatBacklogs = nil - client.PendingStreamBacklogs = nil + subs = client.PendingSubscriptionsBacklog + client.PendingSubscriptionsBacklog = nil client.MakePendingRequests = nil client.Mutex.Unlock() - if len(chatSubs) == 0 && len(channelSubs) == 0 { + if len(subs) == 0 { return } if backendUrl == "" { return // for testing runs } - messages, err := FetchBacklogData(chatSubs, channelSubs) + messages, err := FetchBacklogData(subs) if err != nil { // Oh well. diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 1842a0ee..3e1af8b3 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -42,8 +42,6 @@ var CommandHandlers = map[Command]CommandHandler{ "sub": HandleSub, "unsub": HandleUnsub, - "sub_channel": HandleSubChannel, - "unsub_channel": HandleUnsubChannel, "track_follow": HandleTrackFollow, "emoticon_uses": HandleEmoticonUses, diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index 65aaf96a..afb2d944 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -15,8 +15,6 @@ type SubscriberList struct { var ChatSubscriptionInfo map[string]*SubscriberList = make(map[string]*SubscriberList) var ChatSubscriptionLock sync.RWMutex -var WatchingSubscriptionInfo map[string]*SubscriberList = make(map[string]*SubscriberList) -var WatchingSubscriptionLock sync.RWMutex var GlobalSubscriptionInfo SubscriberList func PublishToChat(channel string, msg ClientMessage) (count int) { @@ -34,21 +32,6 @@ func PublishToChat(channel string, msg ClientMessage) (count int) { return } -func PublishToWatchers(channel string, msg ClientMessage) (count int) { - WatchingSubscriptionLock.RLock() - list := WatchingSubscriptionInfo[channel] - if list != nil { - list.RLock() - for _, msgChan := range list.Members { - msgChan <- msg - count++ - } - list.RUnlock() - } - WatchingSubscriptionLock.RUnlock() - return -} - func PublishToAll(msg ClientMessage) (count int) { GlobalSubscriptionInfo.RLock() for _, msgChan := range GlobalSubscriptionInfo.Members { @@ -64,17 +47,17 @@ func PublishToAll(msg ClientMessage) (count int) { // - ALREADY HOLDING a read-lock to the 'which' top-level map via the rlocker object // - possible write lock to the 'which' top-level map via the wlocker object // - write lock to SubscriptionInfo (if not creating new) -func _subscribeWhileRlocked(which map[string]*SubscriberList, channelName string, value chan<- ClientMessage, rlocker sync.Locker, wlocker sync.Locker) { - list := which[channelName] +func _subscribeWhileRlocked(channelName string, value chan<- ClientMessage) { + list := ChatSubscriptionInfo[channelName] if list == nil { // Not found, so create it - rlocker.Unlock() - wlocker.Lock() + ChatSubscriptionLock.RUnlock() + ChatSubscriptionLock.Lock() list = &SubscriberList{} list.Members = []chan<- ClientMessage{value} // Create it populated, to avoid reaper - which[channelName] = list - wlocker.Unlock() - rlocker.Lock() + ChatSubscriptionInfo[channelName] = list + ChatSubscriptionLock.Unlock() + ChatSubscriptionLock.RLock() } else { list.Lock() AddToSliceC(&list.Members, value) @@ -90,16 +73,10 @@ func SubscribeGlobal(client *ClientInfo) { func SubscribeChat(client *ClientInfo, channelName string) { ChatSubscriptionLock.RLock() - _subscribeWhileRlocked(ChatSubscriptionInfo, channelName, client.MessageChannel, ChatSubscriptionLock.RLocker(), &ChatSubscriptionLock) + _subscribeWhileRlocked(channelName, client.MessageChannel) ChatSubscriptionLock.RUnlock() } -func SubscribeWatching(client *ClientInfo, channelName string) { - WatchingSubscriptionLock.RLock() - _subscribeWhileRlocked(WatchingSubscriptionInfo, channelName, client.MessageChannel, WatchingSubscriptionLock.RLocker(), &WatchingSubscriptionLock) - WatchingSubscriptionLock.RUnlock() -} - func unsubscribeAllClients() { GlobalSubscriptionInfo.Lock() GlobalSubscriptionInfo.Members = nil @@ -107,9 +84,6 @@ func unsubscribeAllClients() { ChatSubscriptionLock.Lock() ChatSubscriptionInfo = make(map[string]*SubscriberList) ChatSubscriptionLock.Unlock() - WatchingSubscriptionLock.Lock() - WatchingSubscriptionInfo = make(map[string]*SubscriberList) - WatchingSubscriptionLock.Unlock() } // Unsubscribe the client from all channels, AND clear the CurrentChannels / WatchingChannels fields. @@ -119,8 +93,8 @@ func unsubscribeAllClients() { // - write lock to ClientInfo func UnsubscribeAll(client *ClientInfo) { client.Mutex.Lock() - client.PendingChatBacklogs = nil - client.PendingStreamBacklogs = nil + client.PendingSubscriptionsBacklog = nil + client.PendingSubscriptionsBacklog = nil client.Mutex.Unlock() GlobalSubscriptionInfo.Lock() @@ -140,20 +114,6 @@ func UnsubscribeAll(client *ClientInfo) { client.CurrentChannels = nil client.Mutex.Unlock() ChatSubscriptionLock.RUnlock() - - WatchingSubscriptionLock.RLock() - client.Mutex.Lock() - for _, v := range client.WatchingChannels { - list := WatchingSubscriptionInfo[v] - if list != nil { - list.Lock() - RemoveFromSliceC(&list.Members, client.MessageChannel) - list.Unlock() - } - } - client.WatchingChannels = nil - client.Mutex.Unlock() - WatchingSubscriptionLock.RUnlock() } func UnsubscribeSingleChat(client *ClientInfo, channelName string) { @@ -165,23 +125,13 @@ func UnsubscribeSingleChat(client *ClientInfo, channelName string) { ChatSubscriptionLock.RUnlock() } -func UnsubscribeSingleChannel(client *ClientInfo, channelName string) { - WatchingSubscriptionLock.RLock() - list := WatchingSubscriptionInfo[channelName] - list.Lock() - RemoveFromSliceC(&list.Members, client.MessageChannel) - list.Unlock() - WatchingSubscriptionLock.RUnlock() -} - const ReapingDelay = 120 * time.Minute -// Checks each of ChatSubscriptionInfo / WatchingSubscriptionInfo -// for entries with no subscribers every ReapingDelay. +// Checks ChatSubscriptionInfo for entries with no subscribers every ReapingDelay. // Started from SetupServer(). func deadChannelReaper() { for { - time.Sleep(ReapingDelay / 2) + time.Sleep(ReapingDelay) ChatSubscriptionLock.Lock() for key, val := range ChatSubscriptionInfo { if len(val.Members) == 0 { @@ -189,12 +139,5 @@ func deadChannelReaper() { } } ChatSubscriptionLock.Unlock() - time.Sleep(ReapingDelay / 2) - WatchingSubscriptionLock.Lock() - for key, val := range WatchingSubscriptionInfo { - if len(val.Members) == 0 { - WatchingSubscriptionInfo[key] = nil - } - } } } diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index 2f84f4ac..ebc42ec6 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -98,7 +98,7 @@ func TestSubscriptionAndPublish(t *testing.T) { var doneWg sync.WaitGroup var readyWg sync.WaitGroup - const TestChannelName = "testchannel" + const TestChannelName = "room.testchannel" const TestCommand = "testdata" const TestData = "123456789" @@ -170,7 +170,7 @@ func BenchmarkUserSubscriptionSinglePublish(b *testing.B) { var doneWg sync.WaitGroup var readyWg sync.WaitGroup - const TestChannelName = "testchannel" + const TestChannelName = "room.testchannel" const TestCommand = "testdata" const TestData = "123456789" diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 32cc940d..f99e059d 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -62,19 +62,10 @@ type ClientInfo struct { // Protected by Mutex. CurrentChannels []string - // This list of channels this client needs UI updates for. - // Protected by Mutex. - WatchingChannels []string - // List of channels that we have not yet checked current chat-related channel info for. // This lets us batch the backlog requests. // Protected by Mutex. - PendingChatBacklogs []string - - // List of channels that we have not yet checked current stream-related channel info for. - // This lets us batch the backlog requests. - // Protected by Mutex. - PendingStreamBacklogs []string + PendingSubscriptionsBacklog []string // A timer that, when fired, will make the pending backlog requests. // Usually nil. Protected by Mutex. @@ -151,8 +142,6 @@ func (mtt MessageTargetType) Name() string { return "chat" case MsgTargetTypeMultichat: return "multichat" - case MsgTargetTypeWatching: - return "channel" case MsgTargetTypeGlobal: return "global" } @@ -163,7 +152,6 @@ var TargetTypesByName = map[string]MessageTargetType{ "single": MsgTargetTypeSingle, "chat": MsgTargetTypeChat, "multichat": MsgTargetTypeMultichat, - "channel": MsgTargetTypeWatching, "global": MsgTargetTypeGlobal, } diff --git a/src/ember/channel.js b/src/ember/channel.js index ea154da4..d10eee99 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -153,10 +153,10 @@ FFZ.prototype.setup_channel = function() { if ( id !== f.__old_host_target ) { if ( f.__old_host_target ) - f.ws_send("unsub_channel", f.__old_host_target); + f.ws_send("unsub", "channel." + f.__old_host_target); if ( id ) { - f.ws_send("sub_channel", id); + f.ws_send("sub", "channel." + id); f.__old_host_target = id; } else delete f.__old_host_target; @@ -208,7 +208,7 @@ FFZ.prototype._modify_cindex = function(view) { el = this.get('element'); f._cindex = this; - f.ws_send("sub_channel", id); + f.ws_send("sub", "channel." + id); el.setAttribute('data-channel', id); el.classList.add('ffz-channel'); @@ -621,7 +621,7 @@ FFZ.prototype._modify_cindex = function(view) { ffzTeardown: function() { var id = this.get('controller.id'); if ( id ) - f.ws_send("unsub_channel", id); + f.ws_send("unsub", "channel." + id); this.get('element').setAttribute('data-channel', ''); f._cindex = undefined; diff --git a/src/ember/room.js b/src/ember/room.js index a4b04135..a0ff9e8c 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -591,7 +591,7 @@ FFZ.prototype.add_room = function(id, room) { } // Let the server know where we are. - this.ws_send("sub", id); + this.ws_send("sub", "room." + id); // See if we need history? if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) { @@ -619,7 +619,7 @@ FFZ.prototype.remove_room = function(id) { utils.update_css(this._room_style, id, null); // Let the server know we're gone and delete our data for this room. - this.ws_send("unsub", id); + this.ws_send("unsub", "room." + id); delete this.rooms[id]; // Clean up sets we aren't using any longer. diff --git a/src/socket.js b/src/socket.js index 3e3d2092..08c1f59f 100644 --- a/src/socket.js +++ b/src/socket.js @@ -64,8 +64,8 @@ FFZ.prototype.ws_create = function() { if ( f.is_dashboard ) { var match = location.pathname.match(/\/([^\/]+)/); if ( match ) { - f.ws_send("sub", match[1]); - f.ws_send("sub_channel", match[1]); + f.ws_send("sub", "room." + match[1]); + f.ws_send("sub", "channel." + match[1]); } } @@ -74,7 +74,7 @@ FFZ.prototype.ws_create = function() { if ( ! f.rooms.hasOwnProperty(room_id) || ! f.rooms[room_id] ) continue; - f.ws_send("sub", room_id); + f.ws_send("sub", "room." + room_id); if ( f.rooms[room_id].needs_history ) { f.rooms[room_id].needs_history = false; @@ -89,10 +89,10 @@ FFZ.prototype.ws_create = function() { hosted_id = f._cindex.get('controller.hostModeTarget.id'); if ( channel_id ) - f.ws_send("sub_channel", channel_id); + f.ws_send("sub", "channel." + channel_id); if ( hosted_id ) - f.ws_send("sub_channel", hosted_id); + f.ws_send("sub", "channel." + hosted_id); } // Send any pending commands. @@ -103,11 +103,32 @@ FFZ.prototype.ws_create = function() { var d = pending[i]; f.ws_send(d[0], d[1], d[2]); } + + // If reconnecting, get the backlog that we missed. + if ( f._ws_offline_time ) { + var timestamp = f._ws_offline_time; + delete f._ws_offline_time; + f.ws_send("ready", timestamp); + } else { + f.ws_send("ready", 0); + } + } + + ws.onerror = function() { + if ( ! f._ws_offline_time ) { + f._ws_offline_time = new Date().getTime(); + } + + // Cycle selected server + f._ws_host_idx = (f._ws_host_idx + 1) % constants.WS_SERVERS.length; } ws.onclose = function(e) { f.log("Socket closed. (Code: " + e.code + ", Reason: " + e.reason + ")"); f._ws_open = false; + if ( ! f._ws_offline_time ) { + f._ws_offline_time = new Date().getTime(); + } // When the connection closes, run our callbacks. for (var i=0; i < FFZ.ws_on_close.length; i++) { @@ -118,7 +139,7 @@ FFZ.prototype.ws_create = function() { } } - // Attempt to cycle to backup server + // Cycle selected server f._ws_host_idx = (f._ws_host_idx + 1) % constants.WS_SERVERS.length; if ( f._ws_delay > 10000 ) { From 44bcd7df05ce3150fff90f0cc1ad5f0a976fd888 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 12:26:47 -0700 Subject: [PATCH 24/29] client: Start at random point in round-robin --- src/socket.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/socket.js b/src/socket.js index 08c1f59f..5a2c5e79 100644 --- a/src/socket.js +++ b/src/socket.js @@ -4,7 +4,10 @@ var FFZ = window.FrankerFaceZ, FFZ.prototype._ws_open = false; FFZ.prototype._ws_delay = 0; FFZ.prototype._ws_last_iframe = 0; -FFZ.prototype._ws_host_idx = 0; +FFZ.prototype._ws_host_idx = Math.floor(Math.random() * constants.WS_SERVERS.length) + 1; +if (constants.DEBUG) { + FFZ.prototype._ws_host_idx = 0; +} FFZ.ws_commands = {}; FFZ.ws_on_close = []; From 8ba87e1a279336753b8eed60a2d91635eaa41163 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 14:55:20 -0700 Subject: [PATCH 25/29] Implement backlog and test it --- socketserver/internal/server/backend.go | 2 +- socketserver/internal/server/backlog.go | 251 ++++++++++++++-- socketserver/internal/server/backlog_test.go | 76 +++++ socketserver/internal/server/commands.go | 60 +++- socketserver/internal/server/handlecore.go | 9 +- socketserver/internal/server/publisher.go | 25 ++ .../internal/server/publisher_test.go | 281 +++++++++++++++--- socketserver/internal/server/types.go | 28 ++ 8 files changed, 649 insertions(+), 83 deletions(-) create mode 100644 socketserver/internal/server/backlog_test.go diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 80eb3f64..9f316929 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -169,7 +169,7 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) { formData := url.Values{ - "subs": chatSubs, + "subs": chatSubs, } sealedForm, err := SealRequest(formData) diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index 261bf2a6..2f38f835 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -4,6 +4,10 @@ import ( "errors" "fmt" "net/http" + "sort" + "strconv" + "strings" + "sync" "time" ) @@ -13,7 +17,7 @@ type PushCommandCacheInfo struct { } // this value is just docs right now -var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ +var ServerInitiatedCommands = map[Command]PushCommandCacheInfo{ /// Global updates & notices "update_news": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global "message": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global @@ -22,7 +26,7 @@ var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ /// Emote updates "reload_badges": {CacheTypeTimestamps, MsgTargetTypeGlobal}, // timecache:global "set_badge": {CacheTypeTimestamps, MsgTargetTypeMultichat}, // timecache:multichat - "reload_set": {}, // timecache:multichat + "reload_set": {}, // timecache:multichat "load_set": {}, // TODO what are the semantics of this? /// User auth @@ -31,7 +35,7 @@ var ServerInitiatedCommands = map[string]PushCommandCacheInfo{ /// Channel data // follow_sets: extra emote sets included in the chat // follow_buttons: extra follow buttons below the stream - "follow_sets": {CacheTypePersistent, MsgTargetTypeChat}, // mustcache:chat + "follow_sets": {CacheTypePersistent, MsgTargetTypeChat}, // mustcache:chat "follow_buttons": {CacheTypePersistent, MsgTargetTypeChat}, // mustcache:watching "srl_race": {CacheTypeLastOnly, MsgTargetTypeChat}, // cachelast:watching @@ -81,34 +85,216 @@ var ErrorUnrecognizedCacheType = errors.New("Invalid value for cachetype") // Returned by MessageTargetType.UnmarshalJSON() var ErrorUnrecognizedTargetType = errors.New("Invalid value for message target") -type PersistentCachedMessage struct { - Timestamp time.Time - Channel string - Watching bool - Data string -} - type TimestampedGlobalMessage struct { Timestamp time.Time - Data string + Command Command + Data string } type TimestampedMultichatMessage struct { Timestamp time.Time - Channels string - Data string + Channels []string + Command Command + Data string } type LastSavedMessage struct { Timestamp time.Time - Data string + Data string } -// map command -> channel -> data -var CachedDataLast map[Command]map[string]string +// map is command -> channel -> data + +// CacheTypeLastOnly. Cleaned up by reaper goroutine every ~hour. +var CachedLastMessages map[Command]map[string]LastSavedMessage +var CachedLSMLock sync.RWMutex + +// CacheTypePersistent. Never cleaned. +var PersistentLastMessages map[Command]map[string]LastSavedMessage +var PersistentLSMLock sync.RWMutex + +var CachedGlobalMessages []TimestampedGlobalMessage +var CachedChannelMessages []TimestampedMultichatMessage +var CacheListsLock sync.RWMutex func DumpCache() { - CachedDataLast = make(map[Command]map[string]string) + CachedLSMLock.Lock() + CachedLastMessages = make(map[Command]map[string]LastSavedMessage) + CachedLSMLock.Unlock() + + PersistentLSMLock.Lock() + PersistentLastMessages = make(map[Command]map[string]LastSavedMessage) + // TODO delete file? + PersistentLSMLock.Unlock() + + CacheListsLock.Lock() + CachedGlobalMessages = make(tgmarray, 0) + CachedChannelMessages = make(tmmarray, 0) + CacheListsLock.Unlock() +} + +func SendBacklogForNewClient(client *ClientInfo) { + client.Mutex.Lock() // reading CurrentChannels + PersistentLSMLock.RLock() + for _, cmd := range GetCommandsOfType(PushCommandCacheInfo{CacheTypePersistent, MsgTargetTypeChat}) { + chanMap := CachedLastMessages[cmd] + if chanMap == nil { + continue + } + for _, channel := range client.CurrentChannels { + msg, ok := chanMap[channel] + if ok { + msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: msg.Data} + msg.parseOrigArguments() + client.MessageChannel <- msg + } + } + } + PersistentLSMLock.RUnlock() + + CachedLSMLock.RLock() + for _, cmd := range GetCommandsOfType(PushCommandCacheInfo{CacheTypeLastOnly, MsgTargetTypeChat}) { + chanMap := CachedLastMessages[cmd] + if chanMap == nil { + continue + } + for _, channel := range client.CurrentChannels { + msg, ok := chanMap[channel] + if ok { + msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: msg.Data} + msg.parseOrigArguments() + client.MessageChannel <- msg + } + } + } + CachedLSMLock.RUnlock() + client.Mutex.Unlock() +} + +func SendTimedBacklogMessages(client *ClientInfo, disconnectTime time.Time) { + client.Mutex.Lock() // reading CurrentChannels + CacheListsLock.RLock() + + globIdx := FindFirstNewMessage(tgmarray(CachedGlobalMessages), disconnectTime) + + for i := globIdx; i < len(CachedGlobalMessages); i++ { + item := CachedGlobalMessages[i] + msg := ClientMessage{MessageID: -1, Command: item.Command, origArguments: item.Data} + msg.parseOrigArguments() + client.MessageChannel <- msg + } + + chanIdx := FindFirstNewMessage(tmmarray(CachedChannelMessages), disconnectTime) + + for i := chanIdx; i < len(CachedChannelMessages); i++ { + item := CachedChannelMessages[i] + var send bool + for _, channel := range item.Channels { + for _, matchChannel := range client.CurrentChannels { + if channel == matchChannel { + send = true + break + } + } + if send { + break + } + } + if send { + msg := ClientMessage{MessageID: -1, Command: item.Command, origArguments: item.Data} + msg.parseOrigArguments() + client.MessageChannel <- msg + } + } + + CacheListsLock.RUnlock() + client.Mutex.Unlock() +} + +func InsertionSort(ary sort.Interface) { + for i := 1; i < ary.Len(); i++ { + for j := i; j > 0 && ary.Less(j, j-1); j-- { + ary.Swap(j, j-1) + } + } +} + +type TimestampArray interface { + Len() int + GetTime(int) time.Time +} + +func FindFirstNewMessage(ary TimestampArray, disconnectTime time.Time) (idx int) { + // TODO needs tests + len := ary.Len() + i := len + + // Walk backwards until we find GetTime() before disconnectTime + step := 1 + for i > 0 { + i -= step + if i < 0 { + i = 0 + } + if !ary.GetTime(i).After(disconnectTime) { + break + } + step = int(float64(step)*1.5) + 1 + } + + // Walk forwards until we find GetTime() after disconnectTime + for i < len && !ary.GetTime(i).After(disconnectTime) { + i++ + } + + if i == len { + return -1 + } + return i +} + +func SaveLastMessage(which map[Command]map[string]LastSavedMessage, locker sync.Locker, cmd Command, channel string, timestamp time.Time, data string, deleting bool) { + locker.Lock() + defer locker.Unlock() + + chanMap, ok := CachedLastMessages[cmd] + if !ok { + if deleting { + return + } + chanMap = make(map[string]LastSavedMessage) + CachedLastMessages[cmd] = chanMap + } + + if deleting { + delete(chanMap, channel) + } else { + chanMap[channel] = LastSavedMessage{timestamp, data} + } +} + +func SaveGlobalMessage(cmd Command, timestamp time.Time, data string) { + CacheListsLock.Lock() + CachedGlobalMessages = append(CachedGlobalMessages, TimestampedGlobalMessage{timestamp, cmd, data}) + InsertionSort(tgmarray(CachedGlobalMessages)) + CacheListsLock.Unlock() +} + +func SaveMultichanMessage(cmd Command, channels string, timestamp time.Time, data string) { + CacheListsLock.Lock() + CachedChannelMessages = append(CachedChannelMessages, TimestampedMultichatMessage{timestamp, strings.Split(channels, ","), cmd, data}) + InsertionSort(tmmarray(CachedChannelMessages)) + CacheListsLock.Unlock() +} + +func GetCommandsOfType(match PushCommandCacheInfo) []Command { + var ret []Command + for cmd, info := range ServerInitiatedCommands { + if info == match { + ret = append(ret, cmd) + } + } + return ret } func HBackendDumpCache(w http.ResponseWriter, r *http.Request) { @@ -138,9 +324,16 @@ func HBackendUpdateAndPublish(w http.ResponseWriter, r *http.Request) { return } - cmd := formData.Get("cmd") + cmd := Command(formData.Get("cmd")) json := formData.Get("args") channel := formData.Get("channel") + deleteMode := formData.Get("delete") != "" + timeStr := formData.Get("time") + timestamp, err := time.Parse(time.UnixDate, timeStr) + if err != nil { + w.WriteHeader(422) + fmt.Fprintf(w, "error parsing time: %v", err) + } cacheinfo, ok := ServerInitiatedCommands[cmd] if !ok { @@ -149,7 +342,23 @@ func HBackendUpdateAndPublish(w http.ResponseWriter, r *http.Request) { return } - _ = cacheinfo - _ = json - _ = channel + var count int + msg := ClientMessage{MessageID: -1, Command: cmd, origArguments: json} + msg.parseOrigArguments() + + if cacheinfo.Caching == CacheTypeLastOnly && cacheinfo.Target == MsgTargetTypeChat { + SaveLastMessage(CachedLastMessages, &CachedLSMLock, cmd, channel, timestamp, json, deleteMode) + count = PublishToChat(channel, msg) + } else if cacheinfo.Caching == CacheTypePersistent && cacheinfo.Target == MsgTargetTypeChat { + SaveLastMessage(PersistentLastMessages, &PersistentLSMLock, cmd, channel, timestamp, json, deleteMode) + count = PublishToChat(channel, msg) + } else if cacheinfo.Caching == CacheTypeTimestamps && cacheinfo.Target == MsgTargetTypeMultichat { + SaveMultichanMessage(cmd, channel, timestamp, json) + count = PublishToMultiple(strings.Split(channel, ","), msg) + } else if cacheinfo.Caching == CacheTypeTimestamps && cacheinfo.Target == MsgTargetTypeGlobal { + SaveGlobalMessage(cmd, timestamp, json) + count = PublishToAll(msg) + } + + w.Write([]byte(strconv.Itoa(count))) } diff --git a/socketserver/internal/server/backlog_test.go b/socketserver/internal/server/backlog_test.go new file mode 100644 index 00000000..68757587 --- /dev/null +++ b/socketserver/internal/server/backlog_test.go @@ -0,0 +1,76 @@ +package server + +import ( + "testing" + "time" +) + +func TestFindFirstNewMessageEmpty(t *testing.T) { + CachedGlobalMessages = []TimestampedGlobalMessage{} + i := FindFirstNewMessage(tgmarray(CachedGlobalMessages), time.Unix(10, 0)) + if i != -1 { + t.Errorf("Expected -1, got %d", i) + } +} +func TestFindFirstNewMessageOneBefore(t *testing.T) { + CachedGlobalMessages = []TimestampedGlobalMessage{ + {Timestamp: time.Unix(8, 0)}, + } + i := FindFirstNewMessage(tgmarray(CachedGlobalMessages), time.Unix(10, 0)) + if i != -1 { + t.Errorf("Expected -1, got %d", i) + } +} +func TestFindFirstNewMessageSeveralBefore(t *testing.T) { + CachedGlobalMessages = []TimestampedGlobalMessage{ + {Timestamp: time.Unix(1, 0)}, + {Timestamp: time.Unix(2, 0)}, + {Timestamp: time.Unix(3, 0)}, + {Timestamp: time.Unix(4, 0)}, + {Timestamp: time.Unix(5, 0)}, + } + i := FindFirstNewMessage(tgmarray(CachedGlobalMessages), time.Unix(10, 0)) + if i != -1 { + t.Errorf("Expected -1, got %d", i) + } +} +func TestFindFirstNewMessageInMiddle(t *testing.T) { + CachedGlobalMessages = []TimestampedGlobalMessage{ + {Timestamp: time.Unix(1, 0)}, + {Timestamp: time.Unix(2, 0)}, + {Timestamp: time.Unix(3, 0)}, + {Timestamp: time.Unix(4, 0)}, + {Timestamp: time.Unix(5, 0)}, + {Timestamp: time.Unix(11, 0)}, + {Timestamp: time.Unix(12, 0)}, + {Timestamp: time.Unix(13, 0)}, + {Timestamp: time.Unix(14, 0)}, + {Timestamp: time.Unix(15, 0)}, + } + i := FindFirstNewMessage(tgmarray(CachedGlobalMessages), time.Unix(10, 0)) + if i != 5 { + t.Errorf("Expected 5, got %d", i) + } +} +func TestFindFirstNewMessageOneAfter(t *testing.T) { + CachedGlobalMessages = []TimestampedGlobalMessage{ + {Timestamp: time.Unix(15, 0)}, + } + i := FindFirstNewMessage(tgmarray(CachedGlobalMessages), time.Unix(10, 0)) + if i != 0 { + t.Errorf("Expected 0, got %d", i) + } +} +func TestFindFirstNewMessageSeveralAfter(t *testing.T) { + CachedGlobalMessages = []TimestampedGlobalMessage{ + {Timestamp: time.Unix(11, 0)}, + {Timestamp: time.Unix(12, 0)}, + {Timestamp: time.Unix(13, 0)}, + {Timestamp: time.Unix(14, 0)}, + {Timestamp: time.Unix(15, 0)}, + } + i := FindFirstNewMessage(tgmarray(CachedGlobalMessages), time.Unix(10, 0)) + if i != 0 { + t.Errorf("Expected 0, got %d", i) + } +} diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index 5fa82577..c947bf08 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "github.com/satori/go.uuid" "golang.org/x/net/websocket" "log" @@ -17,22 +18,25 @@ const ChannelInfoDelay = 2 * time.Second func HandleCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) { handler, ok := CommandHandlers[msg.Command] if !ok { - log.Print("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) - // uncomment after commands are implemented - // closer() + log.Println("[!] Unknown command", msg.Command, "- sent by client", client.ClientID, "@", conn.RemoteAddr()) + FFZCodec.Send(conn, ClientMessage{ + MessageID: msg.MessageID, + Command: "error", + Arguments: fmt.Sprintf("Unknown command %s", msg.Command), + }) return } - // log.Println(conn.RemoteAddr(), msg.MessageID, msg.Command, msg.Arguments) - response, err := CallHandler(handler, conn, client, msg) if err == nil { - response.MessageID = msg.MessageID - FFZCodec.Send(conn, response) - } else if response.Command == AsyncResponseCommand { - // Don't send anything - // The response will be delivered over client.MessageChannel / serverMessageChan + if response.Command == AsyncResponseCommand { + // Don't send anything + // The response will be delivered over client.MessageChannel / serverMessageChan + } else { + response.MessageID = msg.MessageID + FFZCodec.Send(conn, response) + } } else { FFZCodec.Send(conn, ClientMessage{ MessageID: msg.MessageID, @@ -61,6 +65,42 @@ func HandleHello(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (r }, nil } +func HandleReady(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { + disconnectAt, err := msg.ArgumentsAsInt() + if err != nil { + return + } + + client.Mutex.Lock() + if client.MakePendingRequests != nil { + if !client.MakePendingRequests.Stop() { + // Timer already fired, GetSubscriptionBacklog() has started + rmsg.Command = SuccessCommand + return + } + } + client.PendingSubscriptionsBacklog = nil + client.MakePendingRequests = nil + client.Mutex.Unlock() + + if disconnectAt == 0 { + // backlog only + go func() { + client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand} + SendBacklogForNewClient(client) + }() + return ClientMessage{Command: AsyncResponseCommand}, nil + } else { + // backlog and timed + go func() { + client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand} + SendBacklogForNewClient(client) + SendTimedBacklogMessages(client, time.Unix(disconnectAt, 0)) + }() + return ClientMessage{Command: AsyncResponseCommand}, nil + } +} + func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { username, err := msg.ArgumentsAsString() if err != nil { diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 3e1af8b3..83d05ac4 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -39,9 +39,10 @@ type CommandHandler func(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMes var CommandHandlers = map[Command]CommandHandler{ HelloCommand: HandleHello, "setuser": HandleSetUser, + "ready": HandleReady, - "sub": HandleSub, - "unsub": HandleUnsub, + "sub": HandleSub, + "unsub": HandleUnsub, "track_follow": HandleTrackFollow, "emoticon_uses": HandleEmoticonUses, @@ -335,7 +336,7 @@ func (cm *ClientMessage) ArgumentsAsString() (string1 string, err error) { } // Convenience method: Parse the arguments of the ClientMessage as a single int. -func (cm *ClientMessage) ArgumentsAsInt() (int1 int, err error) { +func (cm *ClientMessage) ArgumentsAsInt() (int1 int64, err error) { var ok bool var num float64 num, ok = cm.Arguments.(float64) @@ -343,7 +344,7 @@ func (cm *ClientMessage) ArgumentsAsInt() (int1 int, err error) { err = ExpectedSingleInt return } else { - int1 = int(num) + int1 = int64(num) return int1, nil } } diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index afb2d944..d9658ac7 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -32,6 +32,31 @@ func PublishToChat(channel string, msg ClientMessage) (count int) { return } +func PublishToMultiple(channels []string, msg ClientMessage) (count int) { + found := make(map[chan<- ClientMessage]struct{}) + + ChatSubscriptionLock.RLock() + + for _, channel := range channels { + list := ChatSubscriptionInfo[channel] + if list != nil { + list.RLock() + for _, msgChan := range list.Members { + found[msgChan] = struct{}{} + } + list.RUnlock() + } + } + + ChatSubscriptionLock.RUnlock() + + for msgChan, _ := range found { + msgChan <- msg + count++ + } + return +} + func PublishToAll(msg ClientMessage) (count int) { GlobalSubscriptionInfo.RLock() for _, msgChan := range GlobalSubscriptionInfo.Members { diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index ebc42ec6..e878f709 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -10,9 +10,11 @@ import ( "net/http/httptest" "net/url" "os" + "strconv" "sync" "syscall" "testing" + "time" ) func TCountOpenFDs() uint64 { @@ -20,7 +22,8 @@ func TCountOpenFDs() uint64 { return uint64(len(ary)) } -const IgnoreReceivedArguments = 1+2i +const IgnoreReceivedArguments = 1 + 2i + func TReceiveExpectedMessage(tb testing.TB, conn *websocket.Conn, messageId int, command Command, arguments interface{}) (ClientMessage, bool) { var msg ClientMessage var fail bool @@ -38,8 +41,15 @@ func TReceiveExpectedMessage(tb testing.TB, conn *websocket.Conn, messageId int, fail = true } if arguments != IgnoreReceivedArguments { - if msg.Arguments != arguments { - tb.Error("Arguments are wrong. Expected", arguments, ", got", msg.Arguments, ":", msg) + if arguments == nil { + if msg.origArguments != "" { + tb.Error("Arguments are wrong. Expected", arguments, ", got", msg.Arguments, ":", msg) + } + } else { + argBytes, _ := json.Marshal(arguments) + if msg.origArguments != string(argBytes) { + tb.Error("Arguments are wrong. Expected", arguments, ", got", msg.Arguments, ":", msg) + } } } return msg, !fail @@ -53,18 +63,66 @@ func TSendMessage(tb testing.TB, conn *websocket.Conn, messageId int, command Co return err == nil } +func TSealForSavePubMsg(tb testing.TB, cmd Command, channel string, arguments interface{}, deleteMode bool) (url.Values, error) { + form := url.Values{} + form.Set("cmd", string(cmd)) + argsBytes, err := json.Marshal(arguments) + if err != nil { + tb.Error(err) + return nil, err + } + form.Set("args", string(argsBytes)) + form.Set("channel", channel) + if deleteMode { + form.Set("delete", "1") + } + form.Set("time", time.Now().Format(time.UnixDate)) + + sealed, err := SealRequest(form) + if err != nil { + tb.Error(err) + return nil, err + } + return sealed, nil +} + +func TCheckResponse(tb testing.TB, resp *http.Response, expected string) bool { + var failed bool + respBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + respStr := string(respBytes) + + if err != nil { + tb.Error(err) + failed = true + } + + if resp.StatusCode != 200 { + tb.Error("Publish failed: ", resp.StatusCode, respStr) + failed = true + } + + if respStr != expected { + tb.Errorf("Got wrong response from server. Expected: '%s' Got: '%s'", expected, respStr) + failed = true + } + return !failed +} + type TURLs struct { - Websocket string - Origin string - PubMsg string + Websocket string + Origin string + PubMsg string + SavePubMsg string // update_and_pub } func TGetUrls(testserver *httptest.Server) TURLs { addr := testserver.Listener.Addr().String() return TURLs{ - Websocket: fmt.Sprintf("ws://%s/", addr), - Origin: fmt.Sprintf("http://%s", addr), - PubMsg: fmt.Sprintf("http://%s/pub_msg", addr), + Websocket: fmt.Sprintf("ws://%s/", addr), + Origin: fmt.Sprintf("http://%s", addr), + PubMsg: fmt.Sprintf("http://%s/pub_msg", addr), + SavePubMsg: fmt.Sprintf("http://%s/update_and_pub", addr), } } @@ -98,32 +156,192 @@ func TestSubscriptionAndPublish(t *testing.T) { var doneWg sync.WaitGroup var readyWg sync.WaitGroup - const TestChannelName = "room.testchannel" - const TestCommand = "testdata" - const TestData = "123456789" + const TestChannelName1 = "room.testchannel" + const TestChannelName2 = "room.chan2" + const TestChannelName3 = "room.chan3" + const TestChannelNameUnused = "room.empty" + const TestCommandChan = "testdata_single" + const TestCommandMulti = "testdata_multi" + const TestCommandGlobal = "testdata_global" + const TestData1 = "123456789" + const TestData2 = 42 + const TestData3 = false + var TestData4 = []interface{}{"str1", "str2", "str3"} + + ServerInitiatedCommands[TestCommandChan] = PushCommandCacheInfo{CacheTypeLastOnly, MsgTargetTypeChat} + ServerInitiatedCommands[TestCommandMulti] = PushCommandCacheInfo{CacheTypeTimestamps, MsgTargetTypeMultichat} + ServerInitiatedCommands[TestCommandGlobal] = PushCommandCacheInfo{CacheTypeTimestamps, MsgTargetTypeGlobal} var server *httptest.Server var urls TURLs TSetup(&server, &urls) + defer server.CloseClientConnections() defer unsubscribeAllClients() - conn, err := websocket.Dial(urls.Websocket, "", urls.Origin) + var conn *websocket.Conn + var err error + + // client 1: sub ch1, ch2 + // client 2: sub ch1, ch3 + // client 3: sub none + // client 4: delayed sub ch1 + // msg 1: ch1 + // msg 2: ch2, ch3 + // msg 3: chEmpty + // msg 4: global + + // Client 1 + conn, err = websocket.Dial(urls.Websocket, "", urls.Origin) if err != nil { t.Error(err) return } + doneWg.Add(1) readyWg.Add(1) - go func(conn *websocket.Conn) { TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()}) TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments) - TSendMessage(t, conn, 2, "sub", TestChannelName) + TSendMessage(t, conn, 2, "sub", TestChannelName1) + TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil) + TSendMessage(t, conn, 3, "sub", TestChannelName2) // 2 + TReceiveExpectedMessage(t, conn, 3, SuccessCommand, nil) + TSendMessage(t, conn, 4, "ready", 0) + TReceiveExpectedMessage(t, conn, 4, SuccessCommand, nil) + + readyWg.Done() + + TReceiveExpectedMessage(t, conn, -1, TestCommandChan, TestData1) + TReceiveExpectedMessage(t, conn, -1, TestCommandMulti, TestData2) + TReceiveExpectedMessage(t, conn, -1, TestCommandGlobal, TestData4) + + conn.Close() + doneWg.Done() + }(conn) + + // Client 2 + conn, err = websocket.Dial(urls.Websocket, "", urls.Origin) + if err != nil { + t.Error(err) + return + } + + doneWg.Add(1) + readyWg.Add(1) + go func(conn *websocket.Conn) { + TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()}) + TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments) + TSendMessage(t, conn, 2, "sub", TestChannelName1) + TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil) + TSendMessage(t, conn, 3, "sub", TestChannelName3) // 3 + TReceiveExpectedMessage(t, conn, 3, SuccessCommand, nil) + TSendMessage(t, conn, 4, "ready", 0) + TReceiveExpectedMessage(t, conn, 4, SuccessCommand, nil) + + readyWg.Done() + + TReceiveExpectedMessage(t, conn, -1, TestCommandChan, TestData1) + TReceiveExpectedMessage(t, conn, -1, TestCommandMulti, TestData2) + TReceiveExpectedMessage(t, conn, -1, TestCommandGlobal, TestData4) + + conn.Close() + doneWg.Done() + }(conn) + + // Client 3 + conn, err = websocket.Dial(urls.Websocket, "", urls.Origin) + if err != nil { + t.Error(err) + return + } + + doneWg.Add(1) + readyWg.Add(1) + go func(conn *websocket.Conn) { + TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()}) + TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments) + TSendMessage(t, conn, 2, "ready", 0) TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil) readyWg.Done() - TReceiveExpectedMessage(t, conn, -1, TestCommand, TestData) + TReceiveExpectedMessage(t, conn, -1, TestCommandGlobal, TestData4) + + conn.Close() + doneWg.Done() + }(conn) + + // Wait for clients 1-3 + readyWg.Wait() + + var form url.Values + var resp *http.Response + + // Publish message 1 - should go to clients 1, 2 + + form, err = TSealForSavePubMsg(t, TestCommandChan, TestChannelName1, TestData1, false) + if err != nil { + t.FailNow() + } + resp, err = http.PostForm(urls.SavePubMsg, form) + if !TCheckResponse(t, resp, strconv.Itoa(2)) { + t.FailNow() + } + + // Publish message 2 - should go to clients 1, 2 + + form, err = TSealForSavePubMsg(t, TestCommandMulti, TestChannelName2+","+TestChannelName3, TestData2, false) + if err != nil { + t.FailNow() + } + resp, err = http.PostForm(urls.SavePubMsg, form) + if !TCheckResponse(t, resp, strconv.Itoa(2)) { + t.FailNow() + } + + // Publish message 3 - should go to no clients + + form, err = TSealForSavePubMsg(t, TestCommandChan, TestChannelNameUnused, TestData3, false) + if err != nil { + t.FailNow() + } + resp, err = http.PostForm(urls.SavePubMsg, form) + if !TCheckResponse(t, resp, strconv.Itoa(0)) { + t.FailNow() + } + + // Publish message 4 - should go to clients 1, 2, 3 + + form, err = TSealForSavePubMsg(t, TestCommandGlobal, "", TestData4, false) + if err != nil { + t.FailNow() + } + resp, err = http.PostForm(urls.SavePubMsg, form) + if !TCheckResponse(t, resp, strconv.Itoa(3)) { + t.FailNow() + } + + // Start client 4 + conn, err = websocket.Dial(urls.Websocket, "", urls.Origin) + if err != nil { + t.Error(err) + return + } + + doneWg.Add(1) + readyWg.Add(1) + go func(conn *websocket.Conn) { + TSendMessage(t, conn, 1, HelloCommand, []interface{}{"ffz_0.0-test", uuid.NewV4().String()}) + TReceiveExpectedMessage(t, conn, 1, SuccessCommand, IgnoreReceivedArguments) + TSendMessage(t, conn, 2, "sub", TestChannelName1) + TReceiveExpectedMessage(t, conn, 2, SuccessCommand, nil) + TSendMessage(t, conn, 3, "ready", 0) + TReceiveExpectedMessage(t, conn, 3, SuccessCommand, nil) + + // backlog message + TReceiveExpectedMessage(t, conn, -1, TestCommandChan, TestData1) + + readyWg.Done() conn.Close() doneWg.Done() @@ -131,37 +349,6 @@ func TestSubscriptionAndPublish(t *testing.T) { readyWg.Wait() - form := url.Values{} - form.Set("cmd", TestCommand) - argsBytes, _ := json.Marshal(TestData) - form.Set("args", string(argsBytes)) - form.Set("channel", TestChannelName) - form.Set("scope", MsgTargetTypeChat.Name()) - - sealedForm, err := SealRequest(form) - if err != nil { - t.Error(err) - server.CloseClientConnections() - panic("halting test") - } - - resp, err := http.PostForm(urls.PubMsg, sealedForm) - if err != nil { - t.Error(err) - server.CloseClientConnections() - panic("halting test") - } - - respBytes, err := ioutil.ReadAll(resp.Body) - resp.Body.Close() - respStr := string(respBytes) - - if resp.StatusCode != 200 { - t.Error("Publish failed: ", resp.StatusCode, respStr) - server.CloseClientConnections() - panic("halting test") - } - doneWg.Wait() server.Close() } diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index f99e059d..a2bd074d 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -76,6 +76,34 @@ type ClientInfo struct { MessageChannel chan<- ClientMessage } +type tgmarray []TimestampedGlobalMessage +type tmmarray []TimestampedMultichatMessage + +func (ta tgmarray) Len() int { + return len(ta) +} +func (ta tgmarray) Less(i, j int) bool { + return ta[i].Timestamp.Before(ta[j].Timestamp) +} +func (ta tgmarray) Swap(i, j int) { + ta[i], ta[j] = ta[j], ta[i] +} +func (ta tgmarray) GetTime(i int) time.Time { + return ta[i].Timestamp +} +func (ta tmmarray) Len() int { + return len(ta) +} +func (ta tmmarray) Less(i, j int) bool { + return ta[i].Timestamp.Before(ta[j].Timestamp) +} +func (ta tmmarray) Swap(i, j int) { + ta[i], ta[j] = ta[j], ta[i] +} +func (ta tmmarray) GetTime(i int) time.Time { + return ta[i].Timestamp +} + func (bct BacklogCacheType) Name() string { switch bct { case CacheTypeInvalid: From a6508d32abcf1ea2a22469b854197041e87342c1 Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 18:00:29 -0700 Subject: [PATCH 26/29] can't believe i didn't already have this smdh --- socketserver/internal/server/backlog.go | 2 +- socketserver/internal/server/handlecore.go | 27 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/socketserver/internal/server/backlog.go b/socketserver/internal/server/backlog.go index 2f38f835..66f09e93 100644 --- a/socketserver/internal/server/backlog.go +++ b/socketserver/internal/server/backlog.go @@ -297,7 +297,7 @@ func GetCommandsOfType(match PushCommandCacheInfo) []Command { return ret } -func HBackendDumpCache(w http.ResponseWriter, r *http.Request) { +func HBackendDumpBacklog(w http.ResponseWriter, r *http.Request) { r.ParseForm() formData, err := UnsealRequest(r.Form) if err != nil { diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 83d05ac4..1f477f97 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "sync" + "html/template" ) const MAX_PACKET_SIZE = 1024 @@ -125,11 +126,35 @@ func SetupServerAndHandle(config *Config, tlsConfig *tls.Config, serveMux *http. if serveMux == nil { serveMux = http.DefaultServeMux } - serveMux.HandleFunc("/", sockServer.ServeHTTP) + serveMux.HandleFunc("/", ServeWebsocketOrCatbag(sockServer.ServeHTTP)) serveMux.HandleFunc("/pub_msg", HBackendPublishRequest) + serveMux.HandleFunc("/dump_backlog", HBackendDumpBacklog) serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish) } +var Catbag = template.Must(template.New(` + + + +CatBag + + + + +`).Parse("html")) + +func ServeWebsocketOrCatbag(sockfunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Connection") == "Upgrade" { + sockfunc(w, r) + return + } else { + + Catbag.Execute(w, nil) + } + } +} + // Handle a new websocket connection from a FFZ client. // This runs in a goroutine started by net/http. func HandleSocketConnection(conn *websocket.Conn) { From 4eee83a56126c773bd502cbf7478c0c4ebe4fabf Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 26 Oct 2015 22:16:03 -0700 Subject: [PATCH 27/29] Combine config objects, improve first-run ux --- socketserver/cmd/socketserver/socketserver.go | 50 +++++++++---------- socketserver/internal/server/backend.go | 35 ++++--------- socketserver/internal/server/handlecore.go | 50 ++++++++----------- .../internal/server/publisher_test.go | 36 +++++++++---- socketserver/internal/server/types.go | 31 +++++++++--- 5 files changed, 104 insertions(+), 98 deletions(-) diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/socketserver/socketserver.go index 0b0e052d..5de7a059 100644 --- a/socketserver/cmd/socketserver/socketserver.go +++ b/socketserver/cmd/socketserver/socketserver.go @@ -2,56 +2,52 @@ package main // import "bitbucket.org/stendec/frankerfacez/socketserver/cmd/sock import ( "../../internal/server" + "encoding/json" "flag" + "fmt" + "io/ioutil" "log" "net/http" + "os" ) -var origin *string = flag.String("origin", "localhost:8001", "Client-visible origin of the socket server") -var bindAddress *string = flag.String("listen", "", "Address to bind to, if different from origin") -var usessl *bool = flag.Bool("ssl", false, "Enable the use of SSL for connecting clients and backend connections") -var certificateFile *string = flag.String("crt", "ssl.crt", "CA-signed SSL certificate file") -var privateKeyFile *string = flag.String("key", "ssl.key", "SSL private key file") - -var naclKeysFile *string = flag.String("naclkey", "naclkeys.json", "Keypairs for the NaCl crypto library, for communicating with the backend.") +var configFilename *string = flag.String("config", "config.json", "Configuration file, including the keypairs for the NaCl crypto library, for communicating with the backend.") var generateKeys *bool = flag.Bool("genkeys", false, "Generate NaCl keys instead of serving requests.\nArguments: [int serverId] [base64 backendPublic]\nThe backend public key can either be specified in base64 on the command line, or put in the json file later.") func main() { flag.Parse() if *generateKeys { - GenerateKeys(*naclKeysFile) + GenerateKeys(*configFilename) return } - if *origin == "" { - log.Fatalln("--origin argument required") + confFile, err := os.Open(*configFilename) + if os.IsNotExist(err) { + fmt.Println("Error: No config file. Run with -genkeys and edit config.json") + os.Exit(3) } - if *bindAddress == "" { - bindAddress = origin + if err != nil { + log.Fatal(err) } - if (*certificateFile == "") != (*privateKeyFile == "") { - log.Fatalln("Either both --crt and --key can be provided, or neither.") + conf := &server.ConfigFile{} + confBytes, err := ioutil.ReadAll(confFile) + if err != nil { + log.Fatal(err) } - - conf := &server.Config{ - SSLKeyFile: *privateKeyFile, - SSLCertificateFile: *certificateFile, - UseSSL: *usessl, - NaclKeysFile: *naclKeysFile, - - SocketOrigin: *origin, + err = json.Unmarshal(confBytes, &conf) + if err != nil { + log.Fatal(err) } httpServer := &http.Server{ - Addr: *bindAddress, + Addr: conf.ListenAddr, } server.SetupServerAndHandle(conf, httpServer.TLSConfig, nil) - var err error if conf.UseSSL { - err = httpServer.ListenAndServeTLS(*certificateFile, *privateKeyFile) + err = httpServer.ListenAndServeTLS(conf.SSLCertificateFile, conf.SSLKeyFile) } else { err = httpServer.ListenAndServe() } @@ -63,11 +59,13 @@ func main() { func GenerateKeys(outputFile string) { if flag.NArg() < 1 { - log.Fatal("The server ID must be specified") + fmt.Println("Specify a numeric server ID after -genkeys") + os.Exit(2) } if flag.NArg() >= 2 { server.GenerateKeys(outputFile, flag.Arg(0), flag.Arg(1)) } else { server.GenerateKeys(outputFile, flag.Arg(0), "") } + fmt.Println("Keys generated. Now edit config.json") } diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 9f316929..b966b354 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -11,7 +11,6 @@ import ( "log" "net/http" "net/url" - "os" "strconv" "strings" "sync" @@ -29,7 +28,7 @@ var serverId int var messageBufferPool sync.Pool -func SetupBackend(config *Config) { +func SetupBackend(config *ConfigFile) { backendHttpClient.Timeout = 60 * time.Second backendUrl = config.BackendUrl if responseCache != nil { @@ -41,21 +40,10 @@ func SetupBackend(config *Config) { messageBufferPool.New = New4KByteBuffer - var keys CryptoKeysBuf - file, err := os.Open(config.NaclKeysFile) - if err != nil { - log.Fatal(err) - } - dec := json.NewDecoder(file) - err = dec.Decode(&keys) - if err != nil { - log.Fatal(err) - } - var theirPublic, ourPrivate [32]byte - copy(theirPublic[:], keys.TheirPublicKey) - copy(ourPrivate[:], keys.OurPrivateKey) - serverId = keys.ServerId + copy(theirPublic[:], config.BackendPublicKey) + copy(ourPrivate[:], config.OurPrivateKey) + serverId = config.ServerId box.Precompute(&backendSharedKey, &theirPublic, &ourPrivate) } @@ -193,7 +181,7 @@ func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) { func GenerateKeys(outputFile, serverId, theirPublicStr string) { var err error - output := CryptoKeysBuf{} + output := ConfigFile{} output.ServerId, err = strconv.Atoi(serverId) if err != nil { @@ -212,19 +200,16 @@ func GenerateKeys(outputFile, serverId, theirPublicStr string) { if err != nil { log.Fatal(err) } - output.TheirPublicKey = theirPublic + output.BackendPublicKey = theirPublic } - file, err := os.Create(outputFile) + fmt.Println(ourPublic, ourPrivate) + bytes, err := json.MarshalIndent(output, "", "\t") if err != nil { log.Fatal(err) } - enc := json.NewEncoder(file) - err = enc.Encode(output) - if err != nil { - log.Fatal(err) - } - err = file.Close() + fmt.Println(string(bytes)) + err = ioutil.WriteFile(outputFile, bytes, 0600) if err != nil { log.Fatal(err) } diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 1f477f97..7ff9db07 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -6,31 +6,16 @@ import ( "errors" "fmt" "golang.org/x/net/websocket" + "html/template" "log" "net/http" "strconv" "strings" "sync" - "html/template" ) const MAX_PACKET_SIZE = 1024 -type Config struct { - // SSL/TLS - SSLCertificateFile string - SSLKeyFile string - UseSSL bool - - // NaCl keys for backend messages - NaclKeysFile string - - // Hostname of the socket server - SocketOrigin string - // URL to the backend server - BackendUrl string -} - // A command is how the client refers to a function on the server. It's just a string. type Command string @@ -85,10 +70,10 @@ var ExpectedStringAndInt = errors.New("Error: Expected array of string, int as a var ExpectedStringAndBool = errors.New("Error: Expected array of string, bool as arguments.") var ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.") -var gconfig *Config +var gconfig *ConfigFile // Create a websocket.Server with the options from the provided Config. -func setupServer(config *Config, tlsConfig *tls.Config) *websocket.Server { +func setupServer(config *ConfigFile, tlsConfig *tls.Config) *websocket.Server { gconfig = config sockConf, err := websocket.NewConfig("/", config.SocketOrigin) if err != nil { @@ -120,28 +105,36 @@ func setupServer(config *Config, tlsConfig *tls.Config) *websocket.Server { // Set up a websocket listener and register it on /. // (Uses http.DefaultServeMux .) -func SetupServerAndHandle(config *Config, tlsConfig *tls.Config, serveMux *http.ServeMux) { +func SetupServerAndHandle(config *ConfigFile, tlsConfig *tls.Config, serveMux *http.ServeMux) { sockServer := setupServer(config, tlsConfig) if serveMux == nil { serveMux = http.DefaultServeMux } serveMux.HandleFunc("/", ServeWebsocketOrCatbag(sockServer.ServeHTTP)) + serveMux.Handle("/assets", http.FileServer(nil)) // TODO serveMux.HandleFunc("/pub_msg", HBackendPublishRequest) serveMux.HandleFunc("/dump_backlog", HBackendDumpBacklog) serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish) } -var Catbag = template.Must(template.New(` +var Memes = template.Must(template.New("catbag").Parse(` - - CatBag - - - - -`).Parse("html")) + +
+
+
+
+
+
+ A FrankerFaceZ Service + — CatBag by Wolsk +
+ This socket server hosted by {{.}} +
+
+`)) func ServeWebsocketOrCatbag(sockfunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -149,8 +142,7 @@ func ServeWebsocketOrCatbag(sockfunc func(http.ResponseWriter, *http.Request)) h sockfunc(w, r) return } else { - - Catbag.Execute(w, nil) + Memes.Execute(w, "Todo Add Feature") } } } diff --git a/socketserver/internal/server/publisher_test.go b/socketserver/internal/server/publisher_test.go index e878f709..2dc54ed6 100644 --- a/socketserver/internal/server/publisher_test.go +++ b/socketserver/internal/server/publisher_test.go @@ -126,20 +126,36 @@ func TGetUrls(testserver *httptest.Server) TURLs { } } -const TNaclKeysLocation = "/tmp/test_naclkeys.json" - func TSetup(testserver **httptest.Server, urls *TURLs) { - if backendSharedKey[0] == 0 { - GenerateKeys(TNaclKeysLocation, "2", "+ZMqOmxhaVrCV5c0OMZ09QoSGcJHuqQtJrwzRD+JOjE=") - } DumpCache() + conf := &ConfigFile{ + ServerId: 20, + UseSSL: false, + SocketOrigin: "localhost:2002", + BannerHTML: ` + +CatBag + +
+
+
+
+
+
+ A FrankerFaceZ Service + — CatBag by Wolsk +
+
+`, + OurPublicKey: []byte{176, 149, 72, 209, 35, 42, 110, 220, 22, 236, 212, 129, 213, 199, 1, 227, 185, 167, 150, 159, 117, 202, 164, 100, 9, 107, 45, 141, 122, 221, 155, 73}, + OurPrivateKey: []byte{247, 133, 147, 194, 70, 240, 211, 216, 223, 16, 241, 253, 120, 14, 198, 74, 237, 180, 89, 33, 146, 146, 140, 58, 88, 160, 2, 246, 112, 35, 239, 87}, + BackendPublicKey: []byte{19, 163, 37, 157, 50, 139, 193, 85, 229, 47, 166, 21, 153, 231, 31, 133, 41, 158, 8, 53, 73, 0, 113, 91, 13, 181, 131, 248, 176, 18, 1, 107}, + } + gconfig = conf + SetupBackend(conf) + if testserver != nil { - conf := &Config{ - UseSSL: false, - NaclKeysFile: TNaclKeysLocation, - SocketOrigin: "localhost:2002", - } serveMux := http.NewServeMux() SetupServerAndHandle(conf, nil, serveMux) diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index a2bd074d..cc9ba947 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -9,24 +9,39 @@ import ( const CryptoBoxKeyLength = 32 -type CryptoKeysBuf struct { - OurPrivateKey []byte - OurPublicKey []byte - TheirPublicKey []byte - ServerId int +type ConfigFile struct { + // Numeric server id known to the backend + ServerId int + ListenAddr string + // Hostname of the socket server + SocketOrigin string + // URL to the backend server + BackendUrl string + // Memes go here + BannerHTML string + + // SSL/TLS + UseSSL bool + SSLCertificateFile string + SSLKeyFile string + + // Nacl keys + OurPrivateKey []byte + OurPublicKey []byte + BackendPublicKey []byte } type ClientMessage struct { // Message ID. Increments by 1 for each message sent from the client. // When replying to a command, the message ID must be echoed. // When sending a server-initiated message, this is -1. - MessageID int `json:_` + MessageID int // The command that the client wants from the server. // When sent from the server, the literal string 'True' indicates success. // Before sending, a blank Command will be converted into SuccessCommand. - Command Command `json:cmd` + Command Command // Result of json.Unmarshal on the third field send from the client - Arguments interface{} `json:data` + Arguments interface{} origArguments string } From c5f53657cb65da66d7a634db943a2587bc086412 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 27 Oct 2015 17:58:55 -0700 Subject: [PATCH 28/29] rename to ffzsocketserver --- .../cmd/{socketserver => ffzsocketserver}/socketserver.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename socketserver/cmd/{socketserver => ffzsocketserver}/socketserver.go (100%) diff --git a/socketserver/cmd/socketserver/socketserver.go b/socketserver/cmd/ffzsocketserver/socketserver.go similarity index 100% rename from socketserver/cmd/socketserver/socketserver.go rename to socketserver/cmd/ffzsocketserver/socketserver.go From 787f5a1caab27c4212dfb3cafbbc0c634e673747 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 27 Oct 2015 18:22:56 -0700 Subject: [PATCH 29/29] Defaults in the config file are a nice thing to have --- socketserver/internal/server/backend.go | 22 ++++++++++++++++++++-- socketserver/internal/server/handlecore.go | 22 +--------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index b966b354..72d74b22 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -181,7 +181,26 @@ func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) { func GenerateKeys(outputFile, serverId, theirPublicStr string) { var err error - output := ConfigFile{} + output := ConfigFile{ + ListenAddr: "0.0.0.0:8001", + SocketOrigin: "localhost:8001", + BackendUrl: "http://localhost:8002/ffz", + BannerHTML: ` + +CatBag + +
+
+
+
+
+
+ A FrankerFaceZ Service + — CatBag by Wolsk +
+
+`, + } output.ServerId, err = strconv.Atoi(serverId) if err != nil { @@ -203,7 +222,6 @@ func GenerateKeys(outputFile, serverId, theirPublicStr string) { output.BackendPublicKey = theirPublic } - fmt.Println(ourPublic, ourPrivate) bytes, err := json.MarshalIndent(output, "", "\t") if err != nil { log.Fatal(err) diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index 7ff9db07..f227db8b 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "golang.org/x/net/websocket" - "html/template" "log" "net/http" "strconv" @@ -112,37 +111,18 @@ func SetupServerAndHandle(config *ConfigFile, tlsConfig *tls.Config, serveMux *h serveMux = http.DefaultServeMux } serveMux.HandleFunc("/", ServeWebsocketOrCatbag(sockServer.ServeHTTP)) - serveMux.Handle("/assets", http.FileServer(nil)) // TODO serveMux.HandleFunc("/pub_msg", HBackendPublishRequest) serveMux.HandleFunc("/dump_backlog", HBackendDumpBacklog) serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish) } -var Memes = template.Must(template.New("catbag").Parse(` - -CatBag - -
-
-
-
-
-
- A FrankerFaceZ Service - — CatBag by Wolsk -
- This socket server hosted by {{.}} -
-
-`)) - func ServeWebsocketOrCatbag(sockfunc func(http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Connection") == "Upgrade" { sockfunc(w, r) return } else { - Memes.Execute(w, "Todo Add Feature") + w.Write([]byte(gconfig.BannerHTML)) } } }