a73375f152
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.
428 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|