diff --git a/go.mod b/go.mod index f5a2af75e..dd84016d9 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23 require ( github.com/coreos/go-oidc/v3 v3.11.0 + github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 github.com/gorilla/securecookie v1.1.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/labstack/echo/v4 v4.9.1 diff --git a/go.sum b/go.sum index 41e5f8f92..752e2c31f 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0 github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81 h1:5lyLWsV+qCkoYqsKUDuycESh9DEIPVKN6iCFeL7ag50= +github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= diff --git a/server/route/ui.go b/server/route/ui.go index 98fa3e3bb..cf92fc986 100644 --- a/server/route/ui.go +++ b/server/route/ui.go @@ -24,13 +24,21 @@ package route import ( "bytes" + "crypto/rand" + "encoding/hex" "fmt" + "html/template" "io/fs" "log" "net/http" + "path" "regexp" "strings" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/labstack/echo/v4" ) @@ -86,3 +94,99 @@ func buildUIAssetsHandler(assets fs.FS) echo.HandlerFunc { handler := http.FileServer(http.FS(assets)) return echo.WrapHandler(handler) } + +func generateNonce() string { + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + panic(err) + } + return hex.EncodeToString(bytes) +} + +// Generate CSP header +func generateCSP(nonce string) string { + return fmt.Sprintf( + "base-uri 'self'; default-src 'none'; style-src 'nonce-%s'; script-src 'nonce-%s'; frame-ancestors 'self'; form-action 'none'; sandbox allow-same-origin allow-popups allow-popups-to-escape-sandbox;", + nonce, + nonce, + ) +} + +// Process markdown content +func processMarkdown(content string) string { + // Create markdown parser with extensions + extensions := parser.CommonExtensions | parser.AutoHeadingIDs + p := parser.NewWithExtensions(extensions) + + // Parse markdown to AST + ast := p.Parse([]byte(content)) + + // Setup HTML renderer + opts := html.RendererOptions{ + Flags: html.CommonFlags | html.HrefTargetBlank, + } + renderer := html.NewRenderer(opts) + + // Render to HTML + return string(markdown.Render(ast, renderer)) +} + +// Template for the HTML page +const pageTemplate = ` + + +
+