Skip to content

Commit

Permalink
Clean up http-server tree structure. Fixes CSS in banner. (#147)
Browse files Browse the repository at this point in the history
* Clean up http-server tree structure. Fixes CSS in banner.

* Cleanup go.mod and Readme.

* Remove random string generator.

* Add documentation for functions.

* Prevent using casting and use "errors.As()" instead.

* Work through golangci-lint messages.

* Viper does not support a check for config file not found.
  • Loading branch information
patrickdappollonio authored Jan 27, 2025
1 parent bfebd36 commit 2c2591c
Show file tree
Hide file tree
Showing 33 changed files with 590 additions and 369 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/testing-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@ jobs:
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run GolangCI-Lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60
- name: Test application
run: go test ./...
run: go test -v ./...
122 changes: 122 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
run:
tests: false
concurrency: 5
timeout: 3m

linters:
disable-all: true
enable:
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- asasalint
- asciicheck
- bidichk
- bodyclose
- contextcheck
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- exhaustive
- copyloopvar
- ginkgolinter
- gocheckcompilerdirectives
- gochecksumtype
- gocritic
- gocyclo
- gofmt
- gofumpt
- goheader
- goimports
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- grouper
- importas
- inamedparam
- ireturn
- loggercheck
- makezero
- mirror
- misspell
- musttag
- nakedret
- nilerr
- nilnil
- noctx
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- stylecheck
- tenv
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- 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
75 changes: 39 additions & 36 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"bufio"
"errors"
"fmt"
"io"
"log"
"os"
Expand All @@ -23,10 +25,10 @@ var version = "development"

func run() error {
// Server and settings holder
var server server.Server
var srv server.Server

// Define the config prefix for config files
server.ConfigFilePrefix = configFilePrefix
srv.ConfigFilePrefix = configFilePrefix

// Create a logger
logger := log.New(os.Stdout, "", log.LstdFlags)
Expand All @@ -53,31 +55,31 @@ func run() error {
SilenceErrors: true,

// Bind viper settings against the root command
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
return bindCobraAndViper(cmd)
},

// Execute the server
RunE: func(cmd *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
// Set the message output to the appropriate writer
server.LogOutput = cmd.OutOrStdout()
server.SetVersion(version)
srv.LogOutput = cmd.OutOrStdout()
srv.SetVersion(version)

// Validate fields to make sure they're correct
if err := server.Validate(); err != nil {
return err
if err := srv.Validate(); err != nil {
return fmt.Errorf("unable to validate configuration: %w", err)
}

// Load redirections file if enabled
if err := server.LoadRedirectionsIfEnabled(); err != nil {
return err
if err := srv.LoadRedirectionsIfEnabled(); err != nil {
return fmt.Errorf("unable to load redirections file: %w", err)
}

// Print some sane defaults and some information about the request
server.PrintStartup()
srv.PrintStartup()

// Run the server
return server.ListenAndServe()
return srv.ListenAndServe()
},
}

Expand All @@ -100,28 +102,29 @@ func run() error {

// Define the flags for the root command
flags := rootCmd.Flags()
flags.IntVarP(&server.Port, "port", "p", 5000, "port to configure the server to listen on")
flags.StringVarP(&server.Path, "path", "d", "./", "path to the directory you want to serve")
flags.StringVar(&server.PathPrefix, "pathprefix", "/", "path prefix for the URL where the server will listen on")
flags.BoolVar(&server.CorsEnabled, "cors", false, "enable CORS support by setting the \"Access-Control-Allow-Origin\" header to \"*\"")
flags.StringVar(&server.Username, "username", "", "username for basic authentication")
flags.StringVar(&server.Password, "password", "", "password for basic authentication")
flags.StringVar(&server.PageTitle, "title", "", "title of the directory listing page")
flags.BoolVar(&server.HideLinks, "hide-links", false, "hide the links to this project's source code visible in the header and footer")
flags.BoolVar(&server.DisableCacheBuster, "disable-cache-buster", false, "disable the cache buster for assets from the directory listing feature")
flags.BoolVar(&server.DisableMarkdown, "disable-markdown", false, "disable the markdown rendering feature")
flags.BoolVar(&server.MarkdownBeforeDir, "markdown-before-dir", false, "render markdown content before the directory listing")
flags.StringVar(&server.JWTSigningKey, "jwt-key", "", "signing key for JWT authentication")
flags.BoolVar(&server.ValidateTimedJWT, "ensure-unexpired-jwt", false, "enable time validation for JWT claims \"exp\" and \"nbf\"")
flags.StringVar(&server.BannerMarkdown, "banner", "", "markdown text to be rendered at the top of the directory listing page")
flags.BoolVar(&server.ETagDisabled, "disable-etag", false, "disable etag header generation")
flags.StringVar(&server.ETagMaxSize, "etag-max-size", "5M", "maximum size for etag header generation, where bigger size = more memory usage")
flags.BoolVar(&server.GzipEnabled, "gzip", false, "enable gzip compression for supported content-types")
flags.BoolVar(&server.DisableRedirects, "disable-redirects", false, "disable redirection file handling")
flags.BoolVar(&server.DisableDirectoryList, "disable-directory-listing", false, "disable the directory listing feature and return 404s for directories without index")
flags.StringVar(&server.CustomNotFoundPage, "custom-404", "", "custom \"page not found\" to serve")
flags.IntVar(&server.CustomNotFoundStatusCode, "custom-404-code", 0, "custtom status code for pages not found")

flags.IntVarP(&srv.Port, "port", "p", 5000, "port to configure the server to listen on")
flags.StringVarP(&srv.Path, "path", "d", "./", "path to the directory you want to serve")
flags.StringVar(&srv.PathPrefix, "pathprefix", "/", "path prefix for the URL where the server will listen on")
flags.BoolVar(&srv.CorsEnabled, "cors", false, "enable CORS support by setting the \"Access-Control-Allow-Origin\" header to \"*\"")
flags.StringVar(&srv.Username, "username", "", "username for basic authentication")
flags.StringVar(&srv.Password, "password", "", "password for basic authentication")
flags.StringVar(&srv.PageTitle, "title", "", "title of the directory listing page")
flags.BoolVar(&srv.HideLinks, "hide-links", false, "hide the links to this project's source code visible in the header and footer")
flags.BoolVar(&srv.DisableCacheBuster, "disable-cache-buster", false, "disable the cache buster for assets from the directory listing feature")
flags.BoolVar(&srv.DisableMarkdown, "disable-markdown", false, "disable the markdown rendering feature")
flags.BoolVar(&srv.MarkdownBeforeDir, "markdown-before-dir", false, "render markdown content before the directory listing")
flags.StringVar(&srv.JWTSigningKey, "jwt-key", "", "signing key for JWT authentication")
flags.BoolVar(&srv.ValidateTimedJWT, "ensure-unexpired-jwt", false, "enable time validation for JWT claims \"exp\" and \"nbf\"")
flags.StringVar(&srv.BannerMarkdown, "banner", "", "markdown text to be rendered at the top of the directory listing page")
flags.BoolVar(&srv.ETagDisabled, "disable-etag", false, "disable etag header generation")
flags.StringVar(&srv.ETagMaxSize, "etag-max-size", "5M", "maximum size for etag header generation, where bigger size = more memory usage")
flags.BoolVar(&srv.GzipEnabled, "gzip", false, "enable gzip compression for supported content-types")
flags.BoolVar(&srv.DisableRedirects, "disable-redirects", false, "disable redirection file handling")
flags.BoolVar(&srv.DisableDirectoryList, "disable-directory-listing", false, "disable the directory listing feature and return 404s for directories without index")
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")

//nolint:wrapcheck // no need to wrap this error
return rootCmd.Execute()
}

Expand Down Expand Up @@ -179,8 +182,8 @@ func bindCobraAndViper(rootCommand *cobra.Command) error {
if err := v.ReadInConfig(); err != nil {
// If the configuration file was not found, it's all good, we ignore
// the failure and proceed with the default settings
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return err
if f := (&viper.ConfigFileNotFoundError{}); errors.As(err, &f) {
return fmt.Errorf("unable to read configuration file: %w", err)
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/server/ctypes.go → internal/ctype/ctypes.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package server
package ctype

var ctypes = []struct {
Extension []string
Expand Down Expand Up @@ -191,7 +191,7 @@ var ctypes = []struct {
{[]string{".tgz"}, nil, "application/x-gzip"},
}

func getContentTypeForFilename(name string) string {
func GetContentTypeForFilename(name string) string {
for _, ct := range ctypes {
for _, internalName := range ct.ExactNames {
if name == internalName {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package server
package ctype

import "testing"

Expand Down Expand Up @@ -54,7 +54,7 @@ func Test_getContentTypeForExtension(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.filename, func(t *testing.T) {
if got := getContentTypeForFilename(tt.filename); got != tt.want {
if got := GetContentTypeForFilename(tt.filename); got != tt.want {
t.Errorf("getContentTypeForFilename() filename: %q -- got: %q, want %q", tt.filename, got, tt.want)
}
})
Expand Down
77 changes: 77 additions & 0 deletions internal/mdrendering/goldmark_renderer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package mdrendering

import (
"bytes"
"fmt"

"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)

type HTTPServerRendering struct {
html.Config
}

// RegisterFuncs implements goldmark.Renderer.
func (r *HTTPServerRendering) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHeading, r.renderHeading)
reg.Register(ast.KindImage, r.renderImageAlign)
}

func (r *HTTPServerRendering) renderImageAlign(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
w.WriteString("<img src=\"")

if r.Unsafe || !html.IsDangerousURL(n.Destination) {
u := util.URLEscape(n.Destination, true)

switch {
case bytes.HasSuffix(n.Destination, []byte(`#align-right`)):
w.Write(util.EscapeHTML(bytes.TrimSuffix(u, []byte(`#align-right`))))
w.WriteString(`" align="right`)
case bytes.HasSuffix(n.Destination, []byte(`#align-center`)):
w.Write(util.EscapeHTML(bytes.TrimSuffix(u, []byte(`#align-center`))))
w.WriteString(`" align="center`)
case bytes.HasSuffix(n.Destination, []byte(`#align-left`)):
w.Write(util.EscapeHTML(bytes.TrimSuffix(u, []byte(`#align-left`))))
w.WriteString(`" align="left`)
default:
w.Write(util.EscapeHTML(u))
}
}

w.WriteString(`" alt="`)
w.Write(util.EscapeHTML(n.Text(source)))
w.WriteString(`"`)
if n.Title != nil {
w.WriteString(` title="`)
r.Writer.Write(w, n.Title)
w.WriteString(`"`)
}
if r.XHTML {
w.WriteString(" />")
} else {
w.WriteString(">")
}
return ast.WalkSkipChildren, nil
}

func (r *HTTPServerRendering) renderHeading(w util.BufWriter, _ []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
hn := node.(*ast.Heading)

slug, _ := node.AttributeString("id")

if entering {
node.SetAttribute([]byte("id"), slug)
fmt.Fprintf(w, `<h%d class="content-header" id="%s">`, hn.Level, slug)
return ast.WalkContinue, nil
}

fmt.Fprintf(w, `<a href="#%s" tabindex="-1"><i class="fas fa-link"></i></a></h%d>`, slug, hn.Level)
return ast.WalkContinue, nil
}
3 changes: 2 additions & 1 deletion internal/mw/cors.go → internal/middlewares/cors.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package mw
package middlewares

import "net/http"

// EnableCORS returns a middleware that enables CORS for the next handler.
func EnableCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package mw
package middlewares

import (
"fmt"
Expand Down
Loading

0 comments on commit 2c2591c

Please sign in to comment.