Compare commits

..

No commits in common. "34cc53338f523f1ad363456cbb32dd5cbc37ddb6" and "83809696ac3072ed32d4b64d8da4440fbcc39a95" have entirely different histories.

17 changed files with 181 additions and 268 deletions

View File

@ -73,16 +73,18 @@ You can combine these two parameters, with exclusions taking priority over inclu
Both filtering parameters ignore the file extension and full path; they only compare against the bare filename.
## Ignoring directories
If the `--ignore <filename>` flag is passed, any directory containing a file with the specified name will be skipped during the scanning stage.
If the `--ignore` flag is passed, any directory containing a file named `.roulette-ignore` (configurable with `--ignore-file`) will be skipped during the scanning stage.
## Indexing
If the `-i|--index` flag is passed, all specified paths will be indexed on start.
This will slightly increase the delay before the application begins responding to requests, but should significantly speed up subsequent requests.
Automatic index rebuilds can be enabled via the `--index-interval <duration>` flag, which accepts [time.Duration](https://pkg.go.dev/time#ParseDuration) strings.
The index can be regenerated at any time by accessing the `/index/rebuild` endpoint.
If `--index-file <filename>` is set, the index will be loaded from the specified file on start, and written to the file whenever it is re-generated.
Automatic index rebuilds can be enabled via the `--index-interval` flag, which accepts [time.Duration](https://pkg.go.dev/time#ParseDuration) strings.
If `--index-file` is set, the index will be loaded from the specified file on start, and written to the file whenever it is re-generated.
The index file consists of [zstd](https://facebook.github.io/zstd/)-compressed [gobs](https://pkg.go.dev/encoding/gob).
@ -132,7 +134,7 @@ Note: These options require sequentially-numbered files matching the following p
## Themes
The `--code` handler provides syntax highlighting via [alecthomas/chroma](https://github.com/alecthomas/chroma).
Any [supported theme](https://pkg.go.dev/github.com/alecthomas/chroma/v2@v2.9.1/styles#pkg-variables) can be passed via the `--code-theme` flag.
Any [supported theme](https://pkg.go.dev/github.com/alecthomas/chroma/v2@v2.9.1/styles#pkg-variables) can be passed via the `--theme` flag.
By default, [`solarized-dark256`](https://xyproto.github.io/splash/docs/solarized-dark256.html) is used.
@ -149,12 +151,13 @@ Flags:
--allow-empty allow specifying paths containing no supported files
--api expose REST API
--audio enable support for audio files
--binary-prefix use IEC binary prefixes instead of SI decimal prefixes
-b, --bind string address to bind to (default "0.0.0.0")
--case-insensitive use case-insensitive matching for filters
--code enable support for source code files
--code-theme string theme for source code syntax highlighting (default "solarized-dark256")
--concurrency int maximum concurrency for scan threads (default 1024)
-d, --debug log file permission errors instead of simply skipping the files
-d, --debug display even more verbose logs
--disable-buttons disable first/prev/next/last buttons
--exit-on-error shut down webserver on error, instead of just printing error
--fallback serve files as application/octet-stream if no matching format is registered
@ -164,7 +167,7 @@ Flags:
-h, --help help for roulette
--ignore string filename used to indicate directory should be skipped
--images enable support for image files
-i, --index generate index of supported file paths at startup
--index generate index of supported file paths at startup
--index-file string path to optional persistent index file
--index-interval string interval at which to regenerate index (e.g. "5m" or "1h")
--max-file-count int skip directories with file counts above this value (default 2147483647)

View File

@ -12,7 +12,6 @@ import (
"time"
"github.com/yosssi/gohtml"
"seedno.de/seednode/roulette/types"
)
var (
@ -31,11 +30,7 @@ func notFound(w http.ResponseWriter, r *http.Request, path string) error {
w.WriteHeader(http.StatusNotFound)
w.Header().Add("Content-Type", "text/html")
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce))
_, err := io.WriteString(w, gohtml.Format(newPage("Not Found", "404 Page not found", nonce)))
_, err := io.WriteString(w, gohtml.Format(newPage("Not Found", "404 Page not found")))
if err != nil {
return err
}
@ -56,17 +51,14 @@ func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
w.Header().Add("Content-Type", "text/html")
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce))
io.WriteString(w, gohtml.Format(newPage("Server Error", "An error has occurred. Please try again.", nonce)))
io.WriteString(w, gohtml.Format(newPage("Server Error", "An error has occurred. Please try again.")))
if Verbose {
fmt.Printf("%s | ERROR: Invalid request for %s from %s\n",
startTime.Format(logDate),
r.URL.Path,
r.RemoteAddr)
r.RemoteAddr,
)
}
}

View File

@ -6,7 +6,6 @@ package cmd
import (
"embed"
"fmt"
"net/http"
"strconv"
"strings"
@ -17,15 +16,15 @@ import (
//go:embed favicons/*
var favicons embed.FS
func getFavicon(nonce string) string {
return fmt.Sprintf(`<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
const (
faviconHtml string = `<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
<link rel="manifest" nonce=%q href="/favicons/site.webmanifest">
<link rel="manifest" href="/favicons/site.webmanifest">
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">`, nonce)
}
<meta name="theme-color" content="#ffffff">`
)
func serveFavicons(errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {

View File

@ -33,7 +33,19 @@ type scanStats struct {
}
func humanReadableSize(bytes int) string {
unit := 1000
var unit int
var suffix string
var prefixes string
if BinaryPrefix {
unit = 1024
prefixes = "KMGTPE"
suffix = "iB"
} else {
unit = 1000
prefixes = "kMGTPE"
suffix = "B"
}
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
@ -46,9 +58,8 @@ func humanReadableSize(bytes int) string {
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(bytes)/float64(div),
"kMGTPE"[exp])
return fmt.Sprintf("%.1f %c%s",
float64(bytes)/float64(div), prefixes[exp], suffix)
}
func kill(path string, index *fileIndex) error {
@ -137,10 +148,7 @@ func tryExtensions(splitPath *splitPath, formats types.Types) (string, error) {
var path string
for extension := range formats {
path = fmt.Sprintf("%s%s%s",
splitPath.base,
splitPath.number,
extension)
path = fmt.Sprintf("%s%s%s", splitPath.base, splitPath.number, extension)
exists, err := fileExists(path)
if err != nil {
@ -182,7 +190,8 @@ func pathIsValid(path string, paths []string) bool {
case Verbose && !matchesPrefix:
fmt.Printf("%s | ERROR: File outside specified path(s): %s\n",
time.Now().Format(logDate),
path)
path,
)
return false
case !matchesPrefix:
@ -426,7 +435,8 @@ func scanPaths(paths []string, sort string, index *fileIndex, formats types.Type
filesMatched+filesSkipped,
directoriesMatched,
directoriesMatched+directoriesSkipped,
time.Since(startTime).Round(time.Microsecond))
time.Since(startTime).Round(time.Microsecond),
)
}
slices.Sort(list)
@ -480,9 +490,7 @@ func pickFile(list []string) (string, error) {
func preparePath(prefix, path string) string {
if runtime.GOOS == "windows" {
return fmt.Sprintf("%s/%s",
prefix,
filepath.ToSlash(path))
return fmt.Sprintf("%s/%s", prefix, filepath.ToSlash(path))
}
return prefix + path
@ -534,7 +542,8 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
if Verbose {
fmt.Printf("%s | PATHS: Added %s\n",
time.Now().Format(logDate),
args[i])
args[i],
)
}
paths = append(paths, path)
@ -543,7 +552,8 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
fmt.Printf("%s | PATHS: Added %s [resolved to %s]\n",
time.Now().Format(logDate),
args[i],
path)
path,
)
}
paths = append(paths, path)
@ -551,14 +561,16 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
if Verbose {
fmt.Printf("%s | PATHS: Skipped %s (No supported files found)\n",
time.Now().Format(logDate),
args[i])
args[i],
)
}
case !pathMatches && !hasSupportedFiles:
if Verbose {
fmt.Printf("%s | PATHS: Skipped %s [resolved to %s] (No supported files found)\n",
time.Now().Format(logDate),
args[i],
path)
path,
)
}
}
}

View File

@ -6,16 +6,11 @@ package cmd
import (
"encoding/gob"
"encoding/json"
"fmt"
"net/http"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/julienschmidt/httprouter"
"github.com/klauspost/compress/zstd"
"seedno.de/seednode/roulette/types"
)
@ -191,84 +186,6 @@ func (index *fileIndex) Import(path string, errorChannel chan<- error) {
}
}
func rebuildIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) {
index.clear()
fileList(args, &filters{}, "", index, formats, encoder, errorChannel)
}
func importIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) {
if IndexFile != "" {
index.Import(IndexFile, errorChannel)
}
fileList(args, &filters{}, "", index, formats, encoder, errorChannel)
}
func serveIndex(args []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
w.Header().Add("Content-Security-Policy", "default-src 'self';")
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
indexDump := index.List()
sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
})
response, err := json.MarshalIndent(indexDump, "", " ")
if err != nil {
errorChannel <- err
serverError(w, r, nil)
return
}
response = append(response, []byte("\n")...)
written, err := w.Write(response)
if err != nil {
errorChannel <- err
}
if Verbose {
fmt.Printf("%s | SERVE: JSON index page (%s) to %s in %s\n",
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveIndexRebuild(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
if Verbose {
fmt.Printf("%s | SERVE: Index rebuild requested by %s\n",
time.Now().Format(logDate),
realIP(r))
}
w.Header().Add("Content-Security-Policy", "default-src 'self';")
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
rebuildIndex(args, index, formats, encoder, errorChannel)
_, err := w.Write([]byte("Ok\n"))
if err != nil {
errorChannel <- err
return
}
}
}
func registerIndexInterval(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, quit <-chan struct{}, errorChannel chan<- error) {
interval, err := time.ParseDuration(IndexInterval)
if err != nil {
@ -283,11 +200,16 @@ func registerIndexInterval(args []string, index *fileIndex, formats types.Types,
for {
select {
case <-ticker.C:
if Verbose {
fmt.Printf("%s | INDEX: Started scheduled index rebuild\n", time.Now().Format(logDate))
}
startTime := time.Now()
rebuildIndex(args, index, formats, encoder, errorChannel)
if Verbose {
fmt.Printf("%s | INDEX: Automatic rebuild took %s\n",
startTime.Format(logDate),
time.Since(startTime).Round(time.Microsecond),
)
}
case <-quit:
ticker.Stop()
@ -296,3 +218,17 @@ func registerIndexInterval(args []string, index *fileIndex, formats types.Types,
}
}()
}
func rebuildIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) {
index.clear()
fileList(args, &filters{}, "", index, formats, encoder, errorChannel)
}
func importIndex(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) {
if IndexFile != "" {
index.Import(IndexFile, errorChannel)
}
fileList(args, &filters{}, "", index, formats, encoder, errorChannel)
}

View File

@ -5,8 +5,11 @@ Copyright © 2024 Seednode <seednode@seedno.de>
package cmd
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"time"
"github.com/julienschmidt/httprouter"
@ -18,8 +21,6 @@ func serveExtensions(formats types.Types, available bool, errorChannel chan<- er
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
w.Header().Add("Content-Security-Policy", "default-src 'self';")
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
var extensions string
@ -40,7 +41,72 @@ func serveExtensions(formats types.Types, available bool, errorChannel chan<- er
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond))
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveIndex(args []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
indexDump := index.List()
sort.SliceStable(indexDump, func(p, q int) bool {
return strings.ToLower(indexDump[p]) < strings.ToLower(indexDump[q])
})
w.Header().Set("Content-Type", "application/json;charset=UTF-8")
response, err := json.MarshalIndent(indexDump, "", " ")
if err != nil {
errorChannel <- err
serverError(w, r, nil)
return
}
response = append(response, []byte("\n")...)
written, err := w.Write(response)
if err != nil {
errorChannel <- err
}
if Verbose {
fmt.Printf("%s | SERVE: JSON index page (%s) to %s in %s\n",
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
func serveIndexRebuild(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
rebuildIndex(args, index, formats, encoder, errorChannel)
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
_, err := w.Write([]byte("Ok\n"))
if err != nil {
errorChannel <- err
return
}
if Verbose {
fmt.Printf("%s | SERVE: Index rebuild requested by %s took %s\n",
startTime.Format(logDate),
realIP(r),
time.Since(startTime).Round(time.Microsecond),
)
}
}
}
@ -49,8 +115,6 @@ func serveMediaTypes(formats types.Types, available bool, errorChannel chan<- er
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
startTime := time.Now()
w.Header().Add("Content-Security-Policy", "default-src 'self';")
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
var mediaTypes string
@ -71,7 +135,8 @@ func serveMediaTypes(formats types.Types, available bool, errorChannel chan<- er
startTime.Format(logDate),
humanReadableSize(written),
realIP(r),
time.Since(startTime).Round(time.Microsecond))
time.Since(startTime).Round(time.Microsecond),
)
}
}
}

View File

@ -26,11 +26,10 @@ func refreshInterval(r *http.Request) (int64, string) {
}
}
func refreshFunction(rootUrl string, refreshTimer int64, nonce string) string {
func refreshFunction(rootUrl string, refreshTimer int64) string {
var htmlBody strings.Builder
htmlBody.WriteString(fmt.Sprintf(`<script nonce=%q>window.addEventListener("load", function(){ clear = setInterval(function() {window.location.href = '%s';}, %d)});`,
nonce,
htmlBody.WriteString(fmt.Sprintf("<script>window.onload = function(){ clear = setInterval(function() {window.location.href = '%s';}, %d)};",
rootUrl,
refreshTimer))
htmlBody.WriteString("document.body.onkeyup = function(e) { ")

View File

@ -17,7 +17,7 @@ import (
const (
AllowedCharacters string = `^[A-z0-9.\-_]+$`
ReleaseVersion string = "8.4.2"
ReleaseVersion string = "7.0.0"
)
var (
@ -26,6 +26,7 @@ var (
AllowEmpty bool
API bool
Audio bool
BinaryPrefix bool
Bind string
CaseInsensitive bool
Code bool
@ -118,12 +119,13 @@ func init() {
rootCmd.Flags().BoolVar(&AllowEmpty, "allow-empty", false, "allow specifying paths containing no supported files")
rootCmd.Flags().BoolVar(&API, "api", false, "expose REST API")
rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files")
rootCmd.Flags().BoolVar(&BinaryPrefix, "binary-prefix", false, "use IEC binary prefixes instead of SI decimal prefixes")
rootCmd.Flags().StringVarP(&Bind, "bind", "b", "0.0.0.0", "address to bind to")
rootCmd.Flags().BoolVar(&CaseInsensitive, "case-insensitive", false, "use case-insensitive matching for filters")
rootCmd.Flags().BoolVar(&Code, "code", false, "enable support for source code files")
rootCmd.Flags().StringVar(&CodeTheme, "code-theme", "solarized-dark256", "theme for source code syntax highlighting")
rootCmd.Flags().IntVar(&Concurrency, "concurrency", 1024, "maximum concurrency for scan threads")
rootCmd.Flags().BoolVarP(&Debug, "debug", "d", false, "log file permission errors instead of simply skipping the files")
rootCmd.Flags().BoolVarP(&Debug, "debug", "d", false, "display even more verbose logs")
rootCmd.Flags().BoolVar(&DisableButtons, "disable-buttons", false, "disable first/prev/next/last buttons")
rootCmd.Flags().BoolVar(&ExitOnError, "exit-on-error", false, "shut down webserver on error, instead of just printing error")
rootCmd.Flags().BoolVar(&Fallback, "fallback", false, "serve files as application/octet-stream if no matching format is registered")
@ -132,7 +134,7 @@ func init() {
rootCmd.Flags().BoolVar(&Fun, "fun", false, "add a bit of excitement to your day")
rootCmd.Flags().StringVar(&Ignore, "ignore", "", "filename used to indicate directory should be skipped")
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
rootCmd.Flags().BoolVarP(&Index, "index", "i", false, "generate index of supported file paths at startup")
rootCmd.Flags().BoolVar(&Index, "index", false, "generate index of supported file paths at startup")
rootCmd.Flags().StringVar(&IndexFile, "index-file", "", "path to optional persistent index file")
rootCmd.Flags().StringVar(&IndexInterval, "index-interval", "", "interval at which to regenerate index (e.g. \"5m\" or \"1h\")")
rootCmd.Flags().IntVar(&MaxFileCount, "max-file-count", math.MaxInt32, "skip directories with file counts above this value")

View File

@ -147,7 +147,7 @@ func paginate(path, first, last, queryParams string, filename *regexp.Regexp, fo
var html strings.Builder
html.WriteString(`<table><tr><td>`)
html.WriteString(`<table style="margin-left:auto;margin-right:auto;"><tr><td>`)
html.WriteString(fmt.Sprintf(`<button onclick="window.location.href = '%s%s%s%s';"%s>First</button>`,
Prefix,

View File

@ -40,13 +40,12 @@ const (
timeout time.Duration = 10 * time.Second
)
func newPage(title, body, nonce string) string {
func newPage(title, body string) string {
var htmlBody strings.Builder
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
htmlBody.WriteString(getFavicon(nonce))
htmlBody.WriteString(fmt.Sprintf(`<style nonce=%q>`, nonce))
htmlBody.WriteString(`html,body,a{display:block;height:100%;width:100%;text-decoration:none;color:inherit;cursor:auto;}</style>`)
htmlBody.WriteString(faviconHtml)
htmlBody.WriteString(`<style>html,body,a{display:block;height:100%;width:100%;text-decoration:none;color:inherit;cursor:auto;}</style>`)
htmlBody.WriteString(fmt.Sprintf("<title>%s</title></head>", title))
htmlBody.WriteString(fmt.Sprintf("<body><a href=\"/\">%s</a></body></html>", body))
@ -55,8 +54,6 @@ func newPage(title, body, nonce string) string {
func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Add("Content-Security-Policy", "default-src 'self';")
prefix := Prefix + sourcePrefix
path := strings.TrimPrefix(r.URL.Path, prefix)
@ -159,8 +156,6 @@ func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error
func serveRoot(paths []string, index *fileIndex, filename *regexp.Regexp, formats types.Types, encoder *zstd.Encoder, errorChannel chan<- error) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.Header().Add("Content-Security-Policy", "default-src 'self';")
refererUri, err := stripQueryParams(refererToUri(r.Referer()))
if err != nil {
errorChannel <- err
@ -313,8 +308,6 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
return
}
nonce := format.CSP(w)
mediaType := format.MediaType(filepath.Ext(path))
fileUri := Prefix + generateFileUri(path)
@ -331,8 +324,8 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
var htmlBody strings.Builder
htmlBody.WriteString(`<!DOCTYPE html><html class="bg" lang="en"><head>`)
htmlBody.WriteString(getFavicon(nonce))
htmlBody.WriteString(fmt.Sprintf(`<style nonce=%q>%s</style>`, nonce, format.CSS()))
htmlBody.WriteString(faviconHtml)
htmlBody.WriteString(fmt.Sprintf(`<style>%s</style>`, format.Css()))
title, err := format.Title(rootUrl, fileUri, path, fileName, Prefix, mediaType)
if err != nil {
@ -372,10 +365,10 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
}
if refreshInterval != "0ms" {
htmlBody.WriteString(refreshFunction(rootUrl, refreshTimer, nonce))
htmlBody.WriteString(refreshFunction(rootUrl, refreshTimer))
}
body, err := format.Body(rootUrl, fileUri, path, fileName, Prefix, mediaType, nonce)
body, err := format.Body(rootUrl, fileUri, path, fileName, Prefix, mediaType)
if err != nil {
errorChannel <- err
@ -427,8 +420,6 @@ func serveVersion(errorChannel chan<- error) httprouter.Handle {
data := []byte(fmt.Sprintf("roulette v%s\n", ReleaseVersion))
w.Header().Add("Content-Security-Policy", "default-src 'self';")
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
@ -617,7 +608,8 @@ func ServePage(args []string) error {
fmt.Printf("%s | SERVE: Listening on http://%s%s/\n",
time.Now().Format(logDate),
listenHost,
Prefix)
Prefix,
)
}
err = srv.ListenAndServe()

View File

@ -6,7 +6,6 @@ package audio
import (
"fmt"
"net/http"
"strings"
"seedno.de/seednode/roulette/types"
@ -14,20 +13,11 @@ import (
type Format struct{}
func (t Format) CSP(w http.ResponseWriter) string {
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce))
return nonce
}
func (t Format) CSS() string {
func (t Format) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
return css.String()
}
@ -36,10 +26,9 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
}
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
return fmt.Sprintf(`<a href="%s"><audio nonce=%q controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
return fmt.Sprintf(`<a href="%s"><audio controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the audio tag.</audio></a>`,
rootUrl,
nonce,
fileUri,
mime,
fileName), nil

View File

@ -9,7 +9,6 @@ import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strings"
@ -25,15 +24,7 @@ type Format struct {
Theme string
}
func (t Format) CSP(w http.ResponseWriter) string {
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';", nonce))
return nonce
}
func (t Format) CSS() string {
func (t Format) Css() string {
var css strings.Builder
formatter := html.New(
@ -64,9 +55,8 @@ func (t Format) CSS() string {
css.Write(b)
css.WriteString("html{height:100%;width:100%;}")
css.WriteString("a{bottom:0;left:0;position:absolute;right:0;top:0;margin:1rem;padding:0;height:99%;width:99%;color:inherit;text-decoration:none;}")
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
css.WriteString("html{height:100%;width:100%;}\n")
css.WriteString("a{bottom:0;left:0;position:absolute;right:0;top:0;margin:1rem;padding:0;height:99%;width:99%;color:inherit;text-decoration:none;}\n")
if t.Fun {
css.WriteString("body{font-family: \"Comic Sans MS\", cursive, \"Brush Script MT\", sans-serif;}\n")
}
@ -78,7 +68,7 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
}
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
contents, err := os.ReadFile(filePath)
if err != nil {
return "", err

View File

@ -6,7 +6,6 @@ package flash
import (
"fmt"
"net/http"
"strings"
"seedno.de/seednode/roulette/types"
@ -14,16 +13,11 @@ import (
type Format struct{}
func (t Format) CSP(w http.ResponseWriter) string {
return ""
}
func (t Format) CSS() string {
func (t Format) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
return css.String()
}
@ -32,17 +26,11 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
}
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
var html strings.Builder
html.WriteString(fmt.Sprintf(`<script nonce=%q src="https://unpkg.com/@ruffle-rs/ruffle"></script><script nonce=%q>window.RufflePlayer.config = {autoplay:"on"};</script><embed nonce=%qsrc="%s"></embed>`,
nonce,
nonce,
nonce,
fileUri),
)
html.WriteString(`<br /><button id="next">Next</button>`)
html.WriteString(fmt.Sprintf(`<script nonce=%q>window.addEventListener("load", function () { document.getElementById("next").addEventListener("click", function () { window.location.href = '%s'; }) }); </script>`, nonce, rootUrl))
html.WriteString(fmt.Sprintf(`<script src="https://unpkg.com/@ruffle-rs/ruffle"></script><script>window.RufflePlayer.config = {autoplay:"on"};</script><embed src="%s"></embed>`, fileUri))
html.WriteString(fmt.Sprintf(`<br /><button onclick="window.location.href = '%s';">Next</button>`, rootUrl))
return html.String(), nil
}

View File

@ -12,7 +12,6 @@ import (
_ "image/jpeg"
_ "image/png"
"math/rand"
"net/http"
"os"
"strings"
@ -31,24 +30,17 @@ type Format struct {
Fun bool
}
func (t Format) CSP(w http.ResponseWriter) string {
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce))
return nonce
}
func (t Format) CSS() string {
func (t Format) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
if t.DisableButtons {
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
} else {
css.WriteString(`a{color:inherit;display:block;height:97%;width:100%;text-decoration:none;}`)
}
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
css.WriteString(`img{margin:auto;display:block;max-width:97%;max-height:97%;`)
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)`)
if t.Fun {
@ -77,24 +69,19 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
dimensions.height), nil
}
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
dimensions, err := ImageDimensions(filePath)
if err != nil {
return "", err
}
var w strings.Builder
w.WriteString(fmt.Sprintf(`<a href="%s"><img nonce=%q src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
return fmt.Sprintf(`<a href="%s"><img style="color: transparent;" onload="this.style.color='inherit'" onerror="this.style.color='inherit'" src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
rootUrl,
nonce,
fileUri,
dimensions.width,
dimensions.height,
mime,
fileName))
return w.String(), nil
fileName), nil
}
func (t Format) Extensions() map[string]string {
@ -133,11 +120,9 @@ func ImageDimensions(path string) (*dimensions, error) {
switch {
case errors.Is(err, os.ErrNotExist):
fmt.Printf("File %s does not exist\n", path)
return &dimensions{}, nil
case err != nil:
fmt.Printf("File %s open returned error: %s\n", path, err)
return &dimensions{}, err
}
defer file.Close()
@ -146,11 +131,9 @@ func ImageDimensions(path string) (*dimensions, error) {
switch {
case errors.Is(err, image.ErrFormat):
fmt.Printf("File %s has invalid image format\n", path)
return &dimensions{width: 0, height: 0}, nil
case err != nil:
fmt.Printf("File %s decode returned error: %s\n", path, err)
return &dimensions{}, err
}

View File

@ -7,7 +7,6 @@ package text
import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"unicode/utf8"
@ -17,20 +16,11 @@ import (
type Format struct{}
func (t Format) CSP(w http.ResponseWriter) string {
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce))
return nonce
}
func (t Format) CSS() string {
func (t Format) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;overflow:hidden;}`)
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
css.WriteString(`textarea{border:none;caret-color:transparent;outline:none;margin:.5rem;`)
css.WriteString(`height:99%;width:99%;white-space:pre;overflow:auto;}`)
@ -41,7 +31,7 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
}
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
body, err := os.ReadFile(filePath)
if err != nil {
body = []byte{}

View File

@ -5,16 +5,11 @@ Copyright © 2024 Seednode <seednode@seedno.de>
package types
import (
"crypto/rand"
"encoding/hex"
"net/http"
"path/filepath"
"slices"
"strings"
)
const NonceLength = 6
var SupportedFormats = make(Types)
type Type interface {
@ -22,17 +17,14 @@ type Type interface {
// should be displayed inline (e.g. code) or embedded (e.g. images)
Type() string
// Adds a CSP header and returns a nonce to be used in generated pages
CSP(http.ResponseWriter) string
// Returns a CSS string used to format the corresponding page
CSS() string
Css() string
// Returns an HTML <title> element for the specified file
Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error)
// Returns an HTML <body> element used to display the specified file
Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error)
Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error)
// Returns a map of file extensions to MIME type strings.
Extensions() map[string]string
@ -137,11 +129,3 @@ func removeDuplicateStr(strSlice []string) []string {
}
return list
}
func GetNonce() string {
b := make([]byte, NonceLength)
if _, err := rand.Read(b); err != nil {
return ""
}
return hex.EncodeToString(b)
}

View File

@ -6,7 +6,6 @@ package video
import (
"fmt"
"net/http"
"path/filepath"
"strings"
@ -15,20 +14,11 @@ import (
type Format struct{}
func (t Format) CSP(w http.ResponseWriter) string {
nonce := types.GetNonce()
w.Header().Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' 'nonce-%s';", nonce))
return nonce
}
func (t Format) CSS() string {
func (t Format) Css() string {
var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
css.WriteString(`video{margin:auto;display:block;max-width:97%;max-height:97%;`)
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
@ -39,10 +29,9 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
}
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
return fmt.Sprintf(`<a href="%s"><video nonce=%q controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
return fmt.Sprintf(`<a href="%s"><video controls autoplay loop preload="auto"><source src="%s" type="%s" alt="Roulette selected: %s">Your browser does not support the video tag.</video></a>`,
rootUrl,
nonce,
fileUri,
mime,
fileName), nil