diff --git a/socketserver/cmd/ffzsocketserver/console.go b/socketserver/cmd/ffzsocketserver/console.go index a82904bd..e6ceeefb 100644 --- a/socketserver/cmd/ffzsocketserver/console.go +++ b/socketserver/cmd/ffzsocketserver/console.go @@ -13,6 +13,11 @@ func commandLineConsole() { shell := ishell.NewShell() + shell.Register("help", func(args ...string) (string, error) { + shell.PrintCommands() + return "", nil + }) + shell.Register("clientcount", func(args ...string) (string, error) { server.GlobalSubscriptionInfo.RLock() count := len(server.GlobalSubscriptionInfo.Members) @@ -67,6 +72,24 @@ func commandLineConsole() { return "", nil }) + shell.Register("authorizeeveryone", func(args ...string) (string, error) { + if len(args) == 0 { + if server.Configuation.SendAuthToNewClients { + return "All clients are recieving auth challenges upon claiming a name.", nil + } else { + return "All clients are not recieving auth challenges upon claiming a name.", nil + } + } else if args[0] == "on" { + server.Configuation.SendAuthToNewClients = true + return "All new clients will recieve auth challenges upon claiming a name.", nil + } else if args[0] == "off" { + server.Configuation.SendAuthToNewClients = false + return "All new clients will not recieve auth challenges upon claiming a name.", nil + } else { + return "Usage: authorizeeveryone [ on | off ]", nil + } + }) + shell.Register("panic", func(args ...string) (string, error) { go func() { panic("requested panic") diff --git a/socketserver/internal/server/backend.go b/socketserver/internal/server/backend.go index 3e98e742..40ebfcf9 100644 --- a/socketserver/internal/server/backend.go +++ b/socketserver/internal/server/backend.go @@ -4,7 +4,9 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "errors" "fmt" + "github.com/gorilla/websocket" "github.com/pmylund/go-cache" "golang.org/x/crypto/nacl/box" "io/ioutil" @@ -15,7 +17,6 @@ import ( "strings" "sync" "time" - "github.com/gorilla/websocket" ) var backendHttpClient http.Client @@ -37,7 +38,7 @@ func SetupBackend(config *ConfigFile) { 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) postStatisticsUrl = fmt.Sprintf("%s/stats", backendUrl) @@ -114,6 +115,8 @@ func (bfe BackendForwardedError) Error() string { return string(bfe) } +var AuthorizationNeededError = errors.New("Must authenticate Twitch username to use this command") + func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) { cached, ok := responseCache.Get(getCacheKey(remoteCommand, data)) if ok { @@ -154,7 +157,9 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s responseStr = string(respBytes) - if resp.StatusCode != 200 { + if resp.StatusCode == 401 { + return "", AuthorizationNeededError + } else if resp.StatusCode != 200 { if resp.Header.Get("Content-Type") == "application/json" { return "", BackendForwardedError(responseStr) } else { @@ -222,8 +227,9 @@ func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) { type NotOkError struct { Response string - Code int + Code int } + func (noe NotOkError) Error() string { return fmt.Sprintf("backend returned %d: %s", noe.Code, noe.Response) } diff --git a/socketserver/internal/server/commands.go b/socketserver/internal/server/commands.go index 9594362e..02f51dae 100644 --- a/socketserver/internal/server/commands.go +++ b/socketserver/internal/server/commands.go @@ -109,6 +109,14 @@ func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) client.UsernameValidated = false client.Mutex.Unlock() + if Configuation.SendAuthToNewClients { + client.MsgChannelKeepalive.Add(1) + go client.StartAuthorization(func(_ *ClientInfo, _ bool) { + client.MsgChannelKeepalive.Done() + }) + + } + return ResponseSuccess, nil } @@ -408,18 +416,32 @@ func HandleBunchedRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg Cl func HandleRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) { client.MsgChannelKeepalive.Add(1) - go func(conn *websocket.Conn, msg ClientMessage, authInfo AuthInfo) { - resp, err := RequestRemoteDataCached(string(msg.Command), msg.origArguments, authInfo) - - if err != nil { - client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: err.Error()} - } else { - msg := ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand, origArguments: resp} - msg.parseOrigArguments() - client.MessageChannel <- msg - } - client.MsgChannelKeepalive.Done() - }(conn, msg, client.AuthInfo) + go doRemoteCommand(conn, msg, client) return ClientMessage{Command: AsyncResponseCommand}, nil } + +const AuthorizationFailedErrorString = "Failed to verify your Twitch username." + +func doRemoteCommand(conn *websocket.Conn, msg ClientMessage, client *ClientInfo) { + resp, err := RequestRemoteDataCached(string(msg.Command), msg.origArguments, client.AuthInfo) + + if err == AuthorizationNeededError { + client.StartAuthorization(func(_ *ClientInfo, success bool) { + if success { + doRemoteCommand(conn, msg, client) + } else { + client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: AuthorizationFailedErrorString} + client.MsgChannelKeepalive.Done() + } + }) + return // without keepalive.Done() + } else if err != nil { + client.MessageChannel <- ClientMessage{MessageID: msg.MessageID, Command: ErrorCommand, Arguments: err.Error()} + } else { + msg := ClientMessage{MessageID: msg.MessageID, Command: SuccessCommand, origArguments: resp} + msg.parseOrigArguments() + client.MessageChannel <- msg + } + client.MsgChannelKeepalive.Done() +} diff --git a/socketserver/internal/server/handlecore.go b/socketserver/internal/server/handlecore.go index f6778d55..21b4c6d8 100644 --- a/socketserver/internal/server/handlecore.go +++ b/socketserver/internal/server/handlecore.go @@ -51,6 +51,8 @@ const ErrorCommand Command = "error" // This must be the first command sent by the client once the connection is established. const HelloCommand Command = "hello" +const AuthorizeCommand Command = "do_authorize" + // 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" @@ -73,14 +75,14 @@ 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 *ConfigFile +var Configuation *ConfigFile var BannerHTML []byte // Set up a websocket listener and register it on /. // (Uses http.DefaultServeMux .) func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) { - gconfig = config + Configuation = config SetupBackend(config) @@ -99,9 +101,12 @@ func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) { serveMux.HandleFunc("/dump_backlog", HBackendDumpBacklog) serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish) - go deadChannelReaper() + go pubsubJanitor() go backlogJanitor() + go authorizationJanitor() go sendAggregateData() + + go ircConnection() } func ServeWebsocketOrCatbag(w http.ResponseWriter, r *http.Request) { diff --git a/socketserver/internal/server/irc.go b/socketserver/internal/server/irc.go new file mode 100644 index 00000000..a9a95edf --- /dev/null +++ b/socketserver/internal/server/irc.go @@ -0,0 +1,163 @@ +package server + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + irc "github.com/fluffle/goirc/client" + "log" + "strings" + "sync" + "time" +) + +type AuthCallback func(client *ClientInfo, successful bool) + +type PendingAuthorization struct { + Client *ClientInfo + Challenge string + Callback AuthCallback + EnteredAt time.Time +} + +var PendingAuths []PendingAuthorization +var PendingAuthLock sync.Mutex + +func AddPendingAuthorization(client *ClientInfo, challenge string, callback AuthCallback) { + PendingAuthLock.Lock() + defer PendingAuthLock.Unlock() + + PendingAuths = append(PendingAuths, PendingAuthorization{ + Client: client, + Challenge: challenge, + Callback: callback, + EnteredAt: time.Now(), + }) +} + +func authorizationJanitor() { + for { + time.Sleep(5 * time.Minute) + + func() { + cullTime := time.Now().Add(-30 * time.Minute) + + PendingAuthLock.Lock() + defer PendingAuthLock.Unlock() + + newPendingAuths := make([]PendingAuthorization, 0, len(PendingAuths)) + + for _, v := range PendingAuths { + if !cullTime.After(v.EnteredAt) { + newPendingAuths = append(newPendingAuths, v) + } + } + + PendingAuths = newPendingAuths + }() + } +} + +func (client *ClientInfo) StartAuthorization(callback AuthCallback) { + fmt.Println(DEBUG, "startig auth for user", client.TwitchUsername, client.RemoteAddr) + var nonce [32]byte + _, err := rand.Read(nonce[:]) + if err != nil { + go func(client *ClientInfo, callback AuthCallback) { + callback(client, false) + }(client, callback) + return + } + buf := bytes.NewBuffer(nil) + enc := base64.NewEncoder(base64.RawURLEncoding, buf) + enc.Write(nonce[:]) + enc.Close() + challenge := buf.String() + + fmt.Println(DEBUG, "adding to auth array") + AddPendingAuthorization(client, challenge, callback) + + fmt.Println(DEBUG, "sending auth message") + client.MessageChannel <- ClientMessage{MessageID: -1, Command: AuthorizeCommand, Arguments: challenge} +} + +const AuthChannelName = "frankerfacezauthorizer" +const AuthChannel = "#" + AuthChannelName +const AuthCommand = "AUTH" + +const DEBUG = "DEBUG" + +func ircConnection() { + + c := irc.SimpleClient("justinfan123") + + c.HandleFunc(irc.CONNECTED, func(conn *irc.Conn, line *irc.Line) { + conn.Join(AuthChannel) + }) + + c.HandleFunc(irc.PRIVMSG, func(conn *irc.Conn, line *irc.Line) { + channel := line.Args[0] + msg := line.Args[1] + if channel != AuthChannel || !strings.HasPrefix(msg, AuthCommand) || !line.Public() { + fmt.Println(DEBUG, "discarded msg", line.Raw) + return + } + + msgArray := strings.Split(msg, " ") + if len(msgArray) != 2 { + fmt.Println(DEBUG, "discarded msg - not 2 strings", line.Raw) + return + } + + submittedUser := line.Nick + submittedChallenge := msgArray[1] + + var auth PendingAuthorization + var idx int = -1 + + PendingAuthLock.Lock() + for i, v := range PendingAuths { + if v.Client.TwitchUsername == submittedUser && v.Challenge == submittedChallenge { + auth = v + idx = i + break + } + } + if idx != -1 { + PendingAuths = append(PendingAuths[:idx], PendingAuths[idx+1:]...) + } + PendingAuthLock.Unlock() + + if idx == -1 { + fmt.Println(DEBUG, "discarded msg - challenge not found", line.Raw) + return + } + + // auth is valid, and removed from pending list + + fmt.Println(DEBUG, "authorization success for user", auth.Client.TwitchUsername) + var usernameChanged bool + auth.Client.Mutex.Lock() + if auth.Client.TwitchUsername == submittedUser { // recheck condition + auth.Client.UsernameValidated = true + } else { + usernameChanged = true + } + auth.Client.Mutex.Unlock() + + if auth.Callback != nil { + if !usernameChanged { + auth.Callback(auth.Client, true) + } else { + auth.Callback(auth.Client, false) + } + } + }) + + err := c.ConnectTo("irc.twitch.tv") + if err != nil { + log.Fatalln("Cannot connect to IRC:", err) + } + +} diff --git a/socketserver/internal/server/publisher.go b/socketserver/internal/server/publisher.go index 54a5b3d1..92b14f68 100644 --- a/socketserver/internal/server/publisher.go +++ b/socketserver/internal/server/publisher.go @@ -4,9 +4,9 @@ package server // If I screwed up the locking, I won't know until it's too late. import ( + "log" "sync" "time" - "log" ) type SubscriberList struct { @@ -165,7 +165,7 @@ const ReapingDelay = 20 * time.Minute // Checks ChatSubscriptionInfo for entries with no subscribers every ReapingDelay. // Started from SetupServer(). -func deadChannelReaper() { +func pubsubJanitor() { for { time.Sleep(ReapingDelay) var cleanedUp = make([]string, 0, 6) diff --git a/socketserver/internal/server/types.go b/socketserver/internal/server/types.go index 7147bb1c..0c889e55 100644 --- a/socketserver/internal/server/types.go +++ b/socketserver/internal/server/types.go @@ -28,6 +28,8 @@ type ConfigFile struct { OurPrivateKey []byte OurPublicKey []byte BackendPublicKey []byte + + SendAuthToNewClients bool } type ClientMessage struct {