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

335 lines
9.5 KiB
Go
Raw Permalink Normal View History

package server
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
2015-11-08 16:44:16 -08:00
"errors"
"fmt"
"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"
2016-07-08 12:46:16 -07:00
"sync"
2017-02-02 23:08:21 -08:00
"time"
2016-07-08 12:46:16 -07:00
2017-09-15 16:40:40 -07:00
"github.com/FrankerFaceZ/FrankerFaceZ/socketserver/server/naclform"
2017-09-25 15:24:58 -07:00
cache "github.com/patrickmn/go-cache"
2016-04-28 14:36:59 -07:00
"golang.org/x/crypto/nacl/box"
"golang.org/x/sync/singleflight"
)
const bPathAnnounceStartup = "/startup"
const bPathAddTopic = "/topics"
const bPathAggStats = "/stats"
const bPathOtherCommand = "/cmd/"
2016-06-02 08:30:49 -07:00
type backendInfo struct {
HTTPClient http.Client
baseURL string
2017-09-25 15:24:58 -07:00
responseCache *cache.Cache
reloadGroup singleflight.Group
2016-06-02 09:03:43 -07:00
postStatisticsURL string
addTopicURL string
announceStartupURL string
2017-09-15 16:40:40 -07:00
secureForm naclform.ServerInfo
lastSuccess map[string]time.Time
lastSuccessLock sync.Mutex
}
2016-06-02 08:30:49 -07:00
var Backend *backendInfo
func setupBackend(config *ConfigFile) *backendInfo {
b := new(backendInfo)
Backend = b
2017-09-15 16:40:40 -07:00
b.secureForm.ServerID = config.ServerID
2016-06-02 08:36:02 -07:00
b.HTTPClient.Timeout = 60 * time.Second
2016-06-02 08:39:01 -07:00
b.baseURL = config.BackendURL
// size in bytes of string payload
2017-09-25 15:24:58 -07:00
b.responseCache = cache.New(60*time.Second, 10*time.Minute)
2015-10-25 03:21:50 -07:00
2016-06-02 09:03:43 -07:00
b.announceStartupURL = fmt.Sprintf("%s%s", b.baseURL, bPathAnnounceStartup)
b.addTopicURL = fmt.Sprintf("%s%s", b.baseURL, bPathAddTopic)
b.postStatisticsURL = fmt.Sprintf("%s%s", b.baseURL, bPathAggStats)
2016-06-02 08:39:11 -07:00
epochTime := time.Unix(0, 0).UTC()
2016-06-02 09:04:46 -07:00
lastBackendSuccess := map[string]time.Time{
bPathAnnounceStartup: epochTime,
bPathAddTopic: epochTime,
bPathAggStats: epochTime,
bPathOtherCommand: epochTime,
}
b.lastSuccess = lastBackendSuccess
2015-10-25 03:21:50 -07:00
2015-10-25 14:06:56 -07:00
var theirPublic, ourPrivate [32]byte
copy(theirPublic[:], config.BackendPublicKey)
copy(ourPrivate[:], config.OurPrivateKey)
2015-10-25 14:06:56 -07:00
2017-09-15 16:40:40 -07:00
box.Precompute(&b.secureForm.SharedKey, &theirPublic, &ourPrivate)
2016-06-02 08:30:49 -07:00
return b
}
func getCacheKey(remoteCommand, data string) string {
return fmt.Sprintf("%s/%s", remoteCommand, data)
}
2015-11-15 18:43:34 -08:00
// ErrForwardedFromBackend is an error returned by the backend server.
2016-01-03 13:50:33 -08:00
type ErrForwardedFromBackend struct {
JSONError interface{}
}
2015-11-15 18:43:34 -08:00
func (bfe ErrForwardedFromBackend) Error() string {
bytes, _ := json.Marshal(bfe.JSONError)
2016-01-03 13:50:33 -08:00
return string(bytes)
}
2015-11-15 18:43:34 -08:00
// ErrAuthorizationNeeded is emitted when the backend replies with HTTP 401.
//
2015-11-15 18:43:34 -08:00
// 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
// SendRemoteCommandCached performs a RPC call on the backend, checking for a
// cached response first.
//
// If a cached, but expired, response is found, the existing value is returned
// and the cache is updated in the background.
2016-06-02 08:36:02 -07:00
func (backend *backendInfo) SendRemoteCommandCached(remoteCommand, data string, auth AuthInfo) (string, error) {
cacheKey := getCacheKey(remoteCommand, data)
2017-09-25 15:24:58 -07:00
cached, ok := backend.responseCache.Get(cacheKey)
if ok {
return cached.(string), nil
}
2016-06-02 08:36:02 -07:00
return backend.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`.
//
2015-11-15 18:43:34 -08:00
// The form data is as follows: `clientData` is the JSON in the `data` parameter
// (should be retrieved from ClientMessage.Arguments), `username` is AuthInfo.TwitchUsername,
// and `authenticated` is 1 or 0 depending on AuthInfo.UsernameValidated.
//
// 401 responses return an ErrAuthorizationNeeded.
//
// Non-2xx responses return the response body as an error to the client (application/json
// responses are sent as-is, non-json are sent as a JSON string).
//
// If a 2xx response has the FFZ-Cache header, its value is used as a minimum number of
// seconds to cache the response for. (Responses may be cached for longer, see
// SendRemoteCommandCached and the cache implementation.)
//
// A successful response updates the Statistics.Health.Backend map.
2016-06-02 08:36:02 -07:00
func (backend *backendInfo) SendRemoteCommand(remoteCommand, data string, auth AuthInfo) (responseStr string, err error) {
2016-06-02 08:39:01 -07:00
destURL := fmt.Sprintf("%s/cmd/%s", backend.baseURL, remoteCommand)
healthBucket := fmt.Sprintf("/cmd/%s", remoteCommand)
formData := url.Values{
"clientData": []string{data},
2015-12-16 15:51:12 -08:00
"username": []string{auth.TwitchUsername},
}
if auth.UsernameValidated {
formData.Set("authenticated", "1")
} else {
formData.Set("authenticated", "0")
}
2017-09-15 16:40:40 -07:00
sealedForm, err := backend.secureForm.Seal(formData)
if err != nil {
return "", err
}
2016-06-02 08:36:02 -07:00
resp, err := backend.HTTPClient.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
2017-08-24 11:54:56 -07:00
} else if resp.StatusCode < 200 || resp.StatusCode > 299 { // any non-2xx
// If the Content-Type header includes a charset, ignore it.
// typeStr, _, _ = mime.ParseMediaType(resp.Header.Get("Content-Type"))
// inline the part of the function we care about
typeStr := resp.Header.Get("Content-Type")
splitIdx := strings.IndexRune(typeStr, ';')
if splitIdx != -1 {
typeStr = strings.TrimSpace(typeStr[0:splitIdx])
}
if typeStr == "application/json" {
2016-01-03 13:50:33 -08:00
var err2 ErrForwardedFromBackend
err := json.Unmarshal(respBytes, &err2.JSONError)
if err != nil {
return "", fmt.Errorf("error decoding json error from backend: %v | %s", err, responseStr)
}
return "", err2
}
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
backend.responseCache.Set(
getCacheKey(remoteCommand, data),
2017-09-25 15:24:58 -07:00
responseStr,
duration,
)
}
now := time.Now().UTC()
backend.lastSuccessLock.Lock()
defer backend.lastSuccessLock.Unlock()
backend.lastSuccess[bPathOtherCommand] = now
backend.lastSuccess[healthBucket] = now
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.
2016-06-02 08:47:07 -07:00
func (backend *backendInfo) SendAggregatedData(form url.Values) error {
2017-09-15 16:40:40 -07:00
sealedForm, err := backend.secureForm.Seal(form)
2016-06-02 08:47:07 -07:00
if err != nil {
return err
}
2016-06-02 09:03:43 -07:00
resp, err := backend.HTTPClient.PostForm(backend.postStatisticsURL, sealedForm)
if err != nil {
return err
}
2016-06-04 11:43:37 -07:00
if resp.StatusCode < 200 || resp.StatusCode > 299 {
resp.Body.Close()
return httpError(resp.StatusCode)
}
backend.lastSuccessLock.Lock()
defer backend.lastSuccessLock.Unlock()
backend.lastSuccess[bPathAggStats] = time.Now().UTC()
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
2016-01-17 17:45:37 -08:00
// Error Implements the error interface.
2015-11-15 18:43:34 -08:00
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
2016-06-02 08:36:02 -07:00
func (backend *backendInfo) SendNewTopicNotice(topic string) error {
return backend.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
2016-06-02 08:36:02 -07:00
func (backend *backendInfo) SendCleanupTopicsNotice(topics []string) error {
return backend.sendTopicNotice(strings.Join(topics, ","), false)
}
2016-06-02 08:36:02 -07:00
func (backend *backendInfo) sendTopicNotice(topic string, added bool) error {
formData := url.Values{}
formData.Set("channels", topic)
if added {
formData.Set("added", "t")
} else {
formData.Set("added", "f")
}
2017-09-15 16:40:40 -07:00
sealedForm, err := backend.secureForm.Seal(formData)
if err != nil {
return err
}
2016-06-02 09:03:43 -07:00
resp, err := backend.HTTPClient.PostForm(backend.addTopicURL, sealedForm)
if err != nil {
return err
}
defer resp.Body.Close()
2016-06-04 11:43:37 -07:00
if resp.StatusCode < 200 || resp.StatusCode > 299 {
respBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
2016-07-08 12:46:16 -07:00
return ErrBackendNotOK{Code: resp.StatusCode, Response: fmt.Sprintf("(error reading non-2xx response): %s", err.Error())}
2016-06-04 11:43:37 -07:00
}
return ErrBackendNotOK{Code: resp.StatusCode, Response: string(respBytes)}
}
backend.lastSuccessLock.Lock()
defer backend.lastSuccessLock.Unlock()
backend.lastSuccess[bPathAddTopic] = time.Now().UTC()
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
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)
}
}