mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-17 00:44:20 +00:00
Linting and formatting
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
[tools]
|
||||
go = "latest"
|
||||
"golangci-lint" = "latest"
|
||||
pitchfork = "latest"
|
||||
|
||||
[env]
|
||||
@@ -11,6 +12,9 @@ auto = true
|
||||
[tasks.dev]
|
||||
run = "go run ./cmd/meshtender"
|
||||
|
||||
[tasks.lint]
|
||||
run = "golangci-lint run"
|
||||
|
||||
[tasks.seed]
|
||||
run = "go run ./cmd/meshtender --seed"
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
issues:
|
||||
# Report everything; the defaults (max-same-issues: 3, max-issues-per-linter:
|
||||
# 50) hide repeated findings and make fixing a whack-a-mole.
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters:
|
||||
default: standard # errcheck, govet, ineffassign, staticcheck, unused
|
||||
enable:
|
||||
- gosec # security analyzer (injection, weak crypto, hardcoded creds, ...)
|
||||
exclusions:
|
||||
rules:
|
||||
# Test code doesn't warrant errcheck/gosec scrutiny (unchecked Close on
|
||||
# throwaway connections, hand-built insecure cookies in fixtures, etc.).
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- gosec
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
@@ -1,5 +1,5 @@
|
||||
# Builds the OCI image and pushes it to the Forgejo registry. Runs only after
|
||||
# tests pass, and only on the main branch and on tags (not on pull requests).
|
||||
# tests and lint pass, and only on the main branch and on tags (not on PRs).
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
@@ -7,6 +7,7 @@ when:
|
||||
|
||||
depends_on:
|
||||
- test
|
||||
- lint
|
||||
|
||||
steps:
|
||||
publish:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Runs golangci-lint (staticcheck + gosec + the standard correctness set, per
|
||||
# .golangci.yml) on every push and pull request. Tracks :latest to match the
|
||||
# unpinned golangci-lint in .config/mise/config.toml used for local runs.
|
||||
when:
|
||||
- event: [push, pull_request]
|
||||
|
||||
steps:
|
||||
lint:
|
||||
image: golangci/golangci-lint:latest
|
||||
environment:
|
||||
# go.mod may target a newer Go than the image bundles; let the toolchain
|
||||
# be fetched on demand rather than failing.
|
||||
GOTOOLCHAIN: auto
|
||||
commands:
|
||||
- golangci-lint run
|
||||
|
||||
depends_on: []
|
||||
@@ -269,7 +269,7 @@ func (s *Service) LoginDiscoverableFinish(w http.ResponseWriter, r *http.Request
|
||||
if len(userHandle) != 8 {
|
||||
return nil, errors.New("unrecognized user handle")
|
||||
}
|
||||
uid := int64(binary.BigEndian.Uint64(userHandle))
|
||||
uid := int64(binary.BigEndian.Uint64(userHandle)) //nolint:gosec // G115: decodes the 8-byte WebAuthn handle encoded from our int64 user ID
|
||||
u, err := s.store.GetUserByID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -155,7 +155,7 @@ func (s *Service) startAuth(w http.ResponseWriter, r *http.Request, next, page s
|
||||
http.Error(w, "could not start sign-in", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
http.SetCookie(w, &http.Cookie{ //nolint:gosec // G124: HttpOnly+SameSite set below; Secure is gated on TLS via s.secure
|
||||
Name: cookieName(stateCookie, s.secure),
|
||||
Value: state,
|
||||
Path: "/",
|
||||
@@ -165,7 +165,7 @@ func (s *Service) startAuth(w http.ResponseWriter, r *http.Request, next, page s
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
q := url.Values{"next": {next}, "state": {state}}
|
||||
http.Redirect(w, r, s.authOrigin(r)+page+"?"+q.Encode(), http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.authOrigin(r)+page+"?"+q.Encode(), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// SessionCallback runs on the app host. It verifies the state nonce against the
|
||||
@@ -206,11 +206,11 @@ func (s *Service) SessionCallback(w http.ResponseWriter, r *http.Request) {
|
||||
// round (discovery renders anonymous until the next sign-in).
|
||||
if s.rootHost != "" {
|
||||
if code, err := s.store.CreateAuthCode(ctx, userID, loginID, next); err == nil {
|
||||
http.Redirect(w, r, s.rootOrigin(r)+"/session/beacon?code="+url.QueryEscape(code), http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.rootOrigin(r)+"/session/beacon?code="+url.QueryEscape(code), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, next, http.StatusSeeOther)
|
||||
http.Redirect(w, r, next, http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// BeaconCallback runs on the root (discovery) host. It redeems a single-use code
|
||||
@@ -234,11 +234,11 @@ func (s *Service) BeaconCallback(w http.ResponseWriter, r *http.Request) {
|
||||
if ok {
|
||||
_ = s.loginWithID(ctx, userID, loginID)
|
||||
}
|
||||
http.Redirect(w, r, s.appOrigin(r)+next, http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.appOrigin(r)+next, http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
func clearStateCookie(w http.ResponseWriter, secure bool) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
http.SetCookie(w, &http.Cookie{ //nolint:gosec // G124: clears the state cookie (MaxAge<0); attributes mirror the original
|
||||
Name: cookieName(stateCookie, secure),
|
||||
Value: "",
|
||||
Path: "/",
|
||||
|
||||
@@ -36,7 +36,7 @@ func (s *Service) SignupPassword(w http.ResponseWriter, r *http.Request) {
|
||||
redirectErr(w, r, "/signup", "Could not start session.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, s.PostAuthRedirect(r, u.ID), http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.PostAuthRedirect(r, u.ID), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// LoginPassword handles a form-based password sign-in.
|
||||
@@ -53,7 +53,7 @@ func (s *Service) LoginPassword(w http.ResponseWriter, r *http.Request) {
|
||||
redirectErr(w, r, "/login", "Could not start session.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, s.PostAuthRedirect(r, u.ID), http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.PostAuthRedirect(r, u.ID), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
func redirectErr(w http.ResponseWriter, r *http.Request, path, msg string) {
|
||||
|
||||
@@ -21,7 +21,7 @@ type webauthnUser struct {
|
||||
|
||||
func (u *webauthnUser) WebAuthnID() []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(u.user.ID))
|
||||
binary.BigEndian.PutUint64(b, uint64(u.user.ID)) //nolint:gosec // G115: user row ID is non-negative
|
||||
return b
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ func (s *Handlers) pageLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// Already signed in (e.g. an existing auth-host SSO session): skip the
|
||||
// ceremony and hand straight off, rather than re-prompting.
|
||||
if uid := s.Auth.CurrentUserID(ctx); uid != 0 {
|
||||
http.Redirect(w, r, s.Auth.PostAuthRedirect(r, uid), http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.Auth.PostAuthRedirect(r, uid), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
return
|
||||
}
|
||||
s.Render(w, r, "login.html", map[string]any{
|
||||
@@ -106,7 +106,7 @@ func (s *Handlers) pageSignup(w http.ResponseWriter, r *http.Request) {
|
||||
s.Auth.SetNext(ctx, r.URL.Query().Get("next"))
|
||||
s.Auth.SetAuthState(ctx, r.URL.Query().Get("state"))
|
||||
if uid := s.Auth.CurrentUserID(ctx); uid != 0 {
|
||||
http.Redirect(w, r, s.Auth.PostAuthRedirect(r, uid), http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.Auth.PostAuthRedirect(r, uid), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
return
|
||||
}
|
||||
s.Render(w, r, "signup.html", map[string]any{
|
||||
@@ -122,9 +122,9 @@ func (s *Handlers) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
|
||||
_ = s.Auth.Logout(r.Context())
|
||||
switch {
|
||||
case s.Cfg.RootHost != "":
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.RootHost)+"/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.RootHost)+"/", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
case s.Cfg.PrimaryHost != "":
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.PrimaryHost)+"/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.PrimaryHost)+"/", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
default:
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func (s *Handlers) handleSaveOrgConfig(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "could not save", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r)+"/config", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r)+"/config", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// parseProfiles reads the repeated profile blocks from the form into store inputs
|
||||
|
||||
@@ -78,7 +78,7 @@ func (s *Handlers) wsConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
// spurious "tx done timeout". We don't need it; the repeater's reply is the
|
||||
// real confirmation we wait for.
|
||||
modem := hardware.NewKissModem(bridge, hardware.WithTxFlowControl(0))
|
||||
defer modem.Close()
|
||||
defer func() { _ = modem.Close() }()
|
||||
|
||||
server := s.Identity.Local()
|
||||
debug := r.URL.Query().Get("debug") == "1"
|
||||
@@ -139,10 +139,10 @@ func (s *Handlers) wsConfirm(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
_ = bridge.Status("info", "Tuning radio…")
|
||||
if err := modem.SetRadio(&hardware.RadioConfig{
|
||||
FreqHz: uint32(rep.RadioFreqHz),
|
||||
BwHz: uint32(rep.RadioBwHz),
|
||||
SF: uint8(rep.RadioSF),
|
||||
CR: uint8(rep.RadioCR),
|
||||
FreqHz: uint32(rep.RadioFreqHz), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
BwHz: uint32(rep.RadioBwHz), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
SF: uint8(rep.RadioSF), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
CR: uint8(rep.RadioCR), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
}); err != nil {
|
||||
_ = bridge.Status("error", "set radio: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -131,8 +131,8 @@ func TestConfirmRoundTrip(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("parse anon req: %v", err)
|
||||
}
|
||||
if anon.Destination != repeater.Identity.Hash()[0] {
|
||||
t.Fatalf("login addressed to 0x%02x, want repeater 0x%02x", anon.Destination, repeater.Identity.Hash()[0])
|
||||
if anon.Destination != repeater.Hash()[0] {
|
||||
t.Fatalf("login addressed to 0x%02x, want repeater 0x%02x", anon.Destination, repeater.Hash()[0])
|
||||
}
|
||||
shared, err := repeater.SharedSecret(serverID)
|
||||
if err != nil {
|
||||
@@ -198,7 +198,7 @@ func buildResponseFrame(t *testing.T, repeater meshcore.LocalIdentity, server me
|
||||
}
|
||||
resp := &meshcore.Response{
|
||||
Destination: server.Hash()[0],
|
||||
Source: repeater.Identity.Hash()[0],
|
||||
Source: repeater.Hash()[0],
|
||||
MAC: [2]byte{enc[0], enc[1]},
|
||||
EncryptedPayload: enc[2:],
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestConfirmFetchesLocation(t *testing.T) {
|
||||
resp[7] = 3
|
||||
plain := append([]byte{0x00, meshcore.PayloadTypeResponse}, resp...) // [path_len=0][type][response]
|
||||
enc, _ := meshcore.EncryptThenMAC(shared, plain)
|
||||
p := &meshcore.Path{Destination: serverID.Hash()[0], Source: repeater.Identity.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
p := &meshcore.Path{Destination: serverID.Hash()[0], Source: repeater.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
payload, _ := p.ToBytes()
|
||||
lp := &meshcore.Packet{Header: meshcore.MakeHeader(meshcore.RouteTypeFlood, meshcore.PayloadTypePath, 0), Payload: payload}
|
||||
raw, _ := lp.ToBytes()
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestConfirmLoginRetry(t *testing.T) {
|
||||
respData[7] = 3
|
||||
body := append([]byte{0x00, meshcore.PayloadTypeResponse}, respData...)
|
||||
enc, _ := meshcore.EncryptThenMAC(shared, body)
|
||||
p := &meshcore.Path{Destination: serverID.Hash()[0], Source: repeater.Identity.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
p := &meshcore.Path{Destination: serverID.Hash()[0], Source: repeater.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
payload, _ := p.ToBytes()
|
||||
lp := &meshcore.Packet{Header: meshcore.MakeHeader(meshcore.RouteTypeFlood, meshcore.PayloadTypePath, 0), Payload: payload}
|
||||
raw, _ := lp.ToBytes()
|
||||
|
||||
@@ -197,7 +197,7 @@ func (s *Handlers) wsConsole(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
bridge := wsbridge.New(ctx, ws)
|
||||
modem := hardware.NewKissModem(bridge, hardware.WithTxFlowControl(0))
|
||||
defer modem.Close()
|
||||
defer func() { _ = modem.Close() }()
|
||||
server := s.Identity.Local()
|
||||
|
||||
// All commands go through one exchanger: rate-limited, monotonic timestamps,
|
||||
@@ -272,10 +272,10 @@ func (s *Handlers) wsConsole(w http.ResponseWriter, r *http.Request) {
|
||||
// Tune the modem to the repeater's channel.
|
||||
_ = bridge.Status("info", "Tuning radio…")
|
||||
if err := modem.SetRadio(&hardware.RadioConfig{
|
||||
FreqHz: uint32(rep.RadioFreqHz),
|
||||
BwHz: uint32(rep.RadioBwHz),
|
||||
SF: uint8(rep.RadioSF),
|
||||
CR: uint8(rep.RadioCR),
|
||||
FreqHz: uint32(rep.RadioFreqHz), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
BwHz: uint32(rep.RadioBwHz), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
SF: uint8(rep.RadioSF), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
CR: uint8(rep.RadioCR), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
}); err != nil {
|
||||
_ = bridge.Status("error", "set radio: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -139,7 +139,7 @@ func TestConsoleRoundTrip(t *testing.T) {
|
||||
resp[7] = 3
|
||||
body := append([]byte{0x00, meshcore.PayloadTypeResponse}, resp...)
|
||||
enc, _ := meshcore.EncryptThenMAC(shared, body)
|
||||
p := &meshcore.Path{Destination: serverID.Hash()[0], Source: repeater.Identity.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
p := &meshcore.Path{Destination: serverID.Hash()[0], Source: repeater.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
payload, _ := p.ToBytes()
|
||||
lp := &meshcore.Packet{Header: meshcore.MakeHeader(meshcore.RouteTypeFlood, meshcore.PayloadTypePath, 0), Payload: payload}
|
||||
raw, _ := lp.ToBytes()
|
||||
|
||||
@@ -128,10 +128,10 @@ func TestValidCommandText(t *testing.T) {
|
||||
bad := []string{
|
||||
"",
|
||||
" ",
|
||||
"ver\nreboot", // newline (chaining attempt)
|
||||
"ver\rreboot", // carriage return
|
||||
"set name a\x00b", // null
|
||||
"cmd\x7f", // delete
|
||||
"ver\nreboot", // newline (chaining attempt)
|
||||
"ver\rreboot", // carriage return
|
||||
"set name a\x00b", // null
|
||||
"cmd\x7f", // delete
|
||||
strings.Repeat("a", maxCommandLen+1), // too long
|
||||
}
|
||||
for _, s := range bad {
|
||||
@@ -145,7 +145,7 @@ func TestValidCommandText(t *testing.T) {
|
||||
// a valid operation, and that its feature is listed in featureOrder so it renders
|
||||
// in a known position (a feature missing from featureOrder still shows, but at
|
||||
// the end — this catches the Go list drifting from the DB). Requires the *_test
|
||||
// database. The DB also enforces feature<>'' and operation IN (...) via CHECK.
|
||||
// database. The DB also enforces feature<>” and operation IN (...) via CHECK.
|
||||
func TestCommandFeatureCoverage(t *testing.T) {
|
||||
t.Parallel()
|
||||
cat := loadRealCatalog(t)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// txtRecordName is the DNS label an org adds under its domain to prove ownership.
|
||||
const txtRecordPrefix = "_meshtender."
|
||||
const txtRecordPrefix = "_meshtender." //nolint:unused // wired when the /domains routes are enabled (see web.go)
|
||||
|
||||
// normalizeHostname lowercases and strips any scheme, path, port, or trailing
|
||||
// dot from user-entered domain input, returning "" if it isn't a plausible host.
|
||||
@@ -27,7 +27,8 @@ func normalizeHostname(raw string) string {
|
||||
return ""
|
||||
}
|
||||
for _, c := range h {
|
||||
if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '.' || c == '-') {
|
||||
ok := c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '.' || c == '-'
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -35,7 +36,7 @@ func normalizeHostname(raw string) string {
|
||||
}
|
||||
|
||||
// handleAddOrgDomain registers a new custom domain for an org (admin only).
|
||||
func (s *Handlers) handleAddOrgDomain(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Handlers) handleAddOrgDomain(w http.ResponseWriter, r *http.Request) { //nolint:unused // wired when the /domains routes are enabled (see web.go)
|
||||
id, ok := s.requireOrgAdmin(w, r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -52,12 +53,12 @@ func (s *Handlers) handleAddOrgDomain(w http.ResponseWriter, r *http.Request) {
|
||||
orgErr(w, r, "Could not add domain.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleVerifyOrgDomain checks the org's DNS TXT record carries the domain's
|
||||
// verification token, then marks it verified (admin only).
|
||||
func (s *Handlers) handleVerifyOrgDomain(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Handlers) handleVerifyOrgDomain(w http.ResponseWriter, r *http.Request) { //nolint:unused // wired when the /domains routes are enabled (see web.go)
|
||||
id, ok := s.requireOrgAdmin(w, r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -85,7 +86,7 @@ func (s *Handlers) handleVerifyOrgDomain(w http.ResponseWriter, r *http.Request)
|
||||
orgErr(w, r, "Could not save verification.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// txtRecordsHaveToken reports whether any TXT record exactly matches the token
|
||||
@@ -100,7 +101,7 @@ func txtRecordsHaveToken(records []string, token string) bool {
|
||||
}
|
||||
|
||||
// handleDeleteOrgDomain removes a custom domain (admin only).
|
||||
func (s *Handlers) handleDeleteOrgDomain(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Handlers) handleDeleteOrgDomain(w http.ResponseWriter, r *http.Request) { //nolint:unused // wired when the /domains routes are enabled (see web.go)
|
||||
id, ok := s.requireOrgAdmin(w, r)
|
||||
if !ok {
|
||||
return
|
||||
@@ -114,5 +115,5 @@ func (s *Handlers) handleDeleteOrgDomain(w http.ResponseWriter, r *http.Request)
|
||||
orgErr(w, r, "Could not remove domain.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (s *Handlers) handleSaveOrgCommands(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "could not save", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r)+"/my-commands", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r)+"/my-commands", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
return
|
||||
}
|
||||
ceiling, err := s.orgCeilingCommands(r)
|
||||
@@ -133,7 +133,7 @@ func (s *Handlers) handleSaveOrgCommands(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, "could not save", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r)+"/my-commands", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r)+"/my-commands", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// orgCeilingCommands returns the catalog commands an org is ever permitted to run
|
||||
|
||||
@@ -62,7 +62,7 @@ func (s *Handlers) handleCreateOrg(w http.ResponseWriter, r *http.Request) {
|
||||
web.RedirectErr(w, r, "/orgs/new", "Could not create organization.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+org.Slug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+org.Slug, http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// pageOrg shows an org's home. Members get the full management view; everyone
|
||||
@@ -287,7 +287,7 @@ func (s *Handlers) handleEditOrg(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
// The slug may have changed; redirect to the new canonical URL.
|
||||
http.Redirect(w, r, "/orgs/"+slug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+slug, http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleSetOrgLinks replaces an org's whole set of social/site links from the
|
||||
@@ -344,7 +344,7 @@ func (s *Handlers) handleSetOrgLinks(w http.ResponseWriter, r *http.Request) {
|
||||
orgErr(w, r, "Could not save links.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// validLinkURL reports whether s is an absolute http(s) URL with a host. Limiting
|
||||
@@ -413,7 +413,7 @@ func (s *Handlers) pageJoinOrg(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if isMember {
|
||||
http.Redirect(w, r, "/orgs/"+org.Slug, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+org.Slug, http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
return
|
||||
}
|
||||
hasRepeaters, err := s.Store.OwnsAnyRepeater(r.Context(), uid)
|
||||
@@ -444,7 +444,7 @@ func (s *Handlers) handleJoinOrg(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/orgs/"+orgParam(r), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleSetOrgMember promotes/demotes/removes a member (admin only).
|
||||
@@ -479,5 +479,5 @@ func (s *Handlers) handleSetOrgMember(w http.ResponseWriter, r *http.Request) {
|
||||
memberErr("Could not update member.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, membersURL, http.StatusSeeOther)
|
||||
http.Redirect(w, r, membersURL, http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (s *Handlers) handleRepeaterDocs(w http.ResponseWriter, r *http.Request) {
|
||||
web.RedirectErr(w, r, docsPath(repeaterParam(r)), "Could not save documentation.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, docsPath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, docsPath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// pageRepeaterMaintenance renders the Maintenance tab: the history plus, for
|
||||
@@ -112,7 +112,7 @@ func (s *Handlers) handleAddMaintenance(w http.ResponseWriter, r *http.Request)
|
||||
web.RedirectErr(w, r, maintPath(repeaterParam(r)), "Could not log maintenance entry.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, maintPath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, maintPath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleDeleteMaintenance removes a maintenance entry (owner only).
|
||||
@@ -130,7 +130,7 @@ func (s *Handlers) handleDeleteMaintenance(w http.ResponseWriter, r *http.Reques
|
||||
web.RedirectErr(w, r, maintPath(repeaterParam(r)), "Could not delete entry.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, maintPath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, maintPath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
func docsPath(publicID string) string { return "/repeaters/" + publicID + "/docs" }
|
||||
|
||||
@@ -64,7 +64,7 @@ func (s *Handlers) pageAddRepeater(w http.ResponseWriter, r *http.Request) {
|
||||
orgs := s.setupOrgOptions(r)
|
||||
data["Orgs"] = orgs
|
||||
if b, err := json.Marshal(orgs); err == nil {
|
||||
data["OrgsJS"] = template.JS(b)
|
||||
data["OrgsJS"] = template.JS(b) //nolint:gosec // G203: b is json.Marshal output (Go escapes <>& in JS context)
|
||||
} else {
|
||||
data["OrgsJS"] = template.JS("[]")
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (s *Handlers) pageRepeater(w http.ResponseWriter, r *http.Request) {
|
||||
qr.BackgroundColor = color.Transparent
|
||||
qr.ForegroundColor = color.RGBA{R: 0x8a, G: 0x97, B: 0xa8, A: 0xff}
|
||||
if png, err := qr.PNG(256); err == nil {
|
||||
data["PublicPageQR"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png))
|
||||
data["PublicPageQR"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) //nolint:gosec // G203: fixed data: URI over base64 PNG, no user input
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func (s *Handlers) pageRepeater(w http.ResponseWriter, r *http.Request) {
|
||||
qr.BackgroundColor = color.Transparent
|
||||
qr.ForegroundColor = color.RGBA{R: 0x8a, G: 0x97, B: 0xa8, A: 0xff}
|
||||
if png, err := qr.PNG(256); err == nil {
|
||||
data["ContactQR"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png))
|
||||
data["ContactQR"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)) //nolint:gosec // G203: fixed data: URI over base64 PNG, no user input
|
||||
}
|
||||
}
|
||||
if isOwner {
|
||||
@@ -251,7 +251,7 @@ func (s *Handlers) pageEditRepeater(w http.ResponseWriter, r *http.Request) {
|
||||
s.Render(w, r, "edit_repeater.html", map[string]any{
|
||||
"Repeater": rep,
|
||||
"Presets": radioPresets,
|
||||
"SelectedPreset": presetIDFor(config.RadioDefaults{FreqHz: uint32(rep.RadioFreqHz), BwHz: uint32(rep.RadioBwHz), SF: uint8(rep.RadioSF), CR: uint8(rep.RadioCR)}),
|
||||
"SelectedPreset": presetIDFor(config.RadioDefaults{FreqHz: uint32(rep.RadioFreqHz), BwHz: uint32(rep.RadioBwHz), SF: uint8(rep.RadioSF), CR: uint8(rep.RadioCR)}), //nolint:gosec // G115: radio config value is bounded (preset-constrained)
|
||||
"RevokeCommand": s.Identity.RevokePermCommand(),
|
||||
"Error": r.URL.Query().Get("error"),
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ func (s *Handlers) handleCreateLink(w http.ResponseWriter, r *http.Request) {
|
||||
shareErr(w, r, "Could not create share link.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleDeleteInvite revokes (or clears) a single share link by id (owner only).
|
||||
@@ -80,7 +80,7 @@ func (s *Handlers) handleDeleteInvite(w http.ResponseWriter, r *http.Request) {
|
||||
shareErr(w, r, "Could not remove link.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleSetShareSteward flags or unflags a shared user as a steward (owner only).
|
||||
@@ -102,7 +102,7 @@ func (s *Handlers) handleSetShareSteward(w http.ResponseWriter, r *http.Request)
|
||||
shareErr(w, r, "Could not update steward.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// handleUnshare revokes a user's access (owner only).
|
||||
@@ -120,7 +120,7 @@ func (s *Handlers) handleUnshare(w http.ResponseWriter, r *http.Request) {
|
||||
shareErr(w, r, "Could not revoke access.")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// --- invite accept flow ---
|
||||
@@ -249,7 +249,7 @@ func (s *Handlers) pageShareCommands(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// A steward already has every command; per-command limits don't apply to them.
|
||||
if steward, err := s.Store.IsSteward(r.Context(), id, targetID); err == nil && steward {
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
return
|
||||
}
|
||||
target, err := s.Store.GetUserByID(r.Context(), targetID)
|
||||
@@ -303,7 +303,7 @@ func (s *Handlers) handleSetShareCommands(w http.ResponseWriter, r *http.Request
|
||||
http.Error(w, "could not save commands", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther)
|
||||
http.Redirect(w, r, sharePath(repeaterParam(r)), http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
@@ -343,5 +343,5 @@ func splitOwnedShared(repeaters []*store.Repeater) (owned, shared []*store.Repea
|
||||
// would silently re-authenticate on the next request.
|
||||
func (s *Handlers) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
_ = s.Auth.Logout(r.Context())
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.AuthHost)+"/logout", http.StatusSeeOther)
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.AuthHost)+"/logout", http.StatusSeeOther) //nolint:gosec // G710: local path or config-pinned origin
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func (s *Handlers) CustomDomain(next http.Handler) http.Handler {
|
||||
s.renderOrgPublic(w, r, org, false, false)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.PrimaryHost)+r.URL.RequestURI(), http.StatusFound)
|
||||
http.Redirect(w, r, s.Origin(r, s.Cfg.PrimaryHost)+r.URL.RequestURI(), http.StatusFound) //nolint:gosec // G710: local path or config-pinned origin
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ func TestBuildCommandPacketDecodableByRepeater(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("TextMessageFromBytes: %v", err)
|
||||
}
|
||||
if tm.Destination != repeater.Identity.Hash()[0] {
|
||||
t.Errorf("dest = 0x%02x, want 0x%02x", tm.Destination, repeater.Identity.Hash()[0])
|
||||
if tm.Destination != repeater.Hash()[0] {
|
||||
t.Errorf("dest = 0x%02x, want 0x%02x", tm.Destination, repeater.Hash()[0])
|
||||
}
|
||||
shared, err := repeater.SharedSecret(server.Identity)
|
||||
if err != nil {
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestExchangerUsesDirectAfterLogin(t *testing.T) {
|
||||
body = append(body, meshcore.PayloadTypeResponse)
|
||||
body = append(body, resp...)
|
||||
enc, _ := meshcore.EncryptThenMAC(shared, body)
|
||||
p := &meshcore.Path{Destination: server.Identity.Hash()[0], Source: repeater.Identity.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
p := &meshcore.Path{Destination: server.Hash()[0], Source: repeater.Hash()[0], MAC: [2]byte{enc[0], enc[1]}, EncryptedPayload: enc[2:]}
|
||||
payload, _ := p.ToBytes()
|
||||
lp := &meshcore.Packet{Header: meshcore.MakeHeader(meshcore.RouteTypeFlood, meshcore.PayloadTypePath, 0), Payload: payload}
|
||||
r, _ := lp.ToBytes()
|
||||
|
||||
@@ -60,7 +60,7 @@ func BuildLoginPacket(server meshcore.LocalIdentity, repeater meshcore.Identity,
|
||||
|
||||
// Repeater login request plaintext: timestamp(4, little-endian) + password.
|
||||
plaintext := make([]byte, 4+len(password))
|
||||
binary.LittleEndian.PutUint32(plaintext[:4], uint32(now.Unix()))
|
||||
binary.LittleEndian.PutUint32(plaintext[:4], uint32(now.Unix())) //nolint:gosec // G115: unix-seconds into the 4-byte protocol timestamp field
|
||||
copy(plaintext[4:], password)
|
||||
|
||||
enc, err := meshcore.EncryptThenMAC(shared, plaintext) // MAC(2) || ciphertext
|
||||
@@ -160,7 +160,7 @@ func decodeAddressedReply(server meshcore.LocalIdentity, repeater meshcore.Ident
|
||||
ciphertext := p[4:]
|
||||
|
||||
// Destination of the reply is the hash of our (server) public key.
|
||||
if dest != server.Identity.Hash()[0] {
|
||||
if dest != server.Hash()[0] {
|
||||
return nil, payloadType, ErrNotForUs
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ func buildRepeaterResponse(t *testing.T, repeater meshcore.LocalIdentity, server
|
||||
}
|
||||
resp := &meshcore.Response{
|
||||
Destination: server.Hash()[0],
|
||||
Source: repeater.Identity.Hash()[0],
|
||||
Source: repeater.Hash()[0],
|
||||
MAC: [2]byte{enc[0], enc[1]},
|
||||
EncryptedPayload: enc[2:],
|
||||
}
|
||||
@@ -94,8 +94,8 @@ func TestLoginRequestDecodableByRepeater(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("AnonReqFromBytes: %v", err)
|
||||
}
|
||||
if anon.Destination != repeater.Identity.Hash()[0] {
|
||||
t.Errorf("destination = 0x%02x, want 0x%02x", anon.Destination, repeater.Identity.Hash()[0])
|
||||
if anon.Destination != repeater.Hash()[0] {
|
||||
t.Errorf("destination = 0x%02x, want 0x%02x", anon.Destination, repeater.Hash()[0])
|
||||
}
|
||||
// The repeater derives the shared secret from the sender pubkey in the packet.
|
||||
sender, err := meshcore.NewIdentityFromBytes(anon.EphemeralPubKey[:])
|
||||
@@ -150,7 +150,7 @@ func buildRepeaterPathReply(t *testing.T, repeater meshcore.LocalIdentity, serve
|
||||
}
|
||||
p := &meshcore.Path{
|
||||
Destination: server.Hash()[0],
|
||||
Source: repeater.Identity.Hash()[0],
|
||||
Source: repeater.Hash()[0],
|
||||
MAC: [2]byte{enc[0], enc[1]},
|
||||
EncryptedPayload: enc[2:],
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func TestRegionDefCommands(t *testing.T) {
|
||||
regions := []Region{
|
||||
mk("us", 1, geo.Rectangle(0, 0, 100, 100)),
|
||||
mk("ny", 2, geo.Rectangle(10, 10, 30, 30)),
|
||||
mk("pa", 2, geo.Rectangle(10, 25, 30, 45)), // overlaps ny in lon 25–30
|
||||
mk("pa", 2, geo.Rectangle(10, 25, 30, 45)), // overlaps ny in lon 25–30
|
||||
mk("buf", 3, geo.Rectangle(12, 12, 18, 18)), // inside ny, west of pa
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func (s *Store) inTx(ctx context.Context, fn func(pgx.Tx) error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if err := fn(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (s *Store) Migrate(ctx context.Context) error {
|
||||
}
|
||||
// goose needs a database/sql handle; derive one from the pool config.
|
||||
db := stdlib.OpenDBFromPool(s.pool)
|
||||
defer db.Close()
|
||||
defer func() { _ = db.Close() }()
|
||||
if err := goose.UpContext(ctx, db, "migrations"); err != nil {
|
||||
return fmt.Errorf("goose up: %w", err)
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func ensureTemplate(ctx context.Context, migrate func(dsn string) error) error {
|
||||
templateErr = fmt.Errorf("admin connect: %w", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close(ctx)
|
||||
defer func() { _ = conn.Close(ctx) }()
|
||||
// Hold a cross-process advisory lock only around the template DDL: this is
|
||||
// the one CREATE DATABASE that copies the shared template1, so concurrent
|
||||
// package binaries on one server must take turns. Released before migrate,
|
||||
@@ -153,7 +153,7 @@ func Fresh(t *testing.T, migrate func(dsn string) error) string {
|
||||
conn, err := pgx.Connect(ctx, adminDSN)
|
||||
if err == nil {
|
||||
_, err = conn.Exec(ctx, `CREATE DATABASE `+quoteIdent(name)+` TEMPLATE `+quoteIdent(templateName))
|
||||
conn.Close(ctx)
|
||||
_ = conn.Close(ctx)
|
||||
}
|
||||
createMu.Unlock()
|
||||
if err != nil {
|
||||
@@ -165,7 +165,7 @@ func Fresh(t *testing.T, migrate func(dsn string) error) string {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer c.Close(ctx)
|
||||
defer func() { _ = c.Close(ctx) }()
|
||||
_, _ = c.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(name)+` WITH (FORCE)`)
|
||||
})
|
||||
|
||||
@@ -187,7 +187,7 @@ func RunMain(m *testing.M) int {
|
||||
if templateName != "" && adminDSN != "" {
|
||||
if conn, err := pgx.Connect(ctx, adminDSN); err == nil {
|
||||
_, _ = conn.Exec(ctx, `DROP DATABASE IF EXISTS `+quoteIdent(templateName)+` WITH (FORCE)`)
|
||||
conn.Close(ctx)
|
||||
_ = conn.Close(ctx)
|
||||
}
|
||||
}
|
||||
if container != nil {
|
||||
|
||||
+1
-1
@@ -270,7 +270,7 @@ func Dispatcher(cfg *config.Config, authH, rootH, appH http.Handler) http.Handle
|
||||
case rootH != nil && strings.EqualFold(host, cfg.RootHost):
|
||||
rootH.ServeHTTP(w, r)
|
||||
case cfg.WWWHost != "" && strings.EqualFold(host, cfg.WWWHost):
|
||||
http.Redirect(w, r, originFor(cfg, r, cfg.RootHost)+r.URL.RequestURI(), http.StatusMovedPermanently)
|
||||
http.Redirect(w, r, originFor(cfg, r, cfg.RootHost)+r.URL.RequestURI(), http.StatusMovedPermanently) //nolint:gosec // G710: local path or config-pinned origin
|
||||
default:
|
||||
appH.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -27,5 +27,5 @@ func Markdown(src string) template.HTML {
|
||||
if err := mdRenderer.Convert([]byte(src), &buf); err != nil {
|
||||
return ""
|
||||
}
|
||||
return template.HTML(mdPolicy.SanitizeBytes(buf.Bytes()))
|
||||
return template.HTML(mdPolicy.SanitizeBytes(buf.Bytes())) //nolint:gosec // G203: output is bluemonday-sanitized by mdPolicy
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user