Merge remote-tracking branch 'origin/room-access-restrictions'

Merge in the room access restrictions changes into master.  Currently,
only the PIN/Password method is implemented, and it's not all that
secure.  Some more work needs to be done in planning before really
moving forward, but this should be fine for now.

Issue #28 should not be considered finished with this merge.
This commit is contained in:
Zorchenhimer 2019-04-04 10:17:33 -04:00
commit d102d0c5ed
7 changed files with 276 additions and 6 deletions

View File

@ -376,6 +376,19 @@ var commands = &CommandControl{
return ""
},
},
common.CNPin.String(): Command{
HelpText: "Display the current room access type and pin/password (if applicable).",
Function: func(cl *Client, args []string) string {
switch settings.RoomAccess {
case AccessPin:
return "Room is secured via PIN. Current PIN: " + settings.RoomAccessPin
case AccessRequest:
return "Room is secured via access requests. Users must request to be granted access."
}
return "Room is open access. Anybody can join."
},
},
},
admin: map[string]Command{
@ -430,6 +443,65 @@ var commands = &CommandControl{
},
},
common.CNNewPin.String(): Command{
HelpText: "Generate a room acces new pin",
Function: func(cl *Client, args []string) string {
if settings.RoomAccess != AccessPin {
return "Room is not restricted by Pin. (" + string(settings.RoomAccess) + ")"
}
pin, err := settings.generateNewPin()
if err != nil {
return "Unable to generate new pin: " + err.Error()
}
common.LogInfoln("New room access pin: ", pin)
return "New access pin: " + pin
},
},
common.CNRoomAccess.String(): Command{
HelpText: "Change the room access type.",
Function: func(cl *Client, args []string) string {
// Print current access type if no arguments given
if len(args) == 0 {
return "Current room access type: " + string(settings.RoomAccess)
}
switch AccessMode(strings.ToLower(args[0])) {
case AccessOpen:
settings.RoomAccess = AccessOpen
common.LogInfoln("[access] Room set to open")
return "Room access set to open"
case AccessPin:
// A pin/password was provided, use it.
if len(args) == 2 {
settings.RoomAccessPin = args[1]
// A pin/password was not provided, generate a new one.
} else {
_, err := settings.generateNewPin()
if err != nil {
common.LogErrorln("Error generating new access pin: ", err.Error())
return "Unable to generate a new pin, access unchanged: " + err.Error()
}
}
settings.RoomAccess = AccessPin
common.LogInfoln("[access] Room set to pin: " + settings.RoomAccessPin)
return "Room access set to Pin: " + settings.RoomAccessPin
case AccessRequest:
settings.RoomAccess = AccessRequest
common.LogInfoln("[access] Room set to request")
return "Room access set to request. WARNING: this isn't implemented yet."
default:
return "Invalid access mode"
}
},
},
common.CNIP.String(): Command{
HelpText: "List users and IP in the server console. Requires logging level to be set to info or above.",
Function: func(cl *Client, args []string) string {

View File

@ -29,6 +29,7 @@ var (
CNBan ChatCommandNames = []string{"ban"}
CNUnban ChatCommandNames = []string{"unban"}
CNPurge ChatCommandNames = []string{"purge"}
CNPin ChatCommandNames = []string{"pin", "password"}
// Admin Commands
CNMod ChatCommandNames = []string{"mod"}
CNReloadPlayer ChatCommandNames = []string{"reloadplayer"}
@ -36,6 +37,8 @@ var (
CNModpass ChatCommandNames = []string{"modpass"}
CNIP ChatCommandNames = []string{"iplist"}
CNAddEmotes ChatCommandNames = []string{"addemotes"}
CNNewPin ChatCommandNames = []string{"newpin", "newpassword"}
CNRoomAccess ChatCommandNames = []string{"changeaccess", "hodor"}
)
var ChatCommands = []ChatCommandNames{
@ -50,6 +53,7 @@ var ChatCommands = []ChatCommandNames{
CNNick,
// Mod
CNSv, CNPlaying, CNUnmod, CNKick, CNBan, CNUnban, CNPurge, CNPin,
CNSv,
CNPlaying,
CNUnmod,
@ -59,6 +63,7 @@ var ChatCommands = []ChatCommandNames{
CNPurge,
// Admin
CNMod, CNReloadPlayer, CNReloadEmotes, CNModpass, CNRoomAccess, CNIP,
CNMod,
CNReloadPlayer,
CNReloadEmotes,

View File

@ -1,6 +1,7 @@
package main
import (
"fmt"
"html/template"
"io"
"net/http"
@ -154,6 +155,93 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
}()
}
// returns if it's OK to proceed
func checkRoomAccess(w http.ResponseWriter, r *http.Request) bool {
session, err := sstore.Get(r, "moviesession")
if err != nil {
// Don't return as server error here, just make a new session.
common.LogErrorf("Unable to get session for client %s: %v\n", r.RemoteAddr, err)
}
if settings.RoomAccess == AccessPin {
pin := session.Values["pin"]
// No pin found in session
if pin == nil || len(pin.(string)) == 0 {
if r.Method == "POST" {
// Check for correct pin
err = r.ParseForm()
if err != nil {
common.LogErrorf("Error parsing form")
http.Error(w, "Unable to get session data", http.StatusInternalServerError)
}
postPin := r.Form.Get("txtInput")
common.LogDebugf("Received pin: %s\n", postPin)
if postPin == settings.RoomAccessPin {
// Pin is correct. Save it to session and return true.
session.Values["pin"] = settings.RoomAccessPin
session.Save(r, w)
return true
}
// Pin is incorrect.
handlePinTemplate(w, r, "Incorrect PIN")
return false
}
// nope. display pin entry and return
handlePinTemplate(w, r, "")
return false
}
// Pin found in session, but it has changed since last time.
if pin.(string) != settings.RoomAccessPin {
// Clear out the old pin.
session.Values["pin"] = nil
session.Save(r, w)
// Prompt for new one.
handlePinTemplate(w, r, "Pin has changed. Enter new PIN.")
return false
}
// Correct pin found in session
return true
}
// TODO: this.
if settings.RoomAccess == AccessRequest {
http.Error(w, "Requesting access not implemented yet", http.StatusNotImplemented)
return false
}
// Room is open.
return true
}
func handlePinTemplate(w http.ResponseWriter, r *http.Request, errorMessage string) {
t, err := template.ParseFiles("./static/base.html", "./static/thedoor.html")
if err != nil {
common.LogErrorf("Error parsing template file: %v", err)
return
}
type Data struct {
Title string
SubmitText string
Error string
}
data := Data{
Title: "Enter Pin",
SubmitText: "Submit Pin",
Error: errorMessage,
}
err = t.Execute(w, data)
if err != nil {
common.LogErrorf("Error executing file, %v", err)
}
}
func handleHelpTemplate(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("./static/base.html", "./static/help.html")
if err != nil {
@ -187,7 +275,36 @@ func handleHelpTemplate(w http.ResponseWriter, r *http.Request) {
}
}
func handlePin(w http.ResponseWriter, r *http.Request) {
session, err := sstore.Get(r, "moviesession")
if err != nil {
common.LogDebugf("Unable to get session: %v\n", err)
}
val := session.Values["pin"]
if val == nil {
session.Values["pin"] = "1234"
err := session.Save(r, w)
if err != nil {
fmt.Fprintf(w, "unable to save session: %v", err)
}
fmt.Fprint(w, "Pin was not set")
common.LogDebugln("pin was not set")
} else {
fmt.Fprintf(w, "pin set: %v", val)
common.LogDebugf("pin is set: %v\n", val)
}
}
func handleIndexTemplate(w http.ResponseWriter, r *http.Request) {
if settings.RoomAccess != AccessOpen {
if !checkRoomAccess(w, r) {
common.LogDebugln("Denied access")
return
}
common.LogDebugln("Granted access")
}
t, err := template.ParseFiles("./static/base.html", "./static/main.html")
if err != nil {
common.LogErrorf("Error parsing template file, %v\n", err)

12
main.go
View File

@ -7,6 +7,7 @@ import (
"os"
"os/signal"
"github.com/gorilla/sessions"
"github.com/nareix/joy4/format"
"github.com/nareix/joy4/format/rtmp"
"github.com/zorchenhimer/MovieNight/common"
@ -28,9 +29,11 @@ func setupSettings() error {
return fmt.Errorf("Missing stream key is settings.json")
}
// Save admin password to file
if err = settings.Save(); err != nil {
return fmt.Errorf("Unable to save settings: %s", err)
sstore = sessions.NewCookieStore([]byte(settings.SessionKey))
sstore.Options = &sessions.Options{
Path: "/",
MaxAge: 60 * 60 * 24, // one day
SameSite: http.SameSiteStrictMode,
}
return nil
@ -71,6 +74,8 @@ func main() {
common.LogInfoln("Stream key: ", settings.GetStreamKey())
common.LogInfoln("Admin password: ", settings.AdminPassword)
common.LogInfoln("Listen and serve ", addr)
common.LogInfoln("RoomAccess: ", settings.RoomAccess)
common.LogInfoln("RoomAccessPin: ", settings.RoomAccessPin)
go startServer()
go startRmtpServer()
@ -102,6 +107,7 @@ func startServer() {
http.HandleFunc("/chat", handleIndexTemplate)
http.HandleFunc("/video", handleIndexTemplate)
http.HandleFunc("/help", handleHelpTemplate)
http.HandleFunc("/pin", handlePin)
http.HandleFunc("/", handleDefault)

View File

@ -10,11 +10,13 @@ import (
"sync"
"time"
"github.com/gorilla/sessions"
"github.com/zorchenhimer/MovieNight/common"
)
var settings *Settings
var settingsMtx sync.Mutex
var sstore *sessions.CookieStore
type Settings struct {
// Non-Saved settings
@ -29,9 +31,12 @@ type Settings struct {
StreamKey string
ListenAddress string
ApprovedEmotes []EmoteSet // list of channels that have been approved for emote use. Global emotes are always "approved".
SessionKey string // key for session data
Bans []BanInfo
LogLevel common.LogLevel
LogFile string
RoomAccess AccessMode
RoomAccessPin string // auto generate this,
// Rate limiting stuff, in seconds
RateLimitChat time.Duration
@ -44,6 +49,14 @@ type Settings struct {
NoCache bool
}
type AccessMode string
const (
AccessOpen AccessMode = "open"
AccessPin AccessMode = "pin"
AccessRequest AccessMode = "request"
)
type BanInfo struct {
IP string
Names []string
@ -115,6 +128,14 @@ func LoadSettings(filename string) (*Settings, error) {
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.RoomAccessPin = "1234"
}
// 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)
@ -122,6 +143,26 @@ func LoadSettings(filename string) (*Settings, error) {
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
}
@ -161,11 +202,11 @@ func (s *Settings) AddBan(host string, names []string) error {
IP: host,
When: time.Now(),
}
settings.Bans = append(settings.Bans, b)
s.Bans = append(s.Bans, b)
common.LogInfof("[BAN] %q (%s) has been banned.\n", strings.Join(names, ", "), host)
return settings.Save()
return s.Save()
}
func (s *Settings) RemoveBan(name string) error {
@ -184,7 +225,7 @@ func (s *Settings) RemoveBan(name string) error {
}
}
s.Bans = newBans
return settings.Save()
return s.Save()
}
func (s *Settings) IsBanned(host string) (bool, []string) {
@ -215,3 +256,15 @@ func (s *Settings) GetStreamKey() string {
}
return s.StreamKey
}
func (s *Settings) generateNewPin() (string, error) {
num, err := rand.Int(rand.Reader, big.NewInt(int64(9999)))
if err != nil {
return "", err
}
s.RoomAccessPin = fmt.Sprintf("%04d", num)
if err = s.Save(); err != nil {
return "", err
}
return s.RoomAccessPin, nil
}

View File

@ -211,6 +211,10 @@ input[type=text] {
text-align: center;
}
#accessRequest div {
margin-bottom: 5px;
}
#playing {
color: #288a85;
font-size: x-Large;

13
static/thedoor.html Normal file
View File

@ -0,0 +1,13 @@
{{define "header"}}
{{end}}
{{define "body"}}
<div>
{{if .Error}}<div>{{.Error}}</div>{{end}}
<form action="/" method="post">
<input type="text" name="txtInput" /><br />
<input type="submit" value="{{.SubmitText}}" />
</form>
</div>
{{end}}