Skip to content

Add support for custom CSS #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/testing-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
114 changes: 59 additions & 55 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -52,7 +45,6 @@ linters:
- nilerr
- nilnil
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
Expand All @@ -67,56 +59,68 @@ linters:
- sloglint
- spancheck
- sqlclosecheck
- stylecheck
- tenv
- staticcheck
- testableexamples
- testifylint
- testpackage
- thelper
- 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$
3 changes: 2 additions & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ archives:
{{- else }}{{ .Arch }}{{ end }}
format_overrides:
- goos: windows
format: zip
formats:
- zip
checksum:
name_template: "checksums.txt"
snapshot:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ 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
--disable-markdown disable the markdown rendering feature
--disable-redirects disable redirection file handling
--ensure-unexpired-jwt enable time validation for JWT claims "exp" and "nbf"
--etag-max-size string maximum size for etag header generation, where bigger size = more memory usage (default "5M")
--render-all-markdown if enabled, all Markdown files will be rendered using the same rendering as the directory listing READMEs
--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
Expand All @@ -103,6 +103,7 @@ Flags:
-d, --path string path to the directory you want to serve (default "./")
--pathprefix string path prefix for the URL where the server will listen on (default "/")
-p, --port int port to configure the server to listen on (default 5000)
--render-all-markdown if enabled, all Markdown files will be rendered using the same rendering as the directory listing READMEs
--title string title of the directory listing page
--username string username for basic authentication
-v, --version version for http-server
Expand Down
1 change: 1 addition & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
flags.BoolVar(&srv.FullMarkdownRender, "render-all-markdown", false, "if enabled, all Markdown files will be rendered using the same rendering as the directory listing READMEs")

//nolint:wrapcheck // no need to wrap this error
Expand Down
2 changes: 2 additions & 0 deletions internal/mdrendering/goldmark_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion internal/server/filter.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -213,6 +214,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 {
Expand Down
25 changes: 25 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package server
import (
"html/template"
"io"
"path"
"strings"

"github.com/patrickdappollonio/http-server/internal/redirects"
)
Expand Down Expand Up @@ -50,6 +52,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

Expand All @@ -72,3 +77,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
}
4 changes: 4 additions & 0 deletions internal/server/startup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion internal/server/templates/app.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<link rel="stylesheet" href="{{ assetpath "roboto-font.css" }}">
<link rel="stylesheet" href="{{ assetpath "fontawesome-6.2.0.css" }}">
<link rel="icon" type="image/svg+xml" href="{{ assetpath "file-server.svg" }}">
{{ if not .DisableMarkdown }}<link rel="stylesheet" href="{{ assetpath "gfm.css" }}">{{ end }}
{{- if not .DisableMarkdown }}<link rel="stylesheet" href="{{ assetpath "gfm.css" }}">{{ end }}
<!-- {{ printf "custom css: %#v" .CustomCSS }} -->
{{- if .CustomCSS }}<link rel="stylesheet" href="{{ .CustomCSS }}">{{ end }}
</head>


Expand Down
24 changes: 24 additions & 0 deletions internal/server/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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...)
Expand Down