Files
T
Tobias GesellchenandClaude Sonnet 4.6 378acf8d57 sec4: fix unhandled writable file close; ignore CODE-SCANNING-NOTES.md
Closes CodeQL alerts 280 and 281 (go/unhandled-writable-file-close).

scripts/extract-ws/main.go: change bare 'defer f.Close()' to
'defer func() { _ = f.Close() }()' — function returns void, silent
discard is the correct pattern (matches existing '_, _ = w.Write()'
usage elsewhere).

pkg/service/certmanager/certmanager.go: sequence encode + close for
both the cert file and the key file, checking both errors. This also
fixes resource leaks on the pem.Encode error path (file was previously
left open when encode failed). Matches the established pattern in
handlers_export.go (tw.Close / gz.Close).

.gitignore: exclude CODE-SCANNING-NOTES.md (local working notes;
will be added to VCS once the scanning sweep is complete and the
notes are stable).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:52:35 +02:00

255 lines
6.8 KiB
Go

package main
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcap"
)
// This tool extracts WebSocket payloads and DNS queries from a .pcap file
// and prints them in a format compatible with soundtouch-service interactions.
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run scripts/extract-ws.go <pcap_file> [filter_ip]")
os.Exit(1)
}
pcapFile := os.Args[1]
filterIP := ""
if len(os.Args) > 2 {
filterIP = os.Args[2]
fmt.Printf("[DEBUG] Filtering WebSocket for IP: %s\n", filterIP)
}
handle, err := pcap.OpenOffline(pcapFile)
if err != nil {
log.Fatal(err)
}
defer handle.Close()
fmt.Printf("[DEBUG] Reading file: %s\n", pcapFile)
// Prepare output files
baseName := strings.TrimSuffix(pcapFile, filepath.Ext(pcapFile))
wsFile, err := os.Create(baseName + ".ws.http")
if err != nil {
log.Fatal(err)
}
defer wsFile.Close()
dnsFile, err := os.Create(baseName + ".dns.txt")
if err != nil {
log.Fatal(err)
}
defer dnsFile.Close()
mdnsFile, err := os.Create(baseName + ".mdns.txt")
if err != nil {
log.Fatal(err)
}
defer mdnsFile.Close()
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
// Handle DNS
if dnsLayer := packet.Layer(layers.LayerTypeDNS); dnsLayer != nil {
dns, _ := dnsLayer.(*layers.DNS)
extractDNS(packet, dns, dnsFile, mdnsFile)
}
// Handle SSDP (UDP Port 1900)
if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil {
udp, _ := udpLayer.(*layers.UDP)
if udp.DstPort == 1900 || udp.SrcPort == 1900 {
extractSSDP(packet, udp, baseName+".ssdp.txt")
}
}
// Handle WebSockets (TCP)
if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
tcp, _ := tcpLayer.(*layers.TCP)
extractWebSocket(packet, tcp, filterIP, wsFile)
}
}
fmt.Printf("[DEBUG] Extraction complete. Results written to:\n- %s\n- %s\n- %s\n- %s\n",
baseName+".ws.http", baseName+".dns.txt", baseName+".mdns.txt", baseName+".ssdp.txt")
}
func extractSSDP(packet gopacket.Packet, udp *layers.UDP, ssdpFilename string) {
payload := string(udp.Payload)
if !strings.Contains(payload, "HTTP/1.1") && !strings.Contains(payload, "NOTIFY") && !strings.Contains(payload, "M-SEARCH") {
return
}
f, err := os.OpenFile(ssdpFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
defer func() { _ = f.Close() }()
srcIP := packet.NetworkLayer().NetworkFlow().Src().String()
dstIP := packet.NetworkLayer().NetworkFlow().Dst().String()
timestamp := packet.Metadata().Timestamp.Format("2006-01-02 15:04:05.000")
fmt.Fprintf(f, "[%s] %s:%d -> %s:%d\n", timestamp, srcIP, udp.SrcPort, dstIP, udp.DstPort)
fmt.Fprintf(f, "%s\n", strings.TrimSpace(payload))
fmt.Fprintln(f, "-------------------------------------------------")
}
func extractDNS(packet gopacket.Packet, dns *layers.DNS, dnsFile, mdnsFile *os.File) {
srcIP := packet.NetworkLayer().NetworkFlow().Src().String()
dstIP := packet.NetworkLayer().NetworkFlow().Dst().String()
isMDNS := false
if udpLayer := packet.Layer(layers.LayerTypeUDP); udpLayer != nil {
udp, _ := udpLayer.(*layers.UDP)
if udp.DstPort == 5353 || udp.SrcPort == 5353 {
isMDNS = true
}
}
out := dnsFile
if isMDNS {
out = mdnsFile
}
timestamp := packet.Metadata().Timestamp.Format("2006-01-02 15:04:05.000")
prefix := fmt.Sprintf("[%s] %s -> %s", timestamp, srcIP, dstIP)
for _, q := range dns.Questions {
fmt.Fprintf(out, "%s | QUERY: %s (%s)\n", prefix, string(q.Name), q.Type)
}
for _, a := range dns.Answers {
val := ""
if a.IP != nil {
val = a.IP.String()
} else if len(a.CNAME) > 0 {
val = string(a.CNAME)
} else if len(a.PTR) > 0 {
val = string(a.PTR)
} else if len(a.TXTs) > 0 {
var txts []string
for _, t := range a.TXTs {
txts = append(txts, string(t))
}
val = strings.Join(txts, " ")
} else {
val = fmt.Sprintf("Type: %s", a.Type)
}
fmt.Fprintf(out, "%s | ANSWER: %s -> %s\n", prefix, string(a.Name), val)
}
}
func extractWebSocket(packet gopacket.Packet, tcp *layers.TCP, filterIP string, wsFile *os.File) {
srcIP := packet.NetworkLayer().NetworkFlow().Src().String()
dstIP := packet.NetworkLayer().NetworkFlow().Dst().String()
if filterIP != "" && srcIP != filterIP && dstIP != filterIP {
return
}
payload := tcp.Payload
if len(payload) == 0 {
return
}
// Check for WebSocket Frame (Sliding search)
for i := 0; i < len(payload)-2; i++ {
firstByte := payload[i]
// Opcode 1 (Text) or 2 (Binary).
if (firstByte&0xF0) == 0x80 && (firstByte&0x0F == 1 || firstByte&0x0F == 2) {
secondByte := payload[i+1]
mask := (secondByte & 0x80) != 0
length := int(secondByte & 0x7F)
offset := i + 2
if length == 126 {
if len(payload) < offset+2 {
continue
}
length = int(payload[offset])<<8 | int(payload[offset+1])
offset += 2
} else if length == 127 {
if len(payload) < offset+8 {
continue
}
length = int(payload[offset+4])<<24 | int(payload[offset+5])<<16 | int(payload[offset+6])<<8 | int(payload[offset+7])
offset += 8
}
if mask {
if len(payload) < offset+4+length {
continue
}
maskKey := payload[offset : offset+4]
offset += 4
data := make([]byte, length)
for j := 0; j < length; j++ {
data[j] = payload[offset+j] ^ maskKey[j%4]
}
printInteraction(packet, tcp, data, wsFile)
i = offset + length - 1
} else {
if len(payload) >= offset+length {
data := payload[offset : offset+length]
printInteraction(packet, tcp, data, wsFile)
i = offset + length - 1
}
}
}
}
}
func printInteraction(packet gopacket.Packet, tcp *layers.TCP, data []byte, out io.Writer) {
src := packet.NetworkLayer().NetworkFlow().Src().String()
dst := packet.NetworkLayer().NetworkFlow().Dst().String()
fmt.Fprintf(out, "### WebSocket Message: %s -> %s\n", src, dst)
fmt.Fprintf(out, "// Timestamp: %s\n", packet.Metadata().Timestamp)
fmt.Fprintf(out, "// Ports: %d -> %d\n", tcp.SrcPort, tcp.DstPort)
fmt.Fprintln(out)
// Try to detect if it's GZIP
content := ""
if len(data) > 2 && data[0] == 0x1f && data[1] == 0x8b {
fmt.Fprintln(out, "// [Detected GZIP compression]")
content = decompressGzip(data)
} else {
content = string(data)
}
fmt.Fprintln(out, "/*")
fmt.Fprintln(out, strings.TrimSpace(content))
fmt.Fprintln(out, "*/")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "-------------------------------------------------")
fmt.Fprintln(out, "")
}
func decompressGzip(data []byte) string {
b := bytes.NewBuffer(data)
r, err := gzip.NewReader(b)
if err != nil {
return "[Error: Failed to create GZIP reader: " + err.Error() + "]"
}
defer r.Close()
res, err := io.ReadAll(r)
if err != nil {
return "[Error: Failed to decompress GZIP: " + err.Error() + "]"
}
return string(res)
}