Linting and formatting

This commit is contained in:
Jonathon Leight
2026-06-28 19:59:09 -04:00
parent f2a34cf03c
commit 6a6ec16825
35 changed files with 137 additions and 86 deletions
+4
View File
@@ -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"
+28
View File
@@ -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
+2 -1
View File
@@ -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:
+17
View File
@@ -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: []
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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: "/",
+2 -2
View File
@@ -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) {
+1 -1
View File
@@ -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
}
+4 -4
View File
@@ -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)
}
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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
+3 -3
View File
@@ -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:],
}
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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()
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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()
+5 -5
View File
@@ -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)
+9 -8
View File
@@ -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
}
+2 -2
View File
@@ -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
+6 -6
View File
@@ -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
}
+3 -3
View File
@@ -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" }
+4 -4
View File
@@ -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"),
})
+6 -6
View File
@@ -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 ---
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
})
}
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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
}
+4 -4
View File
@@ -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:],
}
+1 -1
View File
@@ -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 2530
mk("pa", 2, geo.Rectangle(10, 25, 30, 45)), // overlaps ny in lon 2530
mk("buf", 3, geo.Rectangle(12, 12, 18, 18)), // inside ny, west of pa
}
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+4 -4
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -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
}