- http.Server with ReadHeaderTimeout/IdleTimeout (no read/write timeouts: /stream/ is long-lived) - shared outbound clients: providerHTTPClient (5m), apiHTTPClient (30s), imgcache client (30s) - imgcache: download outside the lock, per-item helper, cache URL uses the file name not the fs path - writeFileAtomic (temp + fsync + rename) for settings/xepg/pms/urls/authentication JSON - one package-level logMu for WebScreenLog and notifications; ring buffer keeps the newest lines - notifications evict the oldest instead of random map entries - xepg XMLTV file removal rebuilt after the loop; data.go range-mutation removed - API handler returns after error body; WS request/response fresh per command - checked type assertions in data/backup/provider/screen - SIGINT/SIGTERM handled in main via src.Shutdown(); fatal paths exit 1
131 lines
3.7 KiB
Go
131 lines
3.7 KiB
Go
package src
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// Input validation for values that reach the file system or an external
|
|
// process. Everything here is deliberately strict: xTeVe runs on a trusted
|
|
// LAN, but playlists, EPG feeds and any browser on that LAN are not trusted.
|
|
|
|
var uploadExtensions = map[string]bool{".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".webp": true, ".ico": true}
|
|
|
|
var unsafeFilenameChars = regexp.MustCompile(`[^A-Za-z0-9._-]+`)
|
|
|
|
// sanitizeUploadFilename : reduces a client-supplied logo file name to a safe
|
|
// base name with an image extension.
|
|
func sanitizeUploadFilename(name string) (string, error) {
|
|
|
|
name = strings.TrimSpace(name)
|
|
// Browsers on Windows may send backslash paths; keep only the last part.
|
|
if i := strings.LastIndexAny(name, `\/`); i >= 0 {
|
|
name = name[i+1:]
|
|
}
|
|
name = filepath.Base(name)
|
|
if name == "" || name == "." || name == ".." || name == string(filepath.Separator) {
|
|
return "", errors.New("invalid file name")
|
|
}
|
|
|
|
var ext = strings.ToLower(filepath.Ext(name))
|
|
if !uploadExtensions[ext] {
|
|
return "", fmt.Errorf("unsupported image type %q", ext)
|
|
}
|
|
|
|
var base = unsafeFilenameChars.ReplaceAllString(strings.TrimSuffix(name, filepath.Ext(name)), "_")
|
|
base = strings.Trim(base, "._")
|
|
if base == "" {
|
|
return "", errors.New("invalid file name")
|
|
}
|
|
|
|
return base + ext, nil
|
|
}
|
|
|
|
var streamingBinaries = map[string]bool{"ffmpeg": true, "ffmpeg.exe": true, "vlc": true, "vlc.exe": true, "cvlc": true, "cvlc.exe": true}
|
|
|
|
// checkStreamingBinary : the configured ffmpeg / VLC path must be an existing
|
|
// regular file whose name is one of the known players. This is what stops a
|
|
// settings change from turning the buffer into "run any program".
|
|
func checkStreamingBinary(path string) error {
|
|
|
|
if err := checkFile(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
fi, err := os.Stat(getPlatformFile(path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !fi.Mode().IsRegular() {
|
|
return fmt.Errorf("%s is not a regular file", path)
|
|
}
|
|
|
|
if !streamingBinaries[strings.ToLower(filepath.Base(path))] {
|
|
return fmt.Errorf("%s is not a supported streaming binary (ffmpeg, vlc, cvlc)", path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var streamSchemes = map[string]bool{"http": true, "https": true, "rtsp": true, "rtsps": true, "rtmp": true, "rtmps": true, "rtp": true, "udp": true, "mms": true, "mmsh": true}
|
|
|
|
// checkStreamURL : provider stream URLs are handed to ffmpeg / VLC verbatim.
|
|
// Only network schemes are allowed, so a playlist cannot point the buffer at
|
|
// file:, concat:, pipe: or similar local sources.
|
|
func checkStreamURL(raw string) error {
|
|
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !streamSchemes[strings.ToLower(u.Scheme)] {
|
|
return fmt.Errorf("stream URL scheme %q is not allowed", u.Scheme)
|
|
}
|
|
|
|
if u.Host == "" {
|
|
return errors.New("stream URL has no host")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// insideDir : true when path (already joined) stays inside dir.
|
|
func insideDir(dir, path string) bool {
|
|
|
|
dir = filepath.Clean(dir)
|
|
path = filepath.Clean(path)
|
|
|
|
if path == dir {
|
|
return true
|
|
}
|
|
|
|
return strings.HasPrefix(path, dir+string(filepath.Separator))
|
|
}
|
|
|
|
// plexTokenMask : what the UI sees instead of the real Plex token.
|
|
const plexTokenMask = "********"
|
|
|
|
// maskSettings : copy of Settings with secrets replaced for the web client.
|
|
func maskSettings(s SettingsStruct) SettingsStruct {
|
|
|
|
if len(s.PlexToken) > 0 {
|
|
s.PlexToken = plexTokenMask
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// writePrivateFile : like writeByteToFile but readable only by the owner.
|
|
func writePrivateFile(file string, data []byte) error {
|
|
|
|
// The temp file is chmod'ed before the rename, so an existing
|
|
// world-readable file is replaced by a 0600 one.
|
|
return writeFileAtomic(getPlatformFile(file), data, 0600)
|
|
}
|