2019-03-10 16:42:12 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-03-13 21:47:32 +01:00
|
|
|
"crypto/rand"
|
2019-03-10 16:42:12 +01:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2019-03-13 21:47:32 +01:00
|
|
|
"math/big"
|
2019-03-10 16:42:12 +01:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
2019-03-23 02:39:55 +01:00
|
|
|
|
|
|
|
"github.com/gorilla/sessions"
|
2019-03-24 23:51:39 +01:00
|
|
|
"github.com/zorchenhimer/MovieNight/common"
|
2019-03-10 16:42:12 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
var settings *Settings
|
2019-03-23 02:39:55 +01:00
|
|
|
var sstore *sessions.CookieStore
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
type Settings struct {
|
2019-03-21 21:20:50 +01:00
|
|
|
// Non-Saved settings
|
|
|
|
filename string
|
|
|
|
cmdLineKey string // stream key from the command line
|
|
|
|
|
|
|
|
// Saved settings
|
2020-06-04 09:32:01 +02:00
|
|
|
AdminPassword string
|
|
|
|
ApprovedEmotes []string // list of channels that have been approved for emote use. Global emotes are always "approved".
|
|
|
|
Bans []BanInfo
|
|
|
|
LetThemLurk bool // whether or not to announce users joining/leaving chat
|
|
|
|
ListenAddress string
|
|
|
|
LogFile string
|
|
|
|
LogLevel common.LogLevel
|
|
|
|
MaxMessageCount int
|
|
|
|
NewPin bool // Auto generate a new pin on start. Overwrites RoomAccessPin if set.
|
|
|
|
PageTitle string // primary value for the page <title> element
|
|
|
|
RegenAdminPass bool // regenerate admin password on start?
|
|
|
|
RoomAccess AccessMode
|
|
|
|
RoomAccessPin string // The current pin
|
2020-06-13 05:58:45 +02:00
|
|
|
RtmpListenAddress string // host:port that the RTMP server listens on
|
2020-06-04 09:32:01 +02:00
|
|
|
SessionKey string // key for session data
|
|
|
|
StreamKey string
|
|
|
|
StreamStats bool
|
|
|
|
TitleLength int // maximum length of the title that can be set with the /playing
|
|
|
|
TwitchClientID string // client id from twitch developers portal
|
|
|
|
TwitchClientSecret string // OAuth from twitch developers portal: https://dev.twitch.tv/docs/authentication/getting-tokens-oauth#oauth-client-credentials-flow
|
|
|
|
WrappedEmotesOnly bool // only allow "wrapped" emotes. eg :Kappa: and [Kappa] but not Kappa
|
2020-04-19 18:26:27 +02:00
|
|
|
|
2019-03-30 20:39:04 +01:00
|
|
|
// Rate limiting stuff, in seconds
|
|
|
|
RateLimitChat time.Duration
|
|
|
|
RateLimitNick time.Duration
|
|
|
|
RateLimitColor time.Duration
|
|
|
|
RateLimitAuth time.Duration
|
|
|
|
RateLimitDuplicate time.Duration // Amount of seconds between allowed duplicate messages
|
|
|
|
|
2019-03-30 19:21:46 +01:00
|
|
|
// Send the NoCache header?
|
|
|
|
NoCache bool
|
2020-01-30 20:32:46 +01:00
|
|
|
|
|
|
|
lock sync.RWMutex
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
2019-03-23 02:39:55 +01:00
|
|
|
type AccessMode string
|
|
|
|
|
|
|
|
const (
|
|
|
|
AccessOpen AccessMode = "open"
|
|
|
|
AccessPin AccessMode = "pin"
|
|
|
|
AccessRequest AccessMode = "request"
|
|
|
|
)
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
type BanInfo struct {
|
|
|
|
IP string
|
|
|
|
Names []string
|
|
|
|
When time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
func LoadSettings(filename string) (*Settings, error) {
|
|
|
|
raw, err := ioutil.ReadFile(filename)
|
|
|
|
if err != nil {
|
2019-03-14 20:19:36 +01:00
|
|
|
return nil, fmt.Errorf("error reading file: %s", err)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var s *Settings
|
|
|
|
err = json.Unmarshal(raw, &s)
|
|
|
|
if err != nil {
|
2019-03-14 20:19:36 +01:00
|
|
|
return nil, fmt.Errorf("error unmarshaling: %s", err)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
s.filename = filename
|
|
|
|
|
2020-08-23 17:54:01 +02:00
|
|
|
var logFileDir string = common.RunPath() + "/" + s.LogFile
|
2020-08-23 00:05:30 +02:00
|
|
|
fmt.Printf("Log file: %s\n", logFileDir)
|
|
|
|
if err = common.SetupLogging(s.LogLevel, logFileDir); err != nil {
|
2019-03-30 19:23:04 +01:00
|
|
|
return nil, fmt.Errorf("Unable to setup logger: %s", err)
|
|
|
|
}
|
|
|
|
|
2019-03-13 05:02:36 +01:00
|
|
|
// have a default of 200
|
|
|
|
if s.MaxMessageCount == 0 {
|
|
|
|
s.MaxMessageCount = 300
|
|
|
|
} else if s.MaxMessageCount < 0 {
|
2019-03-14 20:19:36 +01:00
|
|
|
return s, fmt.Errorf("value for MaxMessageCount must be greater than 0, given %d", s.MaxMessageCount)
|
2019-03-13 05:02:36 +01:00
|
|
|
}
|
|
|
|
|
2020-04-18 22:02:41 +02:00
|
|
|
if s.RegenAdminPass == true || s.AdminPassword == "" {
|
|
|
|
s.AdminPassword, err = generatePass(time.Now().Unix())
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to generate admin password: %s", err)
|
|
|
|
}
|
2019-03-13 21:47:32 +01:00
|
|
|
}
|
2019-03-24 23:51:39 +01:00
|
|
|
|
2020-08-10 01:36:22 +02:00
|
|
|
// Set to -1 to reset
|
2019-03-30 20:39:04 +01:00
|
|
|
if s.RateLimitChat == -1 {
|
|
|
|
s.RateLimitChat = 1
|
2020-08-10 01:36:22 +02:00
|
|
|
} else if s.RateLimitChat < 0 {
|
|
|
|
s.RateLimitChat = 0
|
2019-03-30 20:39:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if s.RateLimitNick == -1 {
|
|
|
|
s.RateLimitNick = 300
|
2020-08-10 01:36:22 +02:00
|
|
|
} else if s.RateLimitNick < 0 {
|
|
|
|
s.RateLimitNick = 0
|
2019-03-30 20:39:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if s.RateLimitColor == -1 {
|
|
|
|
s.RateLimitColor = 60
|
2020-08-10 01:36:22 +02:00
|
|
|
} else if s.RateLimitColor < 0 {
|
|
|
|
s.RateLimitColor = 0
|
2019-03-30 20:39:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if s.RateLimitAuth == -1 {
|
|
|
|
s.RateLimitAuth = 5
|
2020-08-10 01:36:22 +02:00
|
|
|
} else if s.RateLimitAuth < 0 {
|
|
|
|
common.LogInfoln("It's not recommended to disable the authentication rate limit.")
|
|
|
|
s.RateLimitAuth = 0
|
2019-03-30 20:39:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if s.RateLimitDuplicate == -1 {
|
|
|
|
s.RateLimitDuplicate = 30
|
2020-08-10 01:36:22 +02:00
|
|
|
} else if s.RateLimitDuplicate < 0 {
|
|
|
|
s.RateLimitDuplicate = 0
|
2019-03-30 20:39:04 +01:00
|
|
|
}
|
|
|
|
|
2020-04-19 18:26:27 +02:00
|
|
|
if s.WrappedEmotesOnly {
|
|
|
|
common.LogInfoln("Only allowing wrapped emotes")
|
|
|
|
common.WrappedEmotesOnly = true
|
|
|
|
}
|
|
|
|
|
2019-03-30 20:39:04 +01:00
|
|
|
// Print this stuff before we multiply it by time.Second
|
|
|
|
common.LogInfof("RateLimitChat: %v", s.RateLimitChat)
|
|
|
|
common.LogInfof("RateLimitNick: %v", s.RateLimitNick)
|
|
|
|
common.LogInfof("RateLimitColor: %v", s.RateLimitColor)
|
|
|
|
common.LogInfof("RateLimitAuth: %v", s.RateLimitAuth)
|
|
|
|
|
2019-03-30 22:46:31 +01:00
|
|
|
if len(s.RoomAccess) == 0 {
|
|
|
|
s.RoomAccess = AccessOpen
|
2019-03-30 21:04:06 +01:00
|
|
|
}
|
|
|
|
|
2019-04-15 17:22:53 +02:00
|
|
|
if (s.RoomAccess != AccessOpen && len(s.RoomAccessPin) == 0) || s.NewPin {
|
|
|
|
pin, err := s.generateNewPin()
|
|
|
|
if err != nil {
|
|
|
|
common.LogErrorf("Unable to generate new pin: %v", err)
|
|
|
|
}
|
|
|
|
common.LogInfof("New pin generated: %s", pin)
|
2019-03-30 21:04:06 +01:00
|
|
|
}
|
|
|
|
|
2019-03-24 23:51:39 +01:00
|
|
|
// Don't use LogInfof() here. Log isn't setup yet when LoadSettings() is called from init().
|
2019-03-10 16:42:12 +01:00
|
|
|
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)
|
|
|
|
|
2019-03-24 23:51:39 +01:00
|
|
|
if s.TitleLength <= 0 {
|
|
|
|
s.TitleLength = 50
|
|
|
|
}
|
|
|
|
|
2019-03-30 21:04:06 +01:00
|
|
|
// Is this a good way to do this? Probably not...
|
2019-03-30 22:46:31 +01:00
|
|
|
if len(s.SessionKey) == 0 {
|
2019-03-30 21:04:06 +01:00
|
|
|
out := ""
|
|
|
|
large := big.NewInt(int64(1 << 60))
|
|
|
|
large = large.Add(large, large)
|
|
|
|
for len(out) < 50 {
|
|
|
|
num, err := rand.Int(rand.Reader, large)
|
|
|
|
if err != nil {
|
|
|
|
panic("Error generating session key: " + err.Error())
|
|
|
|
}
|
|
|
|
out = fmt.Sprintf("%s%X", out, num)
|
|
|
|
}
|
2019-03-30 22:46:31 +01:00
|
|
|
s.SessionKey = out
|
2019-03-30 21:04:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Save admin password to file
|
2019-03-30 22:46:31 +01:00
|
|
|
if err = s.Save(); err != nil {
|
2019-03-30 21:04:06 +01:00
|
|
|
return nil, fmt.Errorf("Unable to save settings: %s", err)
|
|
|
|
}
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
2019-03-13 21:47:32 +01:00
|
|
|
func generatePass(seed int64) (string, error) {
|
2019-03-10 16:42:12 +01:00
|
|
|
out := ""
|
|
|
|
for len(out) < 20 {
|
2019-03-13 21:47:32 +01:00
|
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(15)))
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
out = fmt.Sprintf("%s%X", out, num)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
2019-03-13 21:47:32 +01:00
|
|
|
return out, nil
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Settings) Save() error {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.Unlock()
|
|
|
|
s.lock.Lock()
|
|
|
|
|
|
|
|
return s.unlockedSave()
|
|
|
|
}
|
|
|
|
|
|
|
|
// unlockedSave expects the calling function to lock the RWMutex
|
|
|
|
func (s *Settings) unlockedSave() error {
|
2019-03-12 04:05:01 +01:00
|
|
|
marshaled, err := json.MarshalIndent(s, "", "\t")
|
2019-03-10 16:42:12 +01:00
|
|
|
if err != nil {
|
2019-03-14 20:19:36 +01:00
|
|
|
return fmt.Errorf("error marshaling: %s", err)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
err = ioutil.WriteFile(s.filename, marshaled, 0777)
|
|
|
|
if err != nil {
|
2019-03-14 20:19:36 +01:00
|
|
|
return fmt.Errorf("error saving: %s", err)
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Settings) AddBan(host string, names []string) error {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.Unlock()
|
|
|
|
s.lock.Lock()
|
|
|
|
|
2019-03-20 21:57:29 +01:00
|
|
|
if host == "127.0.0.1" {
|
|
|
|
return fmt.Errorf("Cannot add a ban for localhost.")
|
|
|
|
}
|
|
|
|
|
2019-03-10 16:42:12 +01:00
|
|
|
b := BanInfo{
|
|
|
|
Names: names,
|
|
|
|
IP: host,
|
|
|
|
When: time.Now(),
|
|
|
|
}
|
2019-03-30 22:46:31 +01:00
|
|
|
s.Bans = append(s.Bans, b)
|
2019-03-10 16:42:12 +01:00
|
|
|
|
2019-03-24 23:51:39 +01:00
|
|
|
common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
|
2019-03-10 16:42:12 +01:00
|
|
|
|
2020-01-30 20:32:46 +01:00
|
|
|
return s.unlockedSave()
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Settings) RemoveBan(name string) error {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.Unlock()
|
|
|
|
s.lock.Lock()
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
name = strings.ToLower(name)
|
|
|
|
newBans := []BanInfo{}
|
|
|
|
for _, b := range s.Bans {
|
|
|
|
for _, n := range b.Names {
|
|
|
|
if n == name {
|
2019-03-24 23:51:39 +01:00
|
|
|
common.LogInfof("[ban] Removed ban for %s [%s]\n", b.IP, n)
|
2019-03-10 16:42:12 +01:00
|
|
|
} else {
|
|
|
|
newBans = append(newBans, b)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
s.Bans = newBans
|
2020-01-30 20:32:46 +01:00
|
|
|
return s.unlockedSave()
|
2019-03-10 16:42:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Settings) IsBanned(host string) (bool, []string) {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.RUnlock()
|
|
|
|
s.lock.RLock()
|
2019-03-10 16:42:12 +01:00
|
|
|
|
|
|
|
for _, b := range s.Bans {
|
|
|
|
if b.IP == host {
|
|
|
|
return true, b.Names
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
2019-03-12 04:45:23 +01:00
|
|
|
|
|
|
|
func (s *Settings) SetTempKey(key string) {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.Unlock()
|
|
|
|
s.lock.Lock()
|
2019-03-12 04:45:23 +01:00
|
|
|
|
|
|
|
s.cmdLineKey = key
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Settings) GetStreamKey() string {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.RUnlock()
|
|
|
|
s.lock.RLock()
|
2019-03-12 04:45:23 +01:00
|
|
|
|
|
|
|
if len(s.cmdLineKey) > 0 {
|
|
|
|
return s.cmdLineKey
|
|
|
|
}
|
|
|
|
return s.StreamKey
|
|
|
|
}
|
2019-03-23 02:39:55 +01:00
|
|
|
|
|
|
|
func (s *Settings) generateNewPin() (string, error) {
|
2020-01-30 20:32:46 +01:00
|
|
|
defer s.lock.Unlock()
|
|
|
|
s.lock.Lock()
|
|
|
|
|
2019-03-23 02:39:55 +01:00
|
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2019-03-30 22:46:31 +01:00
|
|
|
s.RoomAccessPin = fmt.Sprintf("%04d", num)
|
2020-01-30 20:32:46 +01:00
|
|
|
if err = s.unlockedSave(); err != nil {
|
2019-03-23 23:08:22 +01:00
|
|
|
return "", err
|
|
|
|
}
|
2019-03-30 22:46:31 +01:00
|
|
|
return s.RoomAccessPin, nil
|
2019-03-23 02:39:55 +01:00
|
|
|
}
|
2020-01-30 20:32:46 +01:00
|
|
|
|
|
|
|
func (s *Settings) AddApprovedEmotes(channels []string) error {
|
|
|
|
defer s.lock.Unlock()
|
|
|
|
s.lock.Lock()
|
|
|
|
|
|
|
|
approved := map[string]int{}
|
|
|
|
for _, e := range s.ApprovedEmotes {
|
|
|
|
approved[e] = 1
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, name := range channels {
|
|
|
|
approved[name] = 1
|
|
|
|
}
|
|
|
|
|
|
|
|
filtered := []string{}
|
|
|
|
for key, _ := range approved {
|
|
|
|
filtered = append(filtered, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
s.ApprovedEmotes = filtered
|
|
|
|
return s.unlockedSave()
|
|
|
|
}
|