1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
FrankerFaceZ/socketserver/server/backend.go

308 lines
8.1 KiB
Go
Raw Normal View History

package server
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
2015-11-08 16:44:16 -08:00
"errors"
"fmt"
"github.com/pmylund/go-cache"
"golang.org/x/crypto/nacl/box"
"io/ioutil"
2015-10-25 03:21:50 -07:00
"log"
"net/http"
"net/url"
"strconv"
2015-10-25 12:40:07 -07:00
"strings"
"sync"
"time"
)
2015-11-15 18:43:34 -08:00
var backendHTTPClient http.Client
var backendURL string
var responseCache *cache.Cache
2015-11-15 18:43:34 -08:00
var postStatisticsURL string
var addTopicURL string
var announceStartupURL string
2015-10-25 03:21:50 -07:00
2015-10-25 12:40:07 -07:00
var backendSharedKey [32]byte
2015-11-15 18:43:34 -08:00
var serverID int
2015-10-25 12:40:07 -07:00
var messageBufferPool sync.Pool
2015-11-15 18:43:34 -08:00
func setupBackend(config *ConfigFile) {
backendHTTPClient.Timeout = 60 * time.Second
backendURL = config.BackendURL
2015-10-25 03:21:50 -07:00
if responseCache != nil {
responseCache.Flush()
}
2015-11-08 16:44:16 -08:00
responseCache = cache.New(60*time.Second, 120*time.Second)
2015-10-25 03:21:50 -07:00
2015-11-15 18:43:34 -08:00
postStatisticsURL = fmt.Sprintf("%s/stats", backendURL)
addTopicURL = fmt.Sprintf("%s/topics", backendURL)
announceStartupURL = fmt.Sprintf("%s/startup", backendURL)
2015-10-25 03:21:50 -07:00
2015-10-25 14:06:56 -07:00
messageBufferPool.New = New4KByteBuffer
2015-10-25 12:40:07 -07:00
2015-10-25 14:06:56 -07:00
var theirPublic, ourPrivate [32]byte
copy(theirPublic[:], config.BackendPublicKey)
copy(ourPrivate[:], config.OurPrivateKey)
2015-11-15 18:43:34 -08:00
serverID = config.ServerID
2015-10-25 14:06:56 -07:00
box.Precompute(&backendSharedKey, &theirPublic, &ourPrivate)
}
func getCacheKey(remoteCommand, data string) string {
return fmt.Sprintf("%s/%s", remoteCommand, data)
}
2015-11-15 18:43:34 -08:00
// HBackendPublishRequest handles the /uncached_pub route.
// The backend can POST here to publish a message to clients with no caching.
// The POST arguments are `cmd`, `args`, `channel`, and `scope`.
// The `scope` argument is required because no attempt is made to infer the scope from the command, unlike /cached_pub.
2015-11-16 13:25:25 -08:00
func HTTPBackendUncachedPublish(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")
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) {
w.WriteHeader(422)
fmt.Fprintf(w, "Error: channel must be specified")
return
}
cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json}
cm.parseOrigArguments()
var count int
switch target {
case MsgTargetTypeChat:
2015-11-08 22:34:06 -08:00
count = PublishToChannel(channel, cm)
case MsgTargetTypeMultichat:
2015-11-08 22:01:32 -08:00
count = PublishToMultiple(strings.Split(channel, ","), cm)
case MsgTargetTypeGlobal:
count = PublishToAll(cm)
case MsgTargetTypeInvalid:
fallthrough
default:
w.WriteHeader(422)
fmt.Fprint(w, "Invalid 'scope'. must be chat, multichat, channel, or global")
return
}
fmt.Fprint(w, count)
}
2015-11-15 18:43:34 -08:00
// ErrForwardedFromBackend is an error returned by the backend server.
type ErrForwardedFromBackend string
2015-11-15 18:43:34 -08:00
func (bfe ErrForwardedFromBackend) Error() string {
return string(bfe)
}
2015-11-15 18:43:34 -08:00
// ErrAuthorizationNeeded is emitted when the backend replies with HTTP 401.
// Indicates that an attempt to validate `ClientInfo.TwitchUsername` should be attempted.
var ErrAuthorizationNeeded = errors.New("Must authenticate Twitch username to use this command")
2015-11-08 16:44:16 -08:00
2015-11-15 18:43:34 -08:00
// SendRemoteCommandCached performs a RPC call on the backend, but caches responses.
2015-11-08 22:01:32 -08:00
func SendRemoteCommandCached(remoteCommand, data string, auth AuthInfo) (string, error) {
cached, ok := responseCache.Get(getCacheKey(remoteCommand, data))
if ok {
return cached.(string), nil
}
2015-11-08 22:01:32 -08:00
return SendRemoteCommand(remoteCommand, data, auth)
}
2015-11-15 18:43:34 -08:00
// SendRemoteCommand performs a RPC call on the backend by POSTing to `/cmd/$remoteCommand`.
// The form data is as follows: `clientData` is the JSON in the `data` parameter
// (should be retrieved from ClientMessage.Arguments), and either `username` or
// `usernameClaimed` depending on whether AuthInfo.UsernameValidates is true is AuthInfo.TwitchUsername.
2015-11-08 22:01:32 -08:00
func SendRemoteCommand(remoteCommand, data string, auth AuthInfo) (responseStr string, err error) {
2015-11-15 18:43:34 -08:00
destURL := fmt.Sprintf("%s/cmd/%s", backendURL, remoteCommand)
var authKey string
if auth.UsernameValidated {
authKey = "usernameClaimed"
} else {
authKey = "username"
}
formData := url.Values{
"clientData": []string{data},
authKey: []string{auth.TwitchUsername},
}
sealedForm, err := SealRequest(formData)
if err != nil {
return "", err
}
2015-11-15 18:43:34 -08:00
resp, err := backendHTTPClient.PostForm(destURL, sealedForm)
if err != nil {
return "", err
}
defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
2015-10-25 12:40:07 -07:00
responseStr = string(respBytes)
2015-11-08 16:44:16 -08:00
if resp.StatusCode == 401 {
2015-11-15 18:43:34 -08:00
return "", ErrAuthorizationNeeded
2015-11-08 16:44:16 -08:00
} else if resp.StatusCode != 200 {
if resp.Header.Get("Content-Type") == "application/json" {
2015-11-15 18:43:34 -08:00
return "", ErrForwardedFromBackend(responseStr)
}
2015-11-15 18:43:34 -08:00
return "", httpError(resp.StatusCode)
}
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
2015-10-25 12:40:07 -07:00
responseCache.Set(getCacheKey(remoteCommand, data), responseStr, duration)
}
2015-10-25 12:40:07 -07:00
return
2015-10-25 03:21:50 -07:00
}
2015-11-16 12:50:00 -08:00
// SendAggregatedData sends aggregated emote usage and following data to the backend server.
2015-10-28 18:12:20 -07:00
func SendAggregatedData(sealedForm url.Values) error {
2015-11-15 18:43:34 -08:00
resp, err := backendHTTPClient.PostForm(postStatisticsURL, sealedForm)
if err != nil {
return err
}
if resp.StatusCode != 200 {
resp.Body.Close()
return httpError(resp.StatusCode)
}
return resp.Body.Close()
}
2015-11-15 18:43:34 -08:00
// ErrBackendNotOK indicates that the backend replied with something other than the string "ok".
type ErrBackendNotOK struct {
Response string
2015-11-08 16:44:16 -08:00
Code int
}
2015-11-08 16:44:16 -08:00
2015-11-15 18:43:34 -08:00
// Implements the error interface.
func (noe ErrBackendNotOK) Error() string {
return fmt.Sprintf("backend returned %d: %s", noe.Code, noe.Response)
}
2015-11-16 12:50:00 -08:00
// SendNewTopicNotice notifies the backend that a client has performed the first subscription to a pub/sub topic.
// POST data:
// channels=room.trihex
// added=t
func SendNewTopicNotice(topic string) error {
return sendTopicNotice(topic, true)
}
2015-11-16 12:50:00 -08:00
// SendCleanupTopicsNotice notifies the backend that pub/sub topics have no subscribers anymore.
// POST data:
// channels=room.sirstendec,room.bobross,feature.foo
// added=f
func SendCleanupTopicsNotice(topics []string) error {
return sendTopicNotice(strings.Join(topics, ","), false)
}
func sendTopicNotice(topic string, added bool) error {
formData := url.Values{}
formData.Set("channels", topic)
if added {
formData.Set("added", "t")
} else {
formData.Set("added", "f")
}
sealedForm, err := SealRequest(formData)
if err != nil {
return err
}
2015-11-15 18:43:34 -08:00
resp, err := backendHTTPClient.PostForm(addTopicURL, sealedForm)
if err != nil {
return err
}
defer resp.Body.Close()
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
respStr := string(respBytes)
if respStr != "ok" {
2015-11-15 18:43:34 -08:00
return ErrBackendNotOK{Code: resp.StatusCode, Response: respStr}
}
return nil
2015-10-25 12:40:07 -07:00
}
func httpError(statusCode int) error {
return fmt.Errorf("backend http error: %d", statusCode)
}
2015-11-16 12:50:00 -08:00
// GenerateKeys generates a new NaCl keypair for the server and writes out the default configuration file.
2015-11-15 18:43:34 -08:00
func GenerateKeys(outputFile, serverID, theirPublicStr string) {
2015-10-25 12:40:07 -07:00
var err error
output := ConfigFile{
2015-11-17 11:11:14 -08:00
ListenAddr: "0.0.0.0:8001",
2015-12-02 19:08:19 -08:00
SSLListenAddr: "0.0.0.0:443",
2015-11-17 11:11:14 -08:00
SocketOrigin: "localhost:8001",
BackendURL: "http://localhost:8002/ffz",
MinMemoryKBytes: defaultMinMemoryKB,
}
2015-10-25 12:40:07 -07:00
2015-11-15 18:43:34 -08:00
output.ServerID, err = strconv.Atoi(serverID)
2015-10-25 12:40:07 -07:00
if err != nil {
log.Fatal(err)
}
ourPublic, ourPrivate, err := box.GenerateKey(rand.Reader)
if err != nil {
log.Fatal(err)
}
2015-10-25 14:06:56 -07:00
output.OurPublicKey, output.OurPrivateKey = ourPublic[:], ourPrivate[:]
2015-10-25 12:40:07 -07:00
if theirPublicStr != "" {
2015-10-25 14:06:56 -07:00
reader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(theirPublicStr))
2015-10-25 12:40:07 -07:00
theirPublic, err := ioutil.ReadAll(reader)
if err != nil {
log.Fatal(err)
}
output.BackendPublicKey = theirPublic
2015-10-25 12:40:07 -07:00
}
bytes, err := json.MarshalIndent(output, "", "\t")
2015-10-25 12:40:07 -07:00
if err != nil {
log.Fatal(err)
}
fmt.Println(string(bytes))
err = ioutil.WriteFile(outputFile, bytes, 0600)
2015-10-25 12:40:07 -07:00
if err != nil {
log.Fatal(err)
}
}