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" "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(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 { if err != nil {
return err 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{}) { func serverError(w http.ResponseWriter, r *http.Request, i interface{}) {
startTime := time.Now() startTime := time.Now()
w.WriteHeader(http.StatusInternalServerError)
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(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 { if Verbose {
fmt.Printf("%s | ERROR: Invalid request for %s from %s\n", fmt.Printf("%s | ERROR: Invalid request for %s from %s\n",

View File

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

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

View File

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

View File

@ -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))
@ -308,6 +309,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 +327,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 +368,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

View File

@ -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,7 +14,11 @@ 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%;}`)
@ -26,9 +31,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

View File

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"strings" "strings"
@ -24,7 +25,11 @@ type Format struct {
Theme string 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 var css strings.Builder
formatter := html.New( 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 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

View File

@ -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,7 +14,11 @@ 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%;}`)
@ -26,10 +31,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) {
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>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)) html.WriteString(fmt.Sprintf(`<br /><button onclick="window.location.href = '%s';">Next</button>`, rootUrl))
return html.String(), nil return html.String(), nil

View File

@ -12,6 +12,7 @@ import (
_ "image/jpeg" _ "image/jpeg"
_ "image/png" _ "image/png"
"math/rand" "math/rand"
"net/http"
"os" "os"
"strings" "strings"
@ -30,7 +31,15 @@ type Format struct {
Fun bool 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 var css strings.Builder
css.WriteString(`html,body{margin:0;padding:0;height:100%;}`) 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(`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%)`) css.WriteString(`object-fit:scale-down;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)`)
if t.Fun { if t.Fun {
rotate := rand.Intn(360) rotate := rand.Intn(360)
@ -69,19 +78,26 @@ 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 id="main" 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))
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 { func (t Format) Extensions() map[string]string {

View File

@ -7,6 +7,7 @@ package text
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"os" "os"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -16,7 +17,11 @@ 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%;}`)
@ -31,7 +36,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{}

View File

@ -5,6 +5,9 @@ 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"
@ -17,14 +20,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 +135,11 @@ func removeDuplicateStr(strSlice []string) []string {
} }
return list 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 ( import (
"fmt" "fmt"
"net/http"
"path/filepath" "path/filepath"
"strings" "strings"
@ -14,7 +15,11 @@ 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%;}`)
@ -29,7 +34,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) {
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 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,
fileUri, fileUri,