1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 16:38:31 +00:00

Implement username validation

This commit is contained in:
Kane York 2015-11-08 16:44:16 -08:00
parent 0dffc494e4
commit 95a8f710f8
7 changed files with 242 additions and 21 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,8 @@ type ConfigFile struct {
OurPrivateKey []byte
OurPublicKey []byte
BackendPublicKey []byte
SendAuthToNewClients bool
}
type ClientMessage struct {