mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-09-15 17:46:55 +00:00
Implement username validation
This commit is contained in:
parent
0dffc494e4
commit
95a8f710f8
7 changed files with 242 additions and 21 deletions
|
@ -13,6 +13,11 @@ func commandLineConsole() {
|
||||||
|
|
||||||
shell := ishell.NewShell()
|
shell := ishell.NewShell()
|
||||||
|
|
||||||
|
shell.Register("help", func(args ...string) (string, error) {
|
||||||
|
shell.PrintCommands()
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
shell.Register("clientcount", func(args ...string) (string, error) {
|
shell.Register("clientcount", func(args ...string) (string, error) {
|
||||||
server.GlobalSubscriptionInfo.RLock()
|
server.GlobalSubscriptionInfo.RLock()
|
||||||
count := len(server.GlobalSubscriptionInfo.Members)
|
count := len(server.GlobalSubscriptionInfo.Members)
|
||||||
|
@ -67,6 +72,24 @@ func commandLineConsole() {
|
||||||
return "", nil
|
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) {
|
shell.Register("panic", func(args ...string) (string, error) {
|
||||||
go func() {
|
go func() {
|
||||||
panic("requested panic")
|
panic("requested panic")
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
"github.com/pmylund/go-cache"
|
"github.com/pmylund/go-cache"
|
||||||
"golang.org/x/crypto/nacl/box"
|
"golang.org/x/crypto/nacl/box"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -15,7 +17,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var backendHttpClient http.Client
|
var backendHttpClient http.Client
|
||||||
|
@ -37,7 +38,7 @@ func SetupBackend(config *ConfigFile) {
|
||||||
if responseCache != nil {
|
if responseCache != nil {
|
||||||
responseCache.Flush()
|
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)
|
getBacklogUrl = fmt.Sprintf("%s/backlog", backendUrl)
|
||||||
postStatisticsUrl = fmt.Sprintf("%s/stats", backendUrl)
|
postStatisticsUrl = fmt.Sprintf("%s/stats", backendUrl)
|
||||||
|
@ -114,6 +115,8 @@ func (bfe BackendForwardedError) Error() string {
|
||||||
return string(bfe)
|
return string(bfe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AuthorizationNeededError = errors.New("Must authenticate Twitch username to use this command")
|
||||||
|
|
||||||
func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) {
|
func RequestRemoteDataCached(remoteCommand, data string, auth AuthInfo) (string, error) {
|
||||||
cached, ok := responseCache.Get(getCacheKey(remoteCommand, data))
|
cached, ok := responseCache.Get(getCacheKey(remoteCommand, data))
|
||||||
if ok {
|
if ok {
|
||||||
|
@ -154,7 +157,9 @@ func RequestRemoteData(remoteCommand, data string, auth AuthInfo) (responseStr s
|
||||||
|
|
||||||
responseStr = string(respBytes)
|
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" {
|
if resp.Header.Get("Content-Type") == "application/json" {
|
||||||
return "", BackendForwardedError(responseStr)
|
return "", BackendForwardedError(responseStr)
|
||||||
} else {
|
} else {
|
||||||
|
@ -222,8 +227,9 @@ func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) {
|
||||||
|
|
||||||
type NotOkError struct {
|
type NotOkError struct {
|
||||||
Response string
|
Response string
|
||||||
Code int
|
Code int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (noe NotOkError) Error() string {
|
func (noe NotOkError) Error() string {
|
||||||
return fmt.Sprintf("backend returned %d: %s", noe.Code, noe.Response)
|
return fmt.Sprintf("backend returned %d: %s", noe.Code, noe.Response)
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,14 @@ func HandleSetUser(conn *websocket.Conn, client *ClientInfo, msg ClientMessage)
|
||||||
client.UsernameValidated = false
|
client.UsernameValidated = false
|
||||||
client.Mutex.Unlock()
|
client.Mutex.Unlock()
|
||||||
|
|
||||||
|
if Configuation.SendAuthToNewClients {
|
||||||
|
client.MsgChannelKeepalive.Add(1)
|
||||||
|
go client.StartAuthorization(func(_ *ClientInfo, _ bool) {
|
||||||
|
client.MsgChannelKeepalive.Done()
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseSuccess, nil
|
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) {
|
func HandleRemoteCommand(conn *websocket.Conn, client *ClientInfo, msg ClientMessage) (rmsg ClientMessage, err error) {
|
||||||
client.MsgChannelKeepalive.Add(1)
|
client.MsgChannelKeepalive.Add(1)
|
||||||
go func(conn *websocket.Conn, msg ClientMessage, authInfo AuthInfo) {
|
go doRemoteCommand(conn, msg, client)
|
||||||
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)
|
|
||||||
|
|
||||||
return ClientMessage{Command: AsyncResponseCommand}, nil
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -51,6 +51,8 @@ const ErrorCommand Command = "error"
|
||||||
// This must be the first command sent by the client once the connection is established.
|
// This must be the first command sent by the client once the connection is established.
|
||||||
const HelloCommand Command = "hello"
|
const HelloCommand Command = "hello"
|
||||||
|
|
||||||
|
const AuthorizeCommand Command = "do_authorize"
|
||||||
|
|
||||||
// A handler returning a ClientMessage with this Command will prevent replying to the client.
|
// 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.
|
// It signals that the work has been handed off to a background goroutine.
|
||||||
const AsyncResponseCommand Command = "_async"
|
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 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 ExpectedStringAndIntGotFloat = errors.New("Error: Second argument was a float, expected an integer.")
|
||||||
|
|
||||||
var gconfig *ConfigFile
|
var Configuation *ConfigFile
|
||||||
|
|
||||||
var BannerHTML []byte
|
var BannerHTML []byte
|
||||||
|
|
||||||
// Set up a websocket listener and register it on /.
|
// Set up a websocket listener and register it on /.
|
||||||
// (Uses http.DefaultServeMux .)
|
// (Uses http.DefaultServeMux .)
|
||||||
func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) {
|
func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) {
|
||||||
gconfig = config
|
Configuation = config
|
||||||
|
|
||||||
SetupBackend(config)
|
SetupBackend(config)
|
||||||
|
|
||||||
|
@ -99,9 +101,12 @@ func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) {
|
||||||
serveMux.HandleFunc("/dump_backlog", HBackendDumpBacklog)
|
serveMux.HandleFunc("/dump_backlog", HBackendDumpBacklog)
|
||||||
serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish)
|
serveMux.HandleFunc("/update_and_pub", HBackendUpdateAndPublish)
|
||||||
|
|
||||||
go deadChannelReaper()
|
go pubsubJanitor()
|
||||||
go backlogJanitor()
|
go backlogJanitor()
|
||||||
|
go authorizationJanitor()
|
||||||
go sendAggregateData()
|
go sendAggregateData()
|
||||||
|
|
||||||
|
go ircConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeWebsocketOrCatbag(w http.ResponseWriter, r *http.Request) {
|
func ServeWebsocketOrCatbag(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
163
socketserver/internal/server/irc.go
Normal file
163
socketserver/internal/server/irc.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,9 +4,9 @@ package server
|
||||||
// If I screwed up the locking, I won't know until it's too late.
|
// If I screwed up the locking, I won't know until it's too late.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubscriberList struct {
|
type SubscriberList struct {
|
||||||
|
@ -165,7 +165,7 @@ const ReapingDelay = 20 * time.Minute
|
||||||
|
|
||||||
// Checks ChatSubscriptionInfo for entries with no subscribers every ReapingDelay.
|
// Checks ChatSubscriptionInfo for entries with no subscribers every ReapingDelay.
|
||||||
// Started from SetupServer().
|
// Started from SetupServer().
|
||||||
func deadChannelReaper() {
|
func pubsubJanitor() {
|
||||||
for {
|
for {
|
||||||
time.Sleep(ReapingDelay)
|
time.Sleep(ReapingDelay)
|
||||||
var cleanedUp = make([]string, 0, 6)
|
var cleanedUp = make([]string, 0, 6)
|
||||||
|
|
|
@ -28,6 +28,8 @@ type ConfigFile struct {
|
||||||
OurPrivateKey []byte
|
OurPrivateKey []byte
|
||||||
OurPublicKey []byte
|
OurPublicKey []byte
|
||||||
BackendPublicKey []byte
|
BackendPublicKey []byte
|
||||||
|
|
||||||
|
SendAuthToNewClients bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientMessage struct {
|
type ClientMessage struct {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue