84f3fc5761
Before, the settings parser would automatically reset the ratelimit times to the default if you set them to zero. You could get around this by setting the ratelimits to -1, but then the parser would immediately set them to zero and write to disk. On next launch, the parser would read the zeros and then reset the values to default, so you would have to modify your ratelimits every launch.
329 lines
7.7 KiB
Go
329 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/sessions"
|
|
"github.com/zorchenhimer/MovieNight/common"
|
|
)
|
|
|
|
var settings *Settings
|
|
var sstore *sessions.CookieStore
|
|
|
|
type Settings struct {
|
|
// Non-Saved settings
|
|
filename string
|
|
cmdLineKey string // stream key from the command line
|
|
|
|
// Saved settings
|
|
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
|
|
RtmpListenAddress string // host:port that the RTMP server listens on
|
|
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
|
|
|
|
// 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
|
|
|
|
// Send the NoCache header?
|
|
NoCache bool
|
|
|
|
lock sync.RWMutex
|
|
}
|
|
|
|
type AccessMode string
|
|
|
|
const (
|
|
AccessOpen AccessMode = "open"
|
|
AccessPin AccessMode = "pin"
|
|
AccessRequest AccessMode = "request"
|
|
)
|
|
|
|
type BanInfo struct {
|
|
IP string
|
|
Names []string
|
|
When time.Time
|
|
}
|
|
|
|
func LoadSettings(filename string) (*Settings, error) {
|
|
raw, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading file: %s", err)
|
|
}
|
|
|
|
var s *Settings
|
|
err = json.Unmarshal(raw, &s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error unmarshaling: %s", err)
|
|
}
|
|
s.filename = filename
|
|
|
|
if err = common.SetupLogging(s.LogLevel, s.LogFile); err != nil {
|
|
return nil, fmt.Errorf("Unable to setup logger: %s", err)
|
|
}
|
|
|
|
// have a default of 200
|
|
if s.MaxMessageCount == 0 {
|
|
s.MaxMessageCount = 300
|
|
} else if s.MaxMessageCount < 0 {
|
|
return s, fmt.Errorf("value for MaxMessageCount must be greater than 0, given %d", s.MaxMessageCount)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Set to -1 to reset
|
|
if s.RateLimitChat == -1 {
|
|
s.RateLimitChat = 1
|
|
} else if s.RateLimitChat < 0 {
|
|
s.RateLimitChat = 0
|
|
}
|
|
|
|
if s.RateLimitNick == -1 {
|
|
s.RateLimitNick = 300
|
|
} else if s.RateLimitNick < 0 {
|
|
s.RateLimitNick = 0
|
|
}
|
|
|
|
if s.RateLimitColor == -1 {
|
|
s.RateLimitColor = 60
|
|
} else if s.RateLimitColor < 0 {
|
|
s.RateLimitColor = 0
|
|
}
|
|
|
|
if s.RateLimitAuth == -1 {
|
|
s.RateLimitAuth = 5
|
|
} else if s.RateLimitAuth < 0 {
|
|
common.LogInfoln("It's not recommended to disable the authentication rate limit.")
|
|
s.RateLimitAuth = 0
|
|
}
|
|
|
|
if s.RateLimitDuplicate == -1 {
|
|
s.RateLimitDuplicate = 30
|
|
} else if s.RateLimitDuplicate < 0 {
|
|
s.RateLimitDuplicate = 0
|
|
}
|
|
|
|
if s.WrappedEmotesOnly {
|
|
common.LogInfoln("Only allowing wrapped emotes")
|
|
common.WrappedEmotesOnly = true
|
|
}
|
|
|
|
// 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)
|
|
|
|
if len(s.RoomAccess) == 0 {
|
|
s.RoomAccess = AccessOpen
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Don't use LogInfof() here. Log isn't setup yet when LoadSettings() is called from init().
|
|
fmt.Printf("Settings reloaded. New admin password: %s\n", s.AdminPassword)
|
|
|
|
if s.TitleLength <= 0 {
|
|
s.TitleLength = 50
|
|
}
|
|
|
|
// Is this a good way to do this? Probably not...
|
|
if len(s.SessionKey) == 0 {
|
|
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)
|
|
}
|
|
s.SessionKey = out
|
|
}
|
|
|
|
// Save admin password to file
|
|
if err = s.Save(); err != nil {
|
|
return nil, fmt.Errorf("Unable to save settings: %s", err)
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
func generatePass(seed int64) (string, error) {
|
|
out := ""
|
|
for len(out) < 20 {
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(15)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
out = fmt.Sprintf("%s%X", out, num)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Settings) Save() error {
|
|
defer s.lock.Unlock()
|
|
s.lock.Lock()
|
|
|
|
return s.unlockedSave()
|
|
}
|
|
|
|
// unlockedSave expects the calling function to lock the RWMutex
|
|
func (s *Settings) unlockedSave() error {
|
|
marshaled, err := json.MarshalIndent(s, "", "\t")
|
|
if err != nil {
|
|
return fmt.Errorf("error marshaling: %s", err)
|
|
}
|
|
|
|
err = ioutil.WriteFile(s.filename, marshaled, 0777)
|
|
if err != nil {
|
|
return fmt.Errorf("error saving: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Settings) AddBan(host string, names []string) error {
|
|
defer s.lock.Unlock()
|
|
s.lock.Lock()
|
|
|
|
if host == "127.0.0.1" {
|
|
return fmt.Errorf("Cannot add a ban for localhost.")
|
|
}
|
|
|
|
b := BanInfo{
|
|
Names: names,
|
|
IP: host,
|
|
When: time.Now(),
|
|
}
|
|
s.Bans = append(s.Bans, b)
|
|
|
|
common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
|
|
|
|
return s.unlockedSave()
|
|
}
|
|
|
|
func (s *Settings) RemoveBan(name string) error {
|
|
defer s.lock.Unlock()
|
|
s.lock.Lock()
|
|
|
|
name = strings.ToLower(name)
|
|
newBans := []BanInfo{}
|
|
for _, b := range s.Bans {
|
|
for _, n := range b.Names {
|
|
if n == name {
|
|
common.LogInfof("[ban] Removed ban for %s [%s]\n", b.IP, n)
|
|
} else {
|
|
newBans = append(newBans, b)
|
|
}
|
|
}
|
|
}
|
|
s.Bans = newBans
|
|
return s.unlockedSave()
|
|
}
|
|
|
|
func (s *Settings) IsBanned(host string) (bool, []string) {
|
|
defer s.lock.RUnlock()
|
|
s.lock.RLock()
|
|
|
|
for _, b := range s.Bans {
|
|
if b.IP == host {
|
|
return true, b.Names
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (s *Settings) SetTempKey(key string) {
|
|
defer s.lock.Unlock()
|
|
s.lock.Lock()
|
|
|
|
s.cmdLineKey = key
|
|
}
|
|
|
|
func (s *Settings) GetStreamKey() string {
|
|
defer s.lock.RUnlock()
|
|
s.lock.RLock()
|
|
|
|
if len(s.cmdLineKey) > 0 {
|
|
return s.cmdLineKey
|
|
}
|
|
return s.StreamKey
|
|
}
|
|
|
|
func (s *Settings) generateNewPin() (string, error) {
|
|
defer s.lock.Unlock()
|
|
s.lock.Lock()
|
|
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
s.RoomAccessPin = fmt.Sprintf("%04d", num)
|
|
if err = s.unlockedSave(); err != nil {
|
|
return "", err
|
|
}
|
|
return s.RoomAccessPin, nil
|
|
}
|
|
|
|
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()
|
|
}
|