From 680f5e740bb9592b8728ab71f43551240a3a6159 Mon Sep 17 00:00:00 2001
From: Patrick D'appollonio
<930925+patrickdappollonio@users.noreply.github.com>
Date: Thu, 17 Apr 2025 17:50:51 -0700
Subject: [PATCH 1/4] Add support for Custom CSS.
---
app.go | 1 +
internal/server/filter.go | 4 +++-
internal/server/handlers.go | 1 +
internal/server/server.go | 25 +++++++++++++++++++++++++
internal/server/startup.go | 4 ++++
internal/server/templates/app.tmpl | 4 +++-
internal/server/validation.go | 24 ++++++++++++++++++++++++
7 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/app.go b/app.go
index e90075d..42956bb 100644
--- a/app.go
+++ b/app.go
@@ -124,6 +124,7 @@ func run() error {
flags.StringVar(&srv.CustomNotFoundPage, "custom-404", "", "custom \"page not found\" to serve")
flags.IntVar(&srv.CustomNotFoundStatusCode, "custom-404-code", 0, "custtom status code for pages not found")
flags.BoolVar(&srv.HideFilesInMarkdown, "hide-files-in-markdown", false, "hide file and directory listing in markdown rendering")
+ flags.StringVar(&srv.CustomCSS, "custom-css-file", "", "path within the served files to a custom CSS file")
//nolint:wrapcheck // no need to wrap this error
return rootCmd.Execute()
diff --git a/internal/server/filter.go b/internal/server/filter.go
index e5ef80c..b07eb02 100644
--- a/internal/server/filter.go
+++ b/internal/server/filter.go
@@ -1,6 +1,8 @@
package server
-import "strings"
+import (
+ "strings"
+)
// forbiddenMatches is a list of filenames that are forbidden to be served.
// This list is used to prevent sensitive files from being
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index c6c1550..0b7fc94 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -171,6 +171,7 @@ func (s *Server) walk(requestedPath string, w http.ResponseWriter, r *http.Reque
"HideLinks": s.HideLinks,
"MarkdownContent": markdownContent.String(),
"MarkdownBeforeDir": s.MarkdownBeforeDir,
+ "CustomCSS": s.getCustomCSSURL(),
}
if err := s.templates.ExecuteTemplate(w, "app.tmpl", content); err != nil {
diff --git a/internal/server/server.go b/internal/server/server.go
index 427831d..f567eef 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -3,6 +3,8 @@ package server
import (
"html/template"
"io"
+ "path"
+ "strings"
"github.com/patrickdappollonio/http-server/internal/redirects"
)
@@ -49,6 +51,9 @@ type Server struct {
JWTSigningKey string `flagName:"jwt-key" validate:"omitempty,excluded_with=Username,excluded_with=Password"`
ValidateTimedJWT bool
+ // Custom CSS settings
+ CustomCSS string `flagName:"custom-css-file" validate:"omitempty,file"`
+
// Viper config settings
ConfigFilePrefix string
@@ -71,3 +76,23 @@ func (s *Server) IsBasicAuthEnabled() bool {
func (s *Server) SetVersion(version string) {
s.version = version
}
+
+// Get path to custom CSS for rendering on the web and ensuring
+// path prefix is set if needed
+func (s *Server) getCustomCSSURL() string {
+ if s.CustomCSS == "" {
+ return ""
+ }
+
+ css := s.CustomCSS
+
+ if s.PathPrefix != "" {
+ css = path.Join(s.PathPrefix, s.CustomCSS)
+ }
+
+ if !strings.HasPrefix(css, "/") {
+ css = "/" + css
+ }
+
+ return css
+}
diff --git a/internal/server/startup.go b/internal/server/startup.go
index ae9f744..a8f8cee 100644
--- a/internal/server/startup.go
+++ b/internal/server/startup.go
@@ -46,6 +46,10 @@ func (s *Server) PrintStartup() {
fmt.Fprintln(s.LogOutput, startupPrefix, "CORS headers enabled: adding \"Access-Control-Allow-Origin=*\" header")
}
+ if s.CustomCSS != "" {
+ fmt.Fprintln(s.LogOutput, startupPrefix, "Using custom CSS file:", s.CustomCSS)
+ }
+
if s.IsBasicAuthEnabled() {
fmt.Fprintln(s.LogOutput, startupPrefix, "Basic authentication enabled with username:", s.Username)
}
diff --git a/internal/server/templates/app.tmpl b/internal/server/templates/app.tmpl
index fd74207..6923692 100644
--- a/internal/server/templates/app.tmpl
+++ b/internal/server/templates/app.tmpl
@@ -11,7 +11,9 @@
- {{ if not .DisableMarkdown }}{{ end }}
+ {{- if not .DisableMarkdown }}{{ end }}
+
+ {{- if .CustomCSS }}{{ end }}
diff --git a/internal/server/validation.go b/internal/server/validation.go
index 6fd7c94..e5c1516 100644
--- a/internal/server/validation.go
+++ b/internal/server/validation.go
@@ -5,8 +5,10 @@ import (
"fmt"
"net/http"
"os"
+ "path/filepath"
"reflect"
"regexp"
+ "strings"
"github.com/go-playground/validator/v10"
"github.com/patrickdappollonio/http-server/internal/utils"
@@ -64,6 +66,14 @@ func (s *Server) Validate() error {
return errors.New("etag max size is required: set it with --etag-max-size")
}
+ // Validate that if custom CSS is set, that it lives in the path where
+ // we're serving files
+ if s.CustomCSS != "" {
+ if !validateIsFileInPath(s.Path, s.CustomCSS) {
+ return fmt.Errorf("css file path %q is outside the server's path %q: it must be served from the server itself", s.CustomCSS, s.Path)
+ }
+ }
+
size, err := utils.ParseSize(s.ETagMaxSize)
if err != nil {
return fmt.Errorf("unable to parse ETag max size: %w", err)
@@ -108,6 +118,20 @@ func validateIsPathPrefix(field validator.FieldLevel) bool {
return reIsPathPrefix.MatchString(field.Field().String())
}
+func validateIsFileInPath(basepath, file string) bool {
+ absbasepath, err := filepath.Abs(basepath)
+ if err != nil {
+ return false
+ }
+
+ absfile, err := filepath.Abs(file)
+ if err != nil {
+ return false
+ }
+
+ return strings.HasPrefix(absfile, absbasepath)
+}
+
func (s *Server) printWarningf(format string, args ...interface{}) {
if s.LogOutput != nil {
fmt.Fprintf(s.LogOutput, warnPrefix+format+"\n", args...)
From a291669dcb609248b2180ed5663d548937b27cd5 Mon Sep 17 00:00:00 2001
From: Patrick D'appollonio
<930925+patrickdappollonio@users.noreply.github.com>
Date: Thu, 17 Apr 2025 18:29:21 -0700
Subject: [PATCH 2/4] Fix README descriptions.
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index cc3c36d..74271f9 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ Flags:
--cors enable CORS support by setting the "Access-Control-Allow-Origin" header to "*"
--custom-404 string custom "page not found" to serve
--custom-404-code int custtom status code for pages not found
+ --custom-css-file string path within the served files to a custom CSS file
--disable-cache-buster disable the cache buster for assets from the directory listing feature
--disable-directory-listing disable the directory listing feature and return 404s for directories without index
--disable-etag disable etag header generation
@@ -75,6 +76,7 @@ Flags:
--etag-max-size string maximum size for etag header generation, where bigger size = more memory usage (default "5M")
--gzip enable gzip compression for supported content-types
-h, --help help for http-server
+ --hide-files-in-markdown hide file and directory listing in markdown rendering
--hide-links hide the links to this project's source code visible in the header and footer
--jwt-key string signing key for JWT authentication
--markdown-before-dir render markdown content before the directory listing
From 2873b0ee7dcb98f6c64cd0d2f299fc15c43c3c5f Mon Sep 17 00:00:00 2001
From: Patrick D'appollonio
<930925+patrickdappollonio@users.noreply.github.com>
Date: Tue, 22 Apr 2025 19:02:44 -0400
Subject: [PATCH 3/4] Also add custom CSS on full markdown files.
---
internal/server/handlers.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 6f2866a..2b7d146 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -113,6 +113,7 @@ func (s *Server) serveMarkdown(requestedPath string, w http.ResponseWriter, r *h
"HideLinks": s.HideLinks,
"MarkdownContent": markdownContent.String(),
"MarkdownBeforeDir": s.MarkdownBeforeDir,
+ "CustomCSS": s.getCustomCSSURL(),
}
if err := s.templates.ExecuteTemplate(w, "app.tmpl", content); err != nil {
From bc97a4082f97357c80915fede9b84d2df9926edb Mon Sep 17 00:00:00 2001
From: Patrick D'appollonio
<930925+patrickdappollonio@users.noreply.github.com>
Date: Tue, 22 Apr 2025 19:11:35 -0400
Subject: [PATCH 4/4] Bump versions.
---
.github/workflows/testing-commit.yaml | 2 +-
.golangci.yaml | 114 +++++++++++-----------
.goreleaser.yml | 3 +-
internal/mdrendering/goldmark_renderer.go | 2 +
4 files changed, 64 insertions(+), 57 deletions(-)
diff --git a/.github/workflows/testing-commit.yaml b/.github/workflows/testing-commit.yaml
index 068447d..9d3d7e1 100644
--- a/.github/workflows/testing-commit.yaml
+++ b/.github/workflows/testing-commit.yaml
@@ -16,7 +16,7 @@ jobs:
with:
go-version-file: go.mod
- name: Run GolangCI-Lint
- uses: golangci/golangci-lint-action@v6
+ uses: golangci/golangci-lint-action@v7
with:
version: latest
- name: Test application
diff --git a/.golangci.yaml b/.golangci.yaml
index a9b98ec..c852f31 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,21 +1,16 @@
+version: "2"
run:
- tests: false
concurrency: 5
- timeout: 3m
-
+ tests: false
linters:
- disable-all: true
+ default: none
enable:
- - gosimple
- - govet
- - ineffassign
- - staticcheck
- - unused
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
+ - copyloopvar
- decorder
- dogsled
- dupl
@@ -25,23 +20,21 @@ linters:
- errname
- errorlint
- exhaustive
- - copyloopvar
- ginkgolinter
- gocheckcompilerdirectives
- gochecksumtype
- gocritic
- gocyclo
- - gofmt
- - gofumpt
- goheader
- - goimports
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
+ - govet
- grouper
- importas
- inamedparam
+ - ineffassign
- ireturn
- loggercheck
- makezero
@@ -52,7 +45,6 @@ linters:
- nilerr
- nilnil
- noctx
- - nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
@@ -67,8 +59,7 @@ linters:
- sloglint
- spancheck
- sqlclosecheck
- - stylecheck
- - tenv
+ - staticcheck
- testableexamples
- testifylint
- testpackage
@@ -76,47 +67,60 @@ linters:
- tparallel
- unconvert
- unparam
+ - unused
- usestdlibvars
- wastedassign
- whitespace
- wrapcheck
- zerologlint
-
-linters-settings:
- perfsprint:
- int-conversion: false
- err-error: false
- errorf: true
- sprintf1: true
- strconcat: false
-
- ireturn:
- allow:
- - error
- - http.Handler
-
- gosec:
- confidence: medium
- excludes:
- - G401 # Use of weak cryptographic primitive: we're using sha1 for etag generation
- - G505 # Blocklisted import crypto/sha1: we're using sha1 for etag generation
-
- stylecheck:
- checks:
- - "all"
- - "-ST1003" # this is covered by a different linter
-
- gocyclo:
- min-complexity: 60
-
- staticcheck:
- checks:
- - "all"
- - "-SA1019" # keeping some deprecated code for compatibility
-
- gocritic:
- enable-all: true
- disabled-checks:
- - appendAssign
- - unnamedResult
- - badRegexp
+ settings:
+ gocritic:
+ enable-all: true
+ disabled-checks:
+ - appendAssign
+ - unnamedResult
+ - badRegexp
+ gocyclo:
+ min-complexity: 60
+ gosec:
+ excludes:
+ - G401
+ - G505
+ confidence: medium
+ ireturn:
+ allow:
+ - error
+ - http.Handler
+ perfsprint:
+ int-conversion: false
+ err-error: false
+ errorf: true
+ sprintf1: true
+ strconcat: false
+ staticcheck:
+ checks:
+ - -SA1019
+ - -ST1003
+ - all
+ exclusions:
+ generated: lax
+ presets:
+ - comments
+ - common-false-positives
+ - legacy
+ - std-error-handling
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
+formatters:
+ enable:
+ - gofmt
+ - gofumpt
+ - goimports
+ exclusions:
+ generated: lax
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
diff --git a/.goreleaser.yml b/.goreleaser.yml
index fd96a40..6a61f4e 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -26,7 +26,8 @@ archives:
{{- else }}{{ .Arch }}{{ end }}
format_overrides:
- goos: windows
- format: zip
+ formats:
+ - zip
checksum:
name_template: "checksums.txt"
snapshot:
diff --git a/internal/mdrendering/goldmark_renderer.go b/internal/mdrendering/goldmark_renderer.go
index c862a1c..10a0a88 100644
--- a/internal/mdrendering/goldmark_renderer.go
+++ b/internal/mdrendering/goldmark_renderer.go
@@ -46,6 +46,8 @@ func (r *HTTPServerRendering) renderImageAlign(w util.BufWriter, source []byte,
}
w.WriteString(`" alt="`)
+
+ //nolint:staticcheck // skipping temporarily until we decide on keeping goldmark
w.Write(util.EscapeHTML(n.Text(source)))
w.WriteString(`"`)
if n.Title != nil {