diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5157ab6..d0d97df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,11 @@ jobs: - name: Vet run: go vet ./... + - name: Lint + uses: golangci/golangci-lint-action@v7 + with: + version: latest + - name: Test run: go test -v ./... diff --git a/.gitignore b/.gitignore index ee74461..39b99f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Go towerops-agent *.test +cover.out # Database files *.db diff --git a/agent.go b/agent.go index d962fe7..e8bf25b 100644 --- a/agent.go +++ b/agent.go @@ -65,7 +65,7 @@ func runSession(ctx context.Context, baseURL, token string) error { if err != nil { return fmt.Errorf("connect: %w", err) } - defer ws.Close() + defer func() { _ = ws.Close() }() agentID := fmt.Sprintf("agent-%d", time.Now().Unix()) topic := "agent:" + agentID diff --git a/flake.nix b/flake.nix index 03819ca..5c81a38 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,7 @@ devShells.default = pkgs.mkShell { buildInputs = [ pkgs.go + pkgs.golangci-lint pkgs.protobuf pkgs.protoc-gen-go pkgs.git diff --git a/mikrotik.go b/mikrotik.go index dfc9e1e..194ae87 100644 --- a/mikrotik.go +++ b/mikrotik.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/tls" "fmt" "io" @@ -42,7 +43,7 @@ func mikrotikConnect(ip string, port uint32, username, password string, useSSL b NetDialer: &net.Dialer{Timeout: mikrotikConnTimeout}, Config: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, } - conn, err = dialer.DialContext(nil, "tcp", addr) + conn, err = dialer.DialContext(context.Background(), "tcp", addr) } else { conn, err = net.DialTimeout("tcp", addr, mikrotikConnTimeout) } @@ -55,11 +56,11 @@ func mikrotikConnect(ip string, port uint32, username, password string, useSSL b // Authenticate resp, err := c.execute("/login", map[string]string{"name": username, "password": password}) if err != nil { - conn.Close() + _ = conn.Close() return nil, fmt.Errorf("auth: %w", err) } if resp.err != "" { - conn.Close() + _ = conn.Close() return nil, fmt.Errorf("auth failed: %s", resp.err) } @@ -85,7 +86,7 @@ func (c *mikrotikClient) execute(command string, args map[string]string) (*mikro } func (c *mikrotikClient) close() error { - c.execute("/quit", nil) // best-effort + _, _ = c.execute("/quit", nil) // best-effort return c.conn.Close() } @@ -145,7 +146,7 @@ func (c *mikrotikClient) readSentence() ([]string, error) { var words []string for { if tc, ok := c.conn.(net.Conn); ok { - tc.SetReadDeadline(time.Now().Add(mikrotikReadTimeout)) + _ = tc.SetReadDeadline(time.Now().Add(mikrotikReadTimeout)) } word, err := c.readWord() if err != nil { @@ -268,7 +269,7 @@ func executeMikrotikJob(job *pb.AgentJob, resultCh chan<- *pb.MikrotikResult) { } return } - defer client.close() + defer func() { _ = client.close() }() var allSentences []*pb.MikrotikSentence var errorMessage string diff --git a/snmp.go b/snmp.go index 033f151..f059ea0 100644 --- a/snmp.go +++ b/snmp.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "time" + "unicode/utf8" "github.com/gosnmp/gosnmp" "github.com/towerops-app/towerops-agent/pb" @@ -22,7 +23,7 @@ func executeSnmpJob(job *pb.AgentJob, resultCh chan<- *pb.SnmpResult) { slog.Error("snmp connect", "job_id", job.JobId, "device", dev.Ip, "error", err) return } - defer conn.Conn.Close() + defer func() { _ = conn.Conn.Close() }() oidValues := make(map[string]string) @@ -96,7 +97,7 @@ func executeCredentialTest(job *pb.AgentJob, resultCh chan<- *pb.CredentialTestR } return } - defer conn.Conn.Close() + defer func() { _ = conn.Conn.Close() }() result, err := conn.Get([]string{"1.3.6.1.2.1.1.1.0"}) if err != nil { @@ -221,10 +222,12 @@ func snmpValueToString(pdu gosnmp.SnmpPDU) string { return fmt.Sprintf("%d", gosnmp.ToBigInt(pdu.Value).Int64()) case gosnmp.OctetString: b := pdu.Value.([]byte) - // Try UTF-8 first + if !utf8.Valid(b) { + return formatHex(b) + } + // Check for non-printable control chars for _, c := range b { if c < 0x20 && c != '\n' && c != '\r' && c != '\t' { - // Non-printable - return hex return formatHex(b) } } diff --git a/snmp_test.go b/snmp_test.go index ba2985b..c931132 100644 --- a/snmp_test.go +++ b/snmp_test.go @@ -67,6 +67,11 @@ func TestSnmpValueToString(t *testing.T) { pdu: gosnmp.SnmpPDU{Type: gosnmp.NoSuchObject, Value: nil}, want: "null", }, + { + name: "invalid utf8", + pdu: gosnmp.SnmpPDU{Type: gosnmp.OctetString, Value: []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x80, 0xFE}}, + want: "48:65:6c:6c:6f:80:fe", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/ssh.go b/ssh.go index 078361b..b78482a 100644 --- a/ssh.go +++ b/ssh.go @@ -23,13 +23,13 @@ func executeMikrotikBackup(ip string, port uint16, username, password string) (s if err != nil { return "", fmt.Errorf("ssh dial %s: %w", addr, err) } - defer conn.Close() + defer func() { _ = conn.Close() }() session, err := conn.NewSession() if err != nil { return "", fmt.Errorf("ssh session: %w", err) } - defer session.Close() + defer func() { _ = session.Close() }() output, err := session.CombinedOutput("/export compact") if err != nil { diff --git a/update.go b/update.go index 02de34d..6d24ae6 100644 --- a/update.go +++ b/update.go @@ -18,7 +18,7 @@ func selfUpdate(downloadURL, expectedChecksum string) error { if err != nil { return fmt.Errorf("download: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("download failed: status %d", resp.StatusCode) @@ -52,7 +52,7 @@ func selfUpdate(downloadURL, expectedChecksum string) error { // Replace current binary if err := os.Rename(tempPath, currentExe); err != nil { - os.Remove(tempPath) + _ = os.Remove(tempPath) return fmt.Errorf("rename: %w", err) } slog.Info("binary replaced", "path", currentExe) diff --git a/update_test.go b/update_test.go index f6fe647..b90b5b2 100644 --- a/update_test.go +++ b/update_test.go @@ -17,7 +17,7 @@ func TestSelfUpdateBadURL(t *testing.T) { func TestSelfUpdateChecksumMismatch(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("fake binary")) + _, _ = w.Write([]byte("fake binary")) })) defer srv.Close() @@ -32,7 +32,7 @@ func TestSelfUpdateChecksumMatch(t *testing.T) { checksum := fmt.Sprintf("%x", sha256.Sum256(body)) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(body) + _, _ = w.Write(body) })) defer srv.Close() diff --git a/websocket.go b/websocket.go index 6d1e71a..22fa285 100644 --- a/websocket.go +++ b/websocket.go @@ -57,7 +57,7 @@ func WSDial(rawURL string) (*WSConn, error) { // Generate random key for Sec-WebSocket-Key keyBytes := make([]byte, 16) if _, err := rand.Read(keyBytes); err != nil { - conn.Close() + _ = conn.Close() return nil, fmt.Errorf("generate key: %w", err) } key := base64.StdEncoding.EncodeToString(keyBytes) @@ -67,7 +67,7 @@ func WSDial(rawURL string) (*WSConn, error) { path, u.Host, key) if _, err := conn.Write([]byte(req)); err != nil { - conn.Close() + _ = conn.Close() return nil, fmt.Errorf("write handshake: %w", err) } @@ -75,12 +75,12 @@ func WSDial(rawURL string) (*WSConn, error) { buf := make([]byte, 4096) n, err := conn.Read(buf) if err != nil { - conn.Close() + _ = conn.Close() return nil, fmt.Errorf("read handshake: %w", err) } resp := string(buf[:n]) if !strings.Contains(resp, "101") { - conn.Close() + _ = conn.Close() return nil, fmt.Errorf("handshake failed: %s", strings.SplitN(resp, "\r\n", 2)[0]) } @@ -102,7 +102,7 @@ func (ws *WSConn) ReadMessage() ([]byte, int, error) { return nil, 0, fmt.Errorf("pong: %w", err) } case opClose: - ws.writeFrame(opClose, nil) // best-effort close reply + _ = ws.writeFrame(opClose, nil) // best-effort close reply return nil, opClose, io.EOF } } @@ -115,7 +115,7 @@ func (ws *WSConn) WriteText(data []byte) error { // Close sends a close frame and closes the underlying connection. func (ws *WSConn) Close() error { - ws.writeFrame(opClose, nil) // best-effort + _ = ws.writeFrame(opClose, nil) // best-effort return ws.conn.Close() } @@ -175,7 +175,7 @@ func (ws *WSConn) writeFrame(opcode int, payload []byte) error { // Max header: 2 + 8 + 4 (mask) = 14 bytes header := make([]byte, 2, 14) header[0] = 0x80 | byte(opcode) // FIN + opcode - header[1] = 0x80 // masked (client must mask) + header[1] = 0x80 // masked (client must mask) switch { case length <= 125: @@ -194,7 +194,7 @@ func (ws *WSConn) writeFrame(opcode int, payload []byte) error { // Generate mask key maskKey := make([]byte, 4) - rand.Read(maskKey) + _, _ = rand.Read(maskKey) header = append(header, maskKey...) // Mask payload diff --git a/websocket_test.go b/websocket_test.go index 6997207..6825afd 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -131,6 +131,9 @@ type captureWriter struct { written []byte } -func (c *captureWriter) Read(p []byte) (int, error) { return c.Reader.Read(p) } -func (c *captureWriter) Write(p []byte) (int, error) { c.written = append(c.written, p...); return len(p), nil } -func (c *captureWriter) Close() error { return nil } +func (c *captureWriter) Read(p []byte) (int, error) { return c.Reader.Read(p) } +func (c *captureWriter) Write(p []byte) (int, error) { + c.written = append(c.written, p...) + return len(p), nil +} +func (c *captureWriter) Close() error { return nil }