MovieNight/handlers.go
Zorchenhimer a73375f152 Fix emotes
This reworks how emotes are cached in relation to their physical
location on disk.  Functionally, they should be the same from the user
perspective but it sets up some stuff that will make it easier to add
emotes from various sources.
2019-06-18 22:13:53 -04:00

428 lines
9.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"path/filepath"
"strings"
"sync"
"github.com/zorchenhimer/MovieNight/common"
"github.com/gorilla/websocket"
"github.com/nareix/joy4/av/avutil"
"github.com/nareix/joy4/av/pubsub"
"github.com/nareix/joy4/format/flv"
"github.com/nareix/joy4/format/rtmp"
)
var (
//global variable for handling all chat traffic
chat *ChatRoom
// Read/Write mutex for rtmp stream
l = &sync.RWMutex{}
// Map of active streams
channels = map[string]*Channel{}
)
type Channel struct {
que *pubsub.Queue
}
type writeFlusher struct {
httpflusher http.Flusher
io.Writer
}
func (self writeFlusher) Flush() error {
self.httpflusher.Flush()
return nil
}
// Serving static files
func wsStaticFiles(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/favicon.ico":
http.ServeFile(w, r, "./favicon.png")
return
case "/justchat":
http.ServeFile(w, r, "./static/justchat.html")
return
case "/justvideo":
http.ServeFile(w, r, "./static/justvideo.html")
return
}
goodPath := r.URL.Path[8:len(r.URL.Path)]
common.LogDebugf("[static] serving %q from folder ./static/\n", goodPath)
http.ServeFile(w, r, "./static/"+goodPath)
}
func wsWasmFile(w http.ResponseWriter, r *http.Request) {
if settings.NoCache {
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
}
common.LogDebugln("[static] serving wasm file")
http.ServeFile(w, r, "./static/main.wasm")
}
func wsImages(w http.ResponseWriter, r *http.Request) {
base := filepath.Base(r.URL.Path)
common.LogDebugln("[img] ", base)
http.ServeFile(w, r, "./static/img/"+base)
}
func wsEmotes(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, path.Join("./static/", r.URL.Path))
}
// Handling the websocket
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true }, //not checking origin
}
//this is also the handler for joining to the chat
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
common.LogErrorln("Error upgrading to websocket:", err)
return
}
common.LogDebugln("Connection has been upgraded to websocket")
chatConn := &chatConnection{
Conn: conn,
// If the server is behind a reverse proxy (eg, Nginx), look
// for this header to get the real IP address of the client.
forwardedFor: r.Header.Get("X-Forwarded-For"),
}
go func() {
var client *Client
// Get the client object
for client == nil {
var data common.ClientData
err := chatConn.ReadData(&data)
if err != nil {
common.LogInfof("[handler] Client closed connection: %s: %v\n",
conn.RemoteAddr().String(), err)
conn.Close()
return
}
var joinData common.JoinData
err = json.Unmarshal([]byte(data.Message), &joinData)
if err != nil {
common.LogInfof("[handler] Could not unmarshal join data %#v: %v\n", data.Message, err)
continue
}
client, err = chat.Join(chatConn, joinData)
if err != nil {
switch err.(type) {
case UserFormatError, UserTakenError:
common.LogInfof("[handler|%s] %v\n", errorName(err), err)
case BannedUserError:
common.LogInfof("[handler|%s] %v\n", errorName(err), err)
// close connection since banned users shouldn't be connecting
conn.Close()
default:
// for now all errors not caught need to be warned
common.LogErrorf("[handler|uncaught] %v\n", err)
conn.Close()
}
}
}
// Handle incomming messages
for {
var data common.ClientData
err := conn.ReadJSON(&data)
if err != nil { //if error then assuming that the connection is closed
client.Exit()
return
}
client.NewMsg(data)
}
}()
}
// 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 := strings.TrimSpace(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) {
type Data struct {
Title string
SubmitText string
Notice string
}
if errorMessage == "" {
errorMessage = "Please enter the PIN"
}
data := Data{
Title: "Enter Pin",
SubmitText: "Submit Pin",
Notice: errorMessage,
}
err := common.ExecuteServerTemplate(w, "pin", data)
if err != nil {
common.LogErrorf("Error executing file, %v", err)
}
}
func handleHelpTemplate(w http.ResponseWriter, r *http.Request) {
type Data struct {
Title string
Commands map[string]string
ModCommands map[string]string
AdminCommands map[string]string
}
data := Data{
Title: "Help",
Commands: getHelp(common.CmdlUser),
}
if len(r.URL.Query().Get("mod")) > 0 {
data.ModCommands = getHelp(common.CmdlMod)
}
if len(r.URL.Query().Get("admin")) > 0 {
data.AdminCommands = getHelp(common.CmdlAdmin)
}
err := common.ExecuteServerTemplate(w, "help", data)
if err != nil {
common.LogErrorf("Error executing file, %v", err)
}
}
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")
}
type Data struct {
Video, Chat bool
MessageHistoryCount int
Title string
}
data := Data{
Video: true,
Chat: true,
MessageHistoryCount: settings.MaxMessageCount,
Title: "Movie Night!",
}
path := strings.Split(strings.TrimLeft(r.URL.Path, "/"), "/")
if path[0] == "chat" {
data.Video = false
data.Title += " - chat"
} else if path[0] == "video" {
data.Chat = false
data.Title += " - video"
}
// Force browser to replace cache since file was not changed
if settings.NoCache {
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
}
err := common.ExecuteServerTemplate(w, "main", data)
if err != nil {
common.LogErrorf("Error executing file, %v", err)
}
}
func handlePublish(conn *rtmp.Conn) {
streams, _ := conn.Streams()
l.Lock()
common.LogDebugln("request string->", conn.URL.RequestURI())
urlParts := strings.Split(strings.Trim(conn.URL.RequestURI(), "/"), "/")
common.LogDebugln("urlParts->", urlParts)
if len(urlParts) > 2 {
common.LogErrorln("Extra garbage after stream key")
return
}
if len(urlParts) != 2 {
common.LogErrorln("Missing stream key")
return
}
if urlParts[1] != settings.GetStreamKey() {
common.LogErrorln("Stream key is incorrect. Denying stream.")
return //If key not match, deny stream
}
streamPath := urlParts[0]
ch := channels[streamPath]
if ch == nil {
ch = &Channel{}
ch.que = pubsub.NewQueue()
ch.que.WriteHeader(streams)
channels[streamPath] = ch
} else {
ch = nil
}
l.Unlock()
if ch == nil {
common.LogErrorln("Unable to start stream, channel is nil.")
return
}
stats.startStream()
common.LogInfoln("Stream started")
avutil.CopyPackets(ch.que, conn)
common.LogInfoln("Stream finished")
stats.endStream()
l.Lock()
delete(channels, streamPath)
l.Unlock()
ch.que.Close()
}
func handlePlay(conn *rtmp.Conn) {
l.RLock()
ch := channels[conn.URL.Path]
l.RUnlock()
if ch != nil {
cursor := ch.que.Latest()
avutil.CopyFile(conn, cursor)
}
}
func handleDefault(w http.ResponseWriter, r *http.Request) {
l.RLock()
ch := channels[strings.Trim(r.URL.Path, "/")]
l.RUnlock()
if ch != nil {
w.Header().Set("Content-Type", "video/x-flv")
w.Header().Set("Transfer-Encoding", "chunked")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(200)
flusher := w.(http.Flusher)
flusher.Flush()
muxer := flv.NewMuxerWriteFlusher(writeFlusher{httpflusher: flusher, Writer: w})
cursor := ch.que.Latest()
avutil.CopyFile(muxer, cursor)
} else {
if r.URL.Path != "/" {
// not really an error for the server, but for the client.
common.LogInfoln("[http 404] ", r.URL.Path)
http.NotFound(w, r)
} else {
handleIndexTemplate(w, r)
}
}
}