Compare commits
9 Commits
83809696ac
...
34cc53338f
Author | SHA1 | Date |
---|---|---|
Seednode | 34cc53338f | |
Seednode | c926a9184e | |
Seednode | 0baba06ace | |
Seednode | 70275a734f | |
Seednode | 3814e10f20 | |
Seednode | dbe3bbe0d6 | |
Seednode | 352eb24c30 | |
Seednode | b8171a535a | |
Seednode | 5488ffc410 |
15
README.md
15
README.md
|
@ -73,18 +73,16 @@ 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.
|
Both filtering parameters ignore the file extension and full path; they only compare against the bare filename.
|
||||||
|
|
||||||
## Ignoring directories
|
## Ignoring directories
|
||||||
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.
|
If the `--ignore <filename>` flag is passed, any directory containing a file with the specified name will be skipped during the scanning stage.
|
||||||
|
|
||||||
## Indexing
|
## Indexing
|
||||||
If the `-i|--index` flag is passed, all specified paths will be indexed on start.
|
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.
|
This will slightly increase the delay before the application begins responding to requests, but should significantly speed up subsequent requests.
|
||||||
|
|
||||||
The index can be regenerated at any time by accessing the `/index/rebuild` endpoint.
|
Automatic index rebuilds can be enabled via the `--index-interval <duration>` flag, which accepts [time.Duration](https://pkg.go.dev/time#ParseDuration) strings.
|
||||||
|
|
||||||
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 <filename>` is set, the index will be loaded from the specified file on start, and written to the file whenever it is re-generated.
|
||||||
|
|
||||||
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).
|
The index file consists of [zstd](https://facebook.github.io/zstd/)-compressed [gobs](https://pkg.go.dev/encoding/gob).
|
||||||
|
|
||||||
|
@ -134,7 +132,7 @@ Note: These options require sequentially-numbered files matching the following p
|
||||||
## Themes
|
## Themes
|
||||||
The `--code` handler provides syntax highlighting via [alecthomas/chroma](https://github.com/alecthomas/chroma).
|
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 `--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 `--code-theme` flag.
|
||||||
|
|
||||||
By default, [`solarized-dark256`](https://xyproto.github.io/splash/docs/solarized-dark256.html) is used.
|
By default, [`solarized-dark256`](https://xyproto.github.io/splash/docs/solarized-dark256.html) is used.
|
||||||
|
|
||||||
|
@ -151,13 +149,12 @@ Flags:
|
||||||
--allow-empty allow specifying paths containing no supported files
|
--allow-empty allow specifying paths containing no supported files
|
||||||
--api expose REST API
|
--api expose REST API
|
||||||
--audio enable support for audio files
|
--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")
|
-b, --bind string address to bind to (default "0.0.0.0")
|
||||||
--case-insensitive use case-insensitive matching for filters
|
--case-insensitive use case-insensitive matching for filters
|
||||||
--code enable support for source code files
|
--code enable support for source code files
|
||||||
--code-theme string theme for source code syntax highlighting (default "solarized-dark256")
|
--code-theme string theme for source code syntax highlighting (default "solarized-dark256")
|
||||||
--concurrency int maximum concurrency for scan threads (default 1024)
|
--concurrency int maximum concurrency for scan threads (default 1024)
|
||||||
-d, --debug display even more verbose logs
|
-d, --debug log file permission errors instead of simply skipping the files
|
||||||
--disable-buttons disable first/prev/next/last buttons
|
--disable-buttons disable first/prev/next/last buttons
|
||||||
--exit-on-error shut down webserver on error, instead of just printing error
|
--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
|
--fallback serve files as application/octet-stream if no matching format is registered
|
||||||
|
@ -167,7 +164,7 @@ Flags:
|
||||||
-h, --help help for roulette
|
-h, --help help for roulette
|
||||||
--ignore string filename used to indicate directory should be skipped
|
--ignore string filename used to indicate directory should be skipped
|
||||||
--images enable support for image files
|
--images enable support for image files
|
||||||
--index generate index of supported file paths at startup
|
-i, --index generate index of supported file paths at startup
|
||||||
--index-file string path to optional persistent index file
|
--index-file string path to optional persistent index file
|
||||||
--index-interval string interval at which to regenerate index (e.g. "5m" or "1h")
|
--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)
|
--max-file-count int skip directories with file counts above this value (default 2147483647)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/yosssi/gohtml"
|
"github.com/yosssi/gohtml"
|
||||||
|
"seedno.de/seednode/roulette/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -30,7 +31,11 @@ func notFound(w http.ResponseWriter, r *http.Request, path string) error {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
w.Header().Add("Content-Type", "text/html")
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
_, err := io.WriteString(w, gohtml.Format(newPage("Not Found", "404 Page not found")))
|
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)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -51,14 +56,17 @@ func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
io.WriteString(w, gohtml.Format(newPage("Server Error", "An error has occurred. Please try again.")))
|
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)))
|
||||||
|
|
||||||
if Verbose {
|
if Verbose {
|
||||||
fmt.Printf("%s | ERROR: Invalid request for %s from %s\n",
|
fmt.Printf("%s | ERROR: Invalid request for %s from %s\n",
|
||||||
startTime.Format(logDate),
|
startTime.Format(logDate),
|
||||||
r.URL.Path,
|
r.URL.Path,
|
||||||
r.RemoteAddr,
|
r.RemoteAddr)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -16,15 +17,15 @@ import (
|
||||||
//go:embed favicons/*
|
//go:embed favicons/*
|
||||||
var favicons embed.FS
|
var favicons embed.FS
|
||||||
|
|
||||||
const (
|
func getFavicon(nonce string) string {
|
||||||
faviconHtml string = `<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
|
return fmt.Sprintf(`<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="32x32" href="/favicons/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/favicons/site.webmanifest">
|
<link rel="manifest" nonce=%q href="/favicons/site.webmanifest">
|
||||||
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<meta name="theme-color" content="#ffffff">`
|
<meta name="theme-color" content="#ffffff">`, nonce)
|
||||||
)
|
}
|
||||||
|
|
||||||
func serveFavicons(errorChannel chan<- error) httprouter.Handle {
|
func serveFavicons(errorChannel chan<- error) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
|
46
cmd/files.go
46
cmd/files.go
|
@ -33,19 +33,7 @@ type scanStats struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func humanReadableSize(bytes int) string {
|
func humanReadableSize(bytes int) string {
|
||||||
var unit int
|
unit := 1000
|
||||||
var suffix string
|
|
||||||
var prefixes string
|
|
||||||
|
|
||||||
if BinaryPrefix {
|
|
||||||
unit = 1024
|
|
||||||
prefixes = "KMGTPE"
|
|
||||||
suffix = "iB"
|
|
||||||
} else {
|
|
||||||
unit = 1000
|
|
||||||
prefixes = "kMGTPE"
|
|
||||||
suffix = "B"
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes < unit {
|
if bytes < unit {
|
||||||
return fmt.Sprintf("%d B", bytes)
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
@ -58,8 +46,9 @@ func humanReadableSize(bytes int) string {
|
||||||
exp++
|
exp++
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%.1f %c%s",
|
return fmt.Sprintf("%.1f %cB",
|
||||||
float64(bytes)/float64(div), prefixes[exp], suffix)
|
float64(bytes)/float64(div),
|
||||||
|
"kMGTPE"[exp])
|
||||||
}
|
}
|
||||||
|
|
||||||
func kill(path string, index *fileIndex) error {
|
func kill(path string, index *fileIndex) error {
|
||||||
|
@ -148,7 +137,10 @@ func tryExtensions(splitPath *splitPath, formats types.Types) (string, error) {
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
for extension := range formats {
|
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)
|
exists, err := fileExists(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -190,8 +182,7 @@ func pathIsValid(path string, paths []string) bool {
|
||||||
case Verbose && !matchesPrefix:
|
case Verbose && !matchesPrefix:
|
||||||
fmt.Printf("%s | ERROR: File outside specified path(s): %s\n",
|
fmt.Printf("%s | ERROR: File outside specified path(s): %s\n",
|
||||||
time.Now().Format(logDate),
|
time.Now().Format(logDate),
|
||||||
path,
|
path)
|
||||||
)
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
case !matchesPrefix:
|
case !matchesPrefix:
|
||||||
|
@ -435,8 +426,7 @@ func scanPaths(paths []string, sort string, index *fileIndex, formats types.Type
|
||||||
filesMatched+filesSkipped,
|
filesMatched+filesSkipped,
|
||||||
directoriesMatched,
|
directoriesMatched,
|
||||||
directoriesMatched+directoriesSkipped,
|
directoriesMatched+directoriesSkipped,
|
||||||
time.Since(startTime).Round(time.Microsecond),
|
time.Since(startTime).Round(time.Microsecond))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Sort(list)
|
slices.Sort(list)
|
||||||
|
@ -490,7 +480,9 @@ func pickFile(list []string) (string, error) {
|
||||||
|
|
||||||
func preparePath(prefix, path string) string {
|
func preparePath(prefix, path string) string {
|
||||||
if runtime.GOOS == "windows" {
|
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
|
return prefix + path
|
||||||
|
@ -542,8 +534,7 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
|
||||||
if Verbose {
|
if Verbose {
|
||||||
fmt.Printf("%s | PATHS: Added %s\n",
|
fmt.Printf("%s | PATHS: Added %s\n",
|
||||||
time.Now().Format(logDate),
|
time.Now().Format(logDate),
|
||||||
args[i],
|
args[i])
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paths = append(paths, path)
|
paths = append(paths, path)
|
||||||
|
@ -552,8 +543,7 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
|
||||||
fmt.Printf("%s | PATHS: Added %s [resolved to %s]\n",
|
fmt.Printf("%s | PATHS: Added %s [resolved to %s]\n",
|
||||||
time.Now().Format(logDate),
|
time.Now().Format(logDate),
|
||||||
args[i],
|
args[i],
|
||||||
path,
|
path)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paths = append(paths, path)
|
paths = append(paths, path)
|
||||||
|
@ -561,16 +551,14 @@ func validatePaths(args []string, formats types.Types) ([]string, error) {
|
||||||
if Verbose {
|
if Verbose {
|
||||||
fmt.Printf("%s | PATHS: Skipped %s (No supported files found)\n",
|
fmt.Printf("%s | PATHS: Skipped %s (No supported files found)\n",
|
||||||
time.Now().Format(logDate),
|
time.Now().Format(logDate),
|
||||||
args[i],
|
args[i])
|
||||||
)
|
|
||||||
}
|
}
|
||||||
case !pathMatches && !hasSupportedFiles:
|
case !pathMatches && !hasSupportedFiles:
|
||||||
if Verbose {
|
if Verbose {
|
||||||
fmt.Printf("%s | PATHS: Skipped %s [resolved to %s] (No supported files found)\n",
|
fmt.Printf("%s | PATHS: Skipped %s [resolved to %s] (No supported files found)\n",
|
||||||
time.Now().Format(logDate),
|
time.Now().Format(logDate),
|
||||||
args[i],
|
args[i],
|
||||||
path,
|
path)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
108
cmd/index.go
108
cmd/index.go
|
@ -6,11 +6,16 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"seedno.de/seednode/roulette/types"
|
"seedno.de/seednode/roulette/types"
|
||||||
)
|
)
|
||||||
|
@ -186,6 +191,84 @@ 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) {
|
func registerIndexInterval(args []string, index *fileIndex, formats types.Types, encoder *zstd.Encoder, quit <-chan struct{}, errorChannel chan<- error) {
|
||||||
interval, err := time.ParseDuration(IndexInterval)
|
interval, err := time.ParseDuration(IndexInterval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -200,16 +283,11 @@ func registerIndexInterval(args []string, index *fileIndex, formats types.Types,
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
startTime := time.Now()
|
if Verbose {
|
||||||
|
fmt.Printf("%s | INDEX: Started scheduled index rebuild\n", time.Now().Format(logDate))
|
||||||
|
}
|
||||||
|
|
||||||
rebuildIndex(args, index, formats, encoder, errorChannel)
|
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:
|
case <-quit:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
|
||||||
|
@ -218,17 +296,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
77
cmd/info.go
77
cmd/info.go
|
@ -5,11 +5,8 @@ Copyright © 2024 Seednode <seednode@seedno.de>
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
@ -21,6 +18,8 @@ func serveExtensions(formats types.Types, available bool, errorChannel chan<- er
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
|
w.Header().Add("Content-Security-Policy", "default-src 'self';")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
|
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
|
||||||
|
|
||||||
var extensions string
|
var extensions string
|
||||||
|
@ -41,72 +40,7 @@ func serveExtensions(formats types.Types, available bool, errorChannel chan<- er
|
||||||
startTime.Format(logDate),
|
startTime.Format(logDate),
|
||||||
humanReadableSize(written),
|
humanReadableSize(written),
|
||||||
realIP(r),
|
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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +49,8 @@ func serveMediaTypes(formats types.Types, available bool, errorChannel chan<- er
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
|
w.Header().Add("Content-Security-Policy", "default-src 'self';")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
|
w.Header().Set("Content-Type", "text/plain;charset=UTF-8")
|
||||||
|
|
||||||
var mediaTypes string
|
var mediaTypes string
|
||||||
|
@ -135,8 +71,7 @@ func serveMediaTypes(formats types.Types, available bool, errorChannel chan<- er
|
||||||
startTime.Format(logDate),
|
startTime.Format(logDate),
|
||||||
humanReadableSize(written),
|
humanReadableSize(written),
|
||||||
realIP(r),
|
realIP(r),
|
||||||
time.Since(startTime).Round(time.Microsecond),
|
time.Since(startTime).Round(time.Microsecond))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,11 @@ func refreshInterval(r *http.Request) (int64, string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshFunction(rootUrl string, refreshTimer int64) string {
|
func refreshFunction(rootUrl string, refreshTimer int64, nonce string) string {
|
||||||
var htmlBody strings.Builder
|
var htmlBody strings.Builder
|
||||||
|
|
||||||
htmlBody.WriteString(fmt.Sprintf("<script>window.onload = function(){ clear = setInterval(function() {window.location.href = '%s';}, %d)};",
|
htmlBody.WriteString(fmt.Sprintf(`<script nonce=%q>window.addEventListener("load", function(){ clear = setInterval(function() {window.location.href = '%s';}, %d)});`,
|
||||||
|
nonce,
|
||||||
rootUrl,
|
rootUrl,
|
||||||
refreshTimer))
|
refreshTimer))
|
||||||
htmlBody.WriteString("document.body.onkeyup = function(e) { ")
|
htmlBody.WriteString("document.body.onkeyup = function(e) { ")
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AllowedCharacters string = `^[A-z0-9.\-_]+$`
|
AllowedCharacters string = `^[A-z0-9.\-_]+$`
|
||||||
ReleaseVersion string = "7.0.0"
|
ReleaseVersion string = "8.4.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -26,7 +26,6 @@ var (
|
||||||
AllowEmpty bool
|
AllowEmpty bool
|
||||||
API bool
|
API bool
|
||||||
Audio bool
|
Audio bool
|
||||||
BinaryPrefix bool
|
|
||||||
Bind string
|
Bind string
|
||||||
CaseInsensitive bool
|
CaseInsensitive bool
|
||||||
Code bool
|
Code bool
|
||||||
|
@ -119,13 +118,12 @@ func init() {
|
||||||
rootCmd.Flags().BoolVar(&AllowEmpty, "allow-empty", false, "allow specifying paths containing no supported files")
|
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(&API, "api", false, "expose REST API")
|
||||||
rootCmd.Flags().BoolVar(&Audio, "audio", false, "enable support for audio files")
|
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().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(&CaseInsensitive, "case-insensitive", false, "use case-insensitive matching for filters")
|
||||||
rootCmd.Flags().BoolVar(&Code, "code", false, "enable support for source code files")
|
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().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().IntVar(&Concurrency, "concurrency", 1024, "maximum concurrency for scan threads")
|
||||||
rootCmd.Flags().BoolVarP(&Debug, "debug", "d", false, "display even more verbose logs")
|
rootCmd.Flags().BoolVarP(&Debug, "debug", "d", false, "log file permission errors instead of simply skipping the files")
|
||||||
rootCmd.Flags().BoolVar(&DisableButtons, "disable-buttons", false, "disable first/prev/next/last buttons")
|
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(&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")
|
rootCmd.Flags().BoolVar(&Fallback, "fallback", false, "serve files as application/octet-stream if no matching format is registered")
|
||||||
|
@ -134,7 +132,7 @@ func init() {
|
||||||
rootCmd.Flags().BoolVar(&Fun, "fun", false, "add a bit of excitement to your day")
|
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().StringVar(&Ignore, "ignore", "", "filename used to indicate directory should be skipped")
|
||||||
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
|
rootCmd.Flags().BoolVar(&Images, "images", false, "enable support for image files")
|
||||||
rootCmd.Flags().BoolVar(&Index, "index", false, "generate index of supported file paths at startup")
|
rootCmd.Flags().BoolVarP(&Index, "index", "i", false, "generate index of supported file paths at startup")
|
||||||
rootCmd.Flags().StringVar(&IndexFile, "index-file", "", "path to optional persistent index file")
|
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().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")
|
rootCmd.Flags().IntVar(&MaxFileCount, "max-file-count", math.MaxInt32, "skip directories with file counts above this value")
|
||||||
|
|
|
@ -147,7 +147,7 @@ func paginate(path, first, last, queryParams string, filename *regexp.Regexp, fo
|
||||||
|
|
||||||
var html strings.Builder
|
var html strings.Builder
|
||||||
|
|
||||||
html.WriteString(`<table style="margin-left:auto;margin-right:auto;"><tr><td>`)
|
html.WriteString(`<table><tr><td>`)
|
||||||
|
|
||||||
html.WriteString(fmt.Sprintf(`<button onclick="window.location.href = '%s%s%s%s';"%s>First</button>`,
|
html.WriteString(fmt.Sprintf(`<button onclick="window.location.href = '%s%s%s%s';"%s>First</button>`,
|
||||||
Prefix,
|
Prefix,
|
||||||
|
|
26
cmd/web.go
26
cmd/web.go
|
@ -40,12 +40,13 @@ const (
|
||||||
timeout time.Duration = 10 * time.Second
|
timeout time.Duration = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
func newPage(title, body string) string {
|
func newPage(title, body, nonce string) string {
|
||||||
var htmlBody strings.Builder
|
var htmlBody strings.Builder
|
||||||
|
|
||||||
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
||||||
htmlBody.WriteString(faviconHtml)
|
htmlBody.WriteString(getFavicon(nonce))
|
||||||
htmlBody.WriteString(`<style>html,body,a{display:block;height:100%;width:100%;text-decoration:none;color:inherit;cursor:auto;}</style>`)
|
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(fmt.Sprintf("<title>%s</title></head>", title))
|
htmlBody.WriteString(fmt.Sprintf("<title>%s</title></head>", title))
|
||||||
htmlBody.WriteString(fmt.Sprintf("<body><a href=\"/\">%s</a></body></html>", body))
|
htmlBody.WriteString(fmt.Sprintf("<body><a href=\"/\">%s</a></body></html>", body))
|
||||||
|
|
||||||
|
@ -54,6 +55,8 @@ func newPage(title, body string) string {
|
||||||
|
|
||||||
func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
|
func serveStaticFile(paths []string, index *fileIndex, errorChannel chan<- error) httprouter.Handle {
|
||||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
w.Header().Add("Content-Security-Policy", "default-src 'self';")
|
||||||
|
|
||||||
prefix := Prefix + sourcePrefix
|
prefix := Prefix + sourcePrefix
|
||||||
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, prefix)
|
path := strings.TrimPrefix(r.URL.Path, prefix)
|
||||||
|
@ -156,6 +159,8 @@ 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 {
|
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) {
|
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()))
|
refererUri, err := stripQueryParams(refererToUri(r.Referer()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChannel <- err
|
errorChannel <- err
|
||||||
|
@ -308,6 +313,8 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nonce := format.CSP(w)
|
||||||
|
|
||||||
mediaType := format.MediaType(filepath.Ext(path))
|
mediaType := format.MediaType(filepath.Ext(path))
|
||||||
|
|
||||||
fileUri := Prefix + generateFileUri(path)
|
fileUri := Prefix + generateFileUri(path)
|
||||||
|
@ -324,8 +331,8 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
|
||||||
|
|
||||||
var htmlBody strings.Builder
|
var htmlBody strings.Builder
|
||||||
htmlBody.WriteString(`<!DOCTYPE html><html class="bg" lang="en"><head>`)
|
htmlBody.WriteString(`<!DOCTYPE html><html class="bg" lang="en"><head>`)
|
||||||
htmlBody.WriteString(faviconHtml)
|
htmlBody.WriteString(getFavicon(nonce))
|
||||||
htmlBody.WriteString(fmt.Sprintf(`<style>%s</style>`, format.Css()))
|
htmlBody.WriteString(fmt.Sprintf(`<style nonce=%q>%s</style>`, nonce, format.CSS()))
|
||||||
|
|
||||||
title, err := format.Title(rootUrl, fileUri, path, fileName, Prefix, mediaType)
|
title, err := format.Title(rootUrl, fileUri, path, fileName, Prefix, mediaType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -365,10 +372,10 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
|
||||||
}
|
}
|
||||||
|
|
||||||
if refreshInterval != "0ms" {
|
if refreshInterval != "0ms" {
|
||||||
htmlBody.WriteString(refreshFunction(rootUrl, refreshTimer))
|
htmlBody.WriteString(refreshFunction(rootUrl, refreshTimer, nonce))
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := format.Body(rootUrl, fileUri, path, fileName, Prefix, mediaType)
|
body, err := format.Body(rootUrl, fileUri, path, fileName, Prefix, mediaType, nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorChannel <- err
|
errorChannel <- err
|
||||||
|
|
||||||
|
@ -420,6 +427,8 @@ func serveVersion(errorChannel chan<- error) httprouter.Handle {
|
||||||
|
|
||||||
data := []byte(fmt.Sprintf("roulette v%s\n", ReleaseVersion))
|
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-Type", "text/plain;charset=UTF-8")
|
||||||
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||||
|
@ -608,8 +617,7 @@ func ServePage(args []string) error {
|
||||||
fmt.Printf("%s | SERVE: Listening on http://%s%s/\n",
|
fmt.Printf("%s | SERVE: Listening on http://%s%s/\n",
|
||||||
time.Now().Format(logDate),
|
time.Now().Format(logDate),
|
||||||
listenHost,
|
listenHost,
|
||||||
Prefix,
|
Prefix)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = srv.ListenAndServe()
|
err = srv.ListenAndServe()
|
||||||
|
|
|
@ -6,6 +6,7 @@ package audio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"seedno.de/seednode/roulette/types"
|
"seedno.de/seednode/roulette/types"
|
||||||
|
@ -13,11 +14,20 @@ import (
|
||||||
|
|
||||||
type Format struct{}
|
type Format struct{}
|
||||||
|
|
||||||
func (t Format) Css() 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';", nonce))
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Format) CSS() string {
|
||||||
var css strings.Builder
|
var css strings.Builder
|
||||||
|
|
||||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
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(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||||
|
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
|
||||||
|
|
||||||
return css.String()
|
return css.String()
|
||||||
}
|
}
|
||||||
|
@ -26,9 +36,10 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
|
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce 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>`,
|
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>`,
|
||||||
rootUrl,
|
rootUrl,
|
||||||
|
nonce,
|
||||||
fileUri,
|
fileUri,
|
||||||
mime,
|
mime,
|
||||||
fileName), nil
|
fileName), nil
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -24,7 +25,15 @@ type Format struct {
|
||||||
Theme string
|
Theme string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Css() 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 {
|
||||||
var css strings.Builder
|
var css strings.Builder
|
||||||
|
|
||||||
formatter := html.New(
|
formatter := html.New(
|
||||||
|
@ -55,8 +64,9 @@ func (t Format) Css() string {
|
||||||
|
|
||||||
css.Write(b)
|
css.Write(b)
|
||||||
|
|
||||||
css.WriteString("html{height:100%;width:100%;}\n")
|
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;}\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;}")
|
||||||
|
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
|
||||||
if t.Fun {
|
if t.Fun {
|
||||||
css.WriteString("body{font-family: \"Comic Sans MS\", cursive, \"Brush Script MT\", sans-serif;}\n")
|
css.WriteString("body{font-family: \"Comic Sans MS\", cursive, \"Brush Script MT\", sans-serif;}\n")
|
||||||
}
|
}
|
||||||
|
@ -68,7 +78,7 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
|
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||||
contents, err := os.ReadFile(filePath)
|
contents, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -6,6 +6,7 @@ package flash
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"seedno.de/seednode/roulette/types"
|
"seedno.de/seednode/roulette/types"
|
||||||
|
@ -13,11 +14,16 @@ import (
|
||||||
|
|
||||||
type Format struct{}
|
type Format struct{}
|
||||||
|
|
||||||
func (t Format) Css() string {
|
func (t Format) CSP(w http.ResponseWriter) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Format) CSS() string {
|
||||||
var css strings.Builder
|
var css strings.Builder
|
||||||
|
|
||||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
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(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||||
|
css.WriteString(`table{margin-left:auto;margin-right:auto;}`)
|
||||||
|
|
||||||
return css.String()
|
return css.String()
|
||||||
}
|
}
|
||||||
|
@ -26,11 +32,17 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
|
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||||
var html strings.Builder
|
var html strings.Builder
|
||||||
|
|
||||||
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(`<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>`,
|
||||||
html.WriteString(fmt.Sprintf(`<br /><button onclick="window.location.href = '%s';">Next</button>`, rootUrl))
|
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))
|
||||||
|
|
||||||
return html.String(), nil
|
return html.String(), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -30,17 +31,24 @@ type Format struct {
|
||||||
Fun bool
|
Fun bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Css() 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';", nonce))
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Format) CSS() string {
|
||||||
var css strings.Builder
|
var css strings.Builder
|
||||||
|
|
||||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||||
|
|
||||||
if t.DisableButtons {
|
if t.DisableButtons {
|
||||||
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
css.WriteString(`a{color:inherit;display:block;height:100%;width:100%;text-decoration:none;}`)
|
||||||
} else {
|
} else {
|
||||||
css.WriteString(`a{color:inherit;display:block;height:97%;width:100%;text-decoration:none;}`)
|
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(`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%)`)
|
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)`)
|
||||||
if t.Fun {
|
if t.Fun {
|
||||||
|
@ -69,19 +77,24 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
||||||
dimensions.height), nil
|
dimensions.height), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
|
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||||
dimensions, err := ImageDimensions(filePath)
|
dimensions, err := ImageDimensions(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
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>`,
|
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>`,
|
||||||
rootUrl,
|
rootUrl,
|
||||||
|
nonce,
|
||||||
fileUri,
|
fileUri,
|
||||||
dimensions.width,
|
dimensions.width,
|
||||||
dimensions.height,
|
dimensions.height,
|
||||||
mime,
|
mime,
|
||||||
fileName), nil
|
fileName))
|
||||||
|
|
||||||
|
return w.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Extensions() map[string]string {
|
func (t Format) Extensions() map[string]string {
|
||||||
|
@ -120,9 +133,11 @@ func ImageDimensions(path string) (*dimensions, error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, os.ErrNotExist):
|
case errors.Is(err, os.ErrNotExist):
|
||||||
fmt.Printf("File %s does not exist\n", path)
|
fmt.Printf("File %s does not exist\n", path)
|
||||||
|
|
||||||
return &dimensions{}, nil
|
return &dimensions{}, nil
|
||||||
case err != nil:
|
case err != nil:
|
||||||
fmt.Printf("File %s open returned error: %s\n", path, err)
|
fmt.Printf("File %s open returned error: %s\n", path, err)
|
||||||
|
|
||||||
return &dimensions{}, err
|
return &dimensions{}, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
@ -131,9 +146,11 @@ func ImageDimensions(path string) (*dimensions, error) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, image.ErrFormat):
|
case errors.Is(err, image.ErrFormat):
|
||||||
fmt.Printf("File %s has invalid image format\n", path)
|
fmt.Printf("File %s has invalid image format\n", path)
|
||||||
|
|
||||||
return &dimensions{width: 0, height: 0}, nil
|
return &dimensions{width: 0, height: 0}, nil
|
||||||
case err != nil:
|
case err != nil:
|
||||||
fmt.Printf("File %s decode returned error: %s\n", path, err)
|
fmt.Printf("File %s decode returned error: %s\n", path, err)
|
||||||
|
|
||||||
return &dimensions{}, err
|
return &dimensions{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ package text
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
@ -16,11 +17,20 @@ import (
|
||||||
|
|
||||||
type Format struct{}
|
type Format struct{}
|
||||||
|
|
||||||
func (t Format) Css() 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';", nonce))
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Format) CSS() string {
|
||||||
var css strings.Builder
|
var css strings.Builder
|
||||||
|
|
||||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
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(`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(`textarea{border:none;caret-color:transparent;outline:none;margin:.5rem;`)
|
||||||
css.WriteString(`height:99%;width:99%;white-space:pre;overflow:auto;}`)
|
css.WriteString(`height:99%;width:99%;white-space:pre;overflow:auto;}`)
|
||||||
|
|
||||||
|
@ -31,7 +41,7 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
|
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||||
body, err := os.ReadFile(filePath)
|
body, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
body = []byte{}
|
body = []byte{}
|
||||||
|
|
|
@ -5,11 +5,16 @@ Copyright © 2024 Seednode <seednode@seedno.de>
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const NonceLength = 6
|
||||||
|
|
||||||
var SupportedFormats = make(Types)
|
var SupportedFormats = make(Types)
|
||||||
|
|
||||||
type Type interface {
|
type Type interface {
|
||||||
|
@ -17,14 +22,17 @@ type Type interface {
|
||||||
// should be displayed inline (e.g. code) or embedded (e.g. images)
|
// should be displayed inline (e.g. code) or embedded (e.g. images)
|
||||||
Type() string
|
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
|
// Returns a CSS string used to format the corresponding page
|
||||||
Css() string
|
CSS() string
|
||||||
|
|
||||||
// Returns an HTML <title> element for the specified file
|
// Returns an HTML <title> element for the specified file
|
||||||
Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error)
|
Title(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error)
|
||||||
|
|
||||||
// Returns an HTML <body> element used to display the specified file
|
// Returns an HTML <body> element used to display the specified file
|
||||||
Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error)
|
Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error)
|
||||||
|
|
||||||
// Returns a map of file extensions to MIME type strings.
|
// Returns a map of file extensions to MIME type strings.
|
||||||
Extensions() map[string]string
|
Extensions() map[string]string
|
||||||
|
@ -129,3 +137,11 @@ func removeDuplicateStr(strSlice []string) []string {
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetNonce() string {
|
||||||
|
b := make([]byte, NonceLength)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ package video
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -14,11 +15,20 @@ import (
|
||||||
|
|
||||||
type Format struct{}
|
type Format struct{}
|
||||||
|
|
||||||
func (t Format) Css() 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';", nonce))
|
||||||
|
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Format) CSS() string {
|
||||||
var css strings.Builder
|
var css strings.Builder
|
||||||
|
|
||||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
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(`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(`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%);}`)
|
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}`)
|
||||||
|
|
||||||
|
@ -29,9 +39,10 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
||||||
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
return fmt.Sprintf(`<title>%s</title>`, fileName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime string) (string, error) {
|
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce 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>`,
|
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>`,
|
||||||
rootUrl,
|
rootUrl,
|
||||||
|
nonce,
|
||||||
fileUri,
|
fileUri,
|
||||||
mime,
|
mime,
|
||||||
fileName), nil
|
fileName), nil
|
||||||
|
|
Loading…
Reference in New Issue