Add initial CSP header support (currently only for images and error pageS)

This commit is contained in:
Seednode 2024-01-30 13:07:59 -06:00
parent b8171a535a
commit 352eb24c30
12 changed files with 107 additions and 36 deletions

View File

@ -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",

View File

@ -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) {

View File

@ -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) { ")

View File

@ -17,7 +17,7 @@ import (
const (
AllowedCharacters string = `^[A-z0-9.\-_]+$`
ReleaseVersion string = "8.0.0"
ReleaseVersion string = "8.1.0"
)
var (

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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{}

View File

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

View File

@ -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,