Add initial CSP header support (currently only for images and error pageS)
This commit is contained in:
parent
b8171a535a
commit
352eb24c30
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/yosssi/gohtml"
|
||||
"seedno.de/seednode/roulette/types"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -30,7 +31,11 @@ func notFound(w http.ResponseWriter, r *http.Request, path string) error {
|
|||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
|
||||
_, err := io.WriteString(w, gohtml.Format(newPage("Not Found", "404 Page not found")))
|
||||
nonce := types.GetNonce(6)
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -49,9 +54,14 @@ func notFound(w http.ResponseWriter, r *http.Request, path string) error {
|
|||
func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
|
||||
startTime := time.Now()
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
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(6)
|
||||
|
||||
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 {
|
||||
fmt.Printf("%s | ERROR: Invalid request for %s from %s\n",
|
||||
|
|
|
@ -6,6 +6,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -16,15 +17,15 @@ import (
|
|||
//go:embed favicons/*
|
||||
var favicons embed.FS
|
||||
|
||||
const (
|
||||
faviconHtml string = `<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
|
||||
func getFavicon(nonce string) string {
|
||||
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="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">
|
||||
<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 {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
|
|
|
@ -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
|
||||
|
||||
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,
|
||||
refreshTimer))
|
||||
htmlBody.WriteString("document.body.onkeyup = function(e) { ")
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
|
||||
const (
|
||||
AllowedCharacters string = `^[A-z0-9.\-_]+$`
|
||||
ReleaseVersion string = "8.0.0"
|
||||
ReleaseVersion string = "8.1.0"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
17
cmd/web.go
17
cmd/web.go
|
@ -40,12 +40,13 @@ const (
|
|||
timeout time.Duration = 10 * time.Second
|
||||
)
|
||||
|
||||
func newPage(title, body string) string {
|
||||
func newPage(title, body, nonce string) string {
|
||||
var htmlBody strings.Builder
|
||||
|
||||
htmlBody.WriteString(`<!DOCTYPE html><html lang="en"><head>`)
|
||||
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(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(fmt.Sprintf("<title>%s</title></head>", title))
|
||||
htmlBody.WriteString(fmt.Sprintf("<body><a href=\"/\">%s</a></body></html>", body))
|
||||
|
||||
|
@ -308,6 +309,8 @@ 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)
|
||||
|
@ -324,8 +327,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(faviconHtml)
|
||||
htmlBody.WriteString(fmt.Sprintf(`<style>%s</style>`, format.Css()))
|
||||
htmlBody.WriteString(getFavicon(nonce))
|
||||
htmlBody.WriteString(fmt.Sprintf(`<style nonce=%q>%s</style>`, nonce, format.CSS()))
|
||||
|
||||
title, err := format.Title(rootUrl, fileUri, path, fileName, Prefix, mediaType)
|
||||
if err != nil {
|
||||
|
@ -365,10 +368,10 @@ func serveMedia(paths []string, index *fileIndex, filename *regexp.Regexp, forma
|
|||
}
|
||||
|
||||
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 {
|
||||
errorChannel <- err
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ package audio
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"seedno.de/seednode/roulette/types"
|
||||
|
@ -13,7 +14,11 @@ import (
|
|||
|
||||
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
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
|
@ -26,9 +31,10 @@ 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 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>`,
|
||||
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>`,
|
||||
rootUrl,
|
||||
nonce,
|
||||
fileUri,
|
||||
mime,
|
||||
fileName), nil
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -24,7 +25,11 @@ type Format struct {
|
|||
Theme string
|
||||
}
|
||||
|
||||
func (t Format) Css() string {
|
||||
func (t Format) CSP(w http.ResponseWriter) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t Format) CSS() string {
|
||||
var css strings.Builder
|
||||
|
||||
formatter := html.New(
|
||||
|
@ -68,7 +73,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 string) (string, error) {
|
||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||
contents, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
@ -6,6 +6,7 @@ package flash
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"seedno.de/seednode/roulette/types"
|
||||
|
@ -13,7 +14,11 @@ import (
|
|||
|
||||
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
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
|
@ -26,10 +31,10 @@ 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 string) (string, error) {
|
||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||
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>window.RufflePlayer.config = {autoplay:"on"};</script><embed src="%s"></embed>`, nonce, fileUri))
|
||||
html.WriteString(fmt.Sprintf(`<br /><button onclick="window.location.href = '%s';">Next</button>`, rootUrl))
|
||||
|
||||
return html.String(), nil
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -30,7 +31,15 @@ type Format struct {
|
|||
Fun bool
|
||||
}
|
||||
|
||||
func (t Format) Css() string {
|
||||
func (t Format) CSP(w http.ResponseWriter) string {
|
||||
nonce := types.GetNonce(6)
|
||||
|
||||
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
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
|
@ -41,7 +50,7 @@ func (t Format) Css() string {
|
|||
css.WriteString(`a{color:inherit;display:block;height:97%;width:100%;text-decoration:none;}`)
|
||||
}
|
||||
|
||||
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%;color:transparent;`)
|
||||
css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)`)
|
||||
if t.Fun {
|
||||
rotate := rand.Intn(360)
|
||||
|
@ -69,19 +78,26 @@ func (t Format) Title(rootUrl, fileUri, filePath, fileName, prefix, mime string)
|
|||
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)
|
||||
if err != nil {
|
||||
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 id="main" src="%s" width="%d" height="%d" type="%s" alt="Roulette selected: %s"></a>`,
|
||||
rootUrl,
|
||||
nonce,
|
||||
fileUri,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
mime,
|
||||
fileName), nil
|
||||
fileName))
|
||||
|
||||
w.WriteString(fmt.Sprintf(`<script nonce=%q>window.addEventListener("load", function (){ document.getElementById("main").style.color='inherit' });</script>`, nonce))
|
||||
|
||||
return w.String(), nil
|
||||
}
|
||||
|
||||
func (t Format) Extensions() map[string]string {
|
||||
|
|
|
@ -7,6 +7,7 @@ package text
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
@ -16,7 +17,11 @@ import (
|
|||
|
||||
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
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
|
@ -31,7 +36,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 string) (string, error) {
|
||||
func (t Format) Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error) {
|
||||
body, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
body = []byte{}
|
||||
|
|
|
@ -5,6 +5,9 @@ Copyright © 2024 Seednode <seednode@seedno.de>
|
|||
package types
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
@ -17,14 +20,17 @@ 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 string) (string, error)
|
||||
Body(rootUrl, fileUri, filePath, fileName, prefix, mime, nonce string) (string, error)
|
||||
|
||||
// Returns a map of file extensions to MIME type strings.
|
||||
Extensions() map[string]string
|
||||
|
@ -129,3 +135,11 @@ func removeDuplicateStr(strSlice []string) []string {
|
|||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func GetNonce(length int) string {
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ package video
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
|
@ -14,7 +15,11 @@ import (
|
|||
|
||||
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
|
||||
|
||||
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`)
|
||||
|
@ -29,7 +34,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 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>`,
|
||||
rootUrl,
|
||||
fileUri,
|
||||
|
|
Loading…
Reference in New Issue