1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-02 17:18:31 +00:00

rename listener package to internal/server

This commit is contained in:
Kane York 2015-10-25 00:44:25 -07:00
parent 93c3f6f672
commit 2a6c36bba5
9 changed files with 346 additions and 284 deletions

View file

@ -1,2 +0,0 @@
package broadcaster

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -1,4 +1,4 @@
package lib
package server
import (
)

View file

@ -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
}