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) }