2015-10-25 00:44:25 -07:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2015-10-26 10:06:45 -07:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2015-11-08 16:44:16 -08:00
|
|
|
"errors"
|
2015-10-25 00:44:25 -07:00
|
|
|
"fmt"
|
2015-11-08 16:44:16 -08:00
|
|
|
"github.com/gorilla/websocket"
|
2015-10-25 00:44:25 -07:00
|
|
|
"github.com/pmylund/go-cache"
|
2015-10-26 10:06:45 -07:00
|
|
|
"golang.org/x/crypto/nacl/box"
|
2015-10-25 00:44:25 -07:00
|
|
|
"io/ioutil"
|
2015-10-25 03:21:50 -07:00
|
|
|
"log"
|
2015-10-26 10:06:45 -07:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
2015-10-25 12:40:07 -07:00
|
|
|
"strings"
|
2015-10-26 10:06:45 -07:00
|
|
|
"sync"
|
|
|
|
"time"
|
2015-10-25 00:44:25 -07:00
|
|
|
)
|
|
|
|
|
2015-11-15 18:43:34 -08:00
|
|
|
var backendHTTPClient http.Client
|
|
|
|
var backendURL string
|
2015-10-25 00:44:25 -07:00
|
|
|
var responseCache *cache.Cache
|
|
|
|
|
2015-11-15 18:43:34 -08:00
|
|
|
var getBacklogURL string
|
|
|
|
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
|
|
|
getBacklogURL = fmt.Sprintf("%s/backlog", backendURL)
|
|
|
|
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
|
2015-10-26 22:16:03 -07:00
|
|
|
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)
|
2015-10-25 00:44:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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-10-26 10:06:45 -07:00
|
|
|
func HBackendPublishRequest(w http.ResponseWriter, r *http.Request) {
|
2015-10-26 11:22:06 -07:00
|
|
|
r.ParseForm()
|
2015-10-26 08:58:04 -07:00
|
|
|
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")
|
2015-10-26 10:07:15 -07:00
|
|
|
channel := formData.Get("channel")
|
|
|
|
scope := formData.Get("scope")
|
2015-10-26 10:23:53 -07:00
|
|
|
|
|
|
|
target := MessageTargetTypeByName(scope)
|
|
|
|
|
|
|
|
if cmd == "" {
|
|
|
|
w.WriteHeader(422)
|
|
|
|
fmt.Fprintf(w, "Error: cmd cannot be blank")
|
|
|
|
return
|
|
|
|
}
|
2015-10-26 12:13:28 -07:00
|
|
|
if channel == "" && (target == MsgTargetTypeChat || target == MsgTargetTypeMultichat) {
|
2015-10-26 10:23:53 -07:00
|
|
|
w.WriteHeader(422)
|
|
|
|
fmt.Fprintf(w, "Error: channel must be specified")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-10-26 08:58:04 -07:00
|
|
|
cm := ClientMessage{MessageID: -1, Command: Command(cmd), origArguments: json}
|
2015-10-26 11:22:06 -07:00
|
|
|
cm.parseOrigArguments()
|
2015-10-26 08:58:04 -07:00
|
|
|
var count int
|
2015-10-26 10:23:53 -07:00
|
|
|
|
|
|
|
switch target {
|
|
|
|
case MsgTargetTypeSingle:
|
2015-11-15 18:43:34 -08:00
|
|
|
// TODO
|
2015-10-26 10:23:53 -07:00
|
|
|
case MsgTargetTypeChat:
|
2015-11-08 22:34:06 -08:00
|
|
|
count = PublishToChannel(channel, cm)
|
2015-10-26 10:23:53 -07:00
|
|
|
case MsgTargetTypeMultichat:
|
2015-11-08 22:01:32 -08:00
|
|
|
count = PublishToMultiple(strings.Split(channel, ","), cm)
|
2015-10-26 10:23:53 -07:00
|
|
|
case MsgTargetTypeGlobal:
|
2015-10-26 10:07:15 -07:00
|
|
|
count = PublishToAll(cm)
|
2015-10-26 10:23:53 -07:00
|
|
|
case MsgTargetTypeInvalid:
|
|
|
|
default:
|
|
|
|
w.WriteHeader(422)
|
|
|
|
fmt.Fprint(w, "Invalid 'scope'. must be single, chat, multichat, channel, or global")
|
2015-10-26 08:58:04 -07:00
|
|
|
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-04 12:25:42 -08:00
|
|
|
|
2015-11-15 18:43:34 -08:00
|
|
|
func (bfe ErrForwardedFromBackend) Error() string {
|
2015-11-04 12:25:42 -08:00
|
|
|
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) {
|
2015-10-25 00:44:25 -07:00
|
|
|
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-10-25 00:44:25 -07:00
|
|
|
}
|
|
|
|
|
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)
|
2015-10-25 00:44:25 -07:00
|
|
|
var authKey string
|
|
|
|
if auth.UsernameValidated {
|
|
|
|
authKey = "usernameClaimed"
|
|
|
|
} else {
|
|
|
|
authKey = "username"
|
|
|
|
}
|
|
|
|
|
|
|
|
formData := url.Values{
|
|
|
|
"clientData": []string{data},
|
2015-10-26 10:06:45 -07:00
|
|
|
authKey: []string{auth.TwitchUsername},
|
2015-10-25 00:44:25 -07:00
|
|
|
}
|
|
|
|
|
2015-10-25 20:17:17 -07:00
|
|
|
sealedForm, err := SealRequest(formData)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2015-11-15 18:43:34 -08:00
|
|
|
resp, err := backendHTTPClient.PostForm(destURL, sealedForm)
|
2015-10-25 00:44:25 -07:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2015-10-31 23:16:58 -07:00
|
|
|
defer resp.Body.Close()
|
2015-10-25 00:44:25 -07:00
|
|
|
|
|
|
|
respBytes, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
2015-10-25 12:40:07 -07:00
|
|
|
responseStr = string(respBytes)
|
2015-10-25 00:44:25 -07:00
|
|
|
|
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 {
|
2015-11-04 12:25:42 -08:00
|
|
|
if resp.Header.Get("Content-Type") == "application/json" {
|
2015-11-15 18:43:34 -08:00
|
|
|
return "", ErrForwardedFromBackend(responseStr)
|
2015-11-04 12:25:42 -08:00
|
|
|
}
|
2015-11-15 18:43:34 -08:00
|
|
|
return "", httpError(resp.StatusCode)
|
2015-11-04 12:25:42 -08:00
|
|
|
}
|
|
|
|
|
2015-10-25 00:44:25 -07:00
|
|
|
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 00:44:25 -07:00
|
|
|
}
|
|
|
|
|
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)
|
2015-10-28 15:19:22 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2015-10-31 23:16:58 -07:00
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
resp.Body.Close()
|
|
|
|
return httpError(resp.StatusCode)
|
|
|
|
}
|
2015-10-28 15:19:22 -07:00
|
|
|
|
|
|
|
return resp.Body.Close()
|
|
|
|
}
|
|
|
|
|
2015-11-16 12:50:00 -08:00
|
|
|
// FetchBacklogData makes a request to the backend for backlog data on a set of pub/sub topics.
|
|
|
|
// TODO scrap this, replaced by /cached_pub
|
2015-10-26 12:13:28 -07:00
|
|
|
func FetchBacklogData(chatSubs []string) ([]ClientMessage, error) {
|
2015-10-25 03:21:50 -07:00
|
|
|
formData := url.Values{
|
2015-10-26 14:55:20 -07:00
|
|
|
"subs": chatSubs,
|
2015-10-25 03:21:50 -07:00
|
|
|
}
|
|
|
|
|
2015-10-25 20:17:17 -07:00
|
|
|
sealedForm, err := SealRequest(formData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-11-15 18:43:34 -08:00
|
|
|
resp, err := backendHTTPClient.PostForm(getBacklogURL, sealedForm)
|
2015-10-25 03:21:50 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-10-31 23:16:58 -07:00
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
|
|
return nil, httpError(resp.StatusCode)
|
|
|
|
}
|
2015-10-25 03:21:50 -07:00
|
|
|
dec := json.NewDecoder(resp.Body)
|
2015-11-04 15:11:49 -08:00
|
|
|
var messageStrings []string
|
|
|
|
err = dec.Decode(messageStrings)
|
2015-10-25 03:21:50 -07:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-11-04 15:11:49 -08:00
|
|
|
var messages = make([]ClientMessage, len(messageStrings))
|
|
|
|
for i, str := range messageStrings {
|
|
|
|
UnmarshalClientMessage([]byte(str), websocket.TextMessage, &messages[i])
|
|
|
|
}
|
|
|
|
|
2015-10-25 03:21:50 -07:00
|
|
|
return messages, nil
|
2015-11-04 15:11:49 -08:00
|
|
|
}
|
|
|
|
|
2015-11-15 18:43:34 -08:00
|
|
|
// ErrBackendNotOK indicates that the backend replied with something other than the string "ok".
|
|
|
|
type ErrBackendNotOK struct {
|
2015-11-04 15:11:49 -08:00
|
|
|
Response string
|
2015-11-08 16:44:16 -08:00
|
|
|
Code int
|
2015-11-04 15:11:49 -08:00
|
|
|
}
|
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 {
|
2015-11-04 15:11:49 -08:00
|
|
|
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
|
2015-11-04 15:11:49 -08:00
|
|
|
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
|
2015-11-04 15:11:49 -08:00
|
|
|
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)
|
2015-11-04 15:11:49 -08:00
|
|
|
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}
|
2015-11-04 15:11:49 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2015-10-25 12:40:07 -07:00
|
|
|
}
|
|
|
|
|
2015-10-31 23:16:58 -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
|
2015-10-27 18:22:56 -07:00
|
|
|
output := ConfigFile{
|
2015-10-27 21:21:06 -07:00
|
|
|
ListenAddr: "0.0.0.0:8001",
|
2015-10-27 18:22:56 -07:00
|
|
|
SocketOrigin: "localhost:8001",
|
2015-11-15 18:43:34 -08:00
|
|
|
BackendURL: "http://localhost:8002/ffz",
|
2015-10-27 18:22:56 -07:00
|
|
|
}
|
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)
|
|
|
|
}
|
2015-10-26 22:16:03 -07:00
|
|
|
output.BackendPublicKey = theirPublic
|
2015-10-25 12:40:07 -07:00
|
|
|
}
|
|
|
|
|
2015-10-26 22:16:03 -07:00
|
|
|
bytes, err := json.MarshalIndent(output, "", "\t")
|
2015-10-25 12:40:07 -07:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2015-10-26 22:16:03 -07:00
|
|
|
fmt.Println(string(bytes))
|
|
|
|
err = ioutil.WriteFile(outputFile, bytes, 0600)
|
2015-10-25 12:40:07 -07:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|