mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
255 lines
6.8 KiB
Go
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 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)
|
|
}
|