1package hdhr23import (4 "bytes"5 "context"6 "encoding/binary"7 "hash/crc32"8 "hash/fnv"9 "log"10 "net"11 "strconv"12)1314// HDHomeRun discovery protocol (libhdhomerun hdhomerun_pkt.h).15const (16 discoverPort = 6500117 typeDiscoverRq = 0x000218 typeDiscoverRp = 0x000319 tagDeviceType = 0x0120 tagDeviceID = 0x0221 tagTunerCount = 0x1022 tagLineupURL = 0x2723 tagBaseURL = 0x2A24 deviceTuner = 0x0000000125 wildcard = 0xFFFFFFFF26)2728// checksumTable is libhdhomerun's device ID check digit table.29var checksumTable = [16]uint32{0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0}3031// ValidDeviceID reports whether id carries a correct check digit; apps32// using libhdhomerun ignore devices that fail it.33func ValidDeviceID(id uint32) bool { return checkDigit(id) == id&0xF }3435func checkDigit(id uint32) uint32 {36 var c uint3237 c ^= checksumTable[(id>>28)&0xF]38 c ^= (id >> 24) & 0xF39 c ^= checksumTable[(id>>20)&0xF]40 c ^= (id >> 16) & 0xF41 c ^= checksumTable[(id>>12)&0xF]42 c ^= (id >> 8) & 0xF43 c ^= checksumTable[(id>>4)&0xF]44 return c45}4647// DeviceIDFor derives a stable, valid device ID from a seed such as the48// server name.49func DeviceIDFor(seed string) uint32 {50 h := fnv.New32a()51 h.Write([]byte("airwaves:" + seed))52 id := h.Sum32() &^ 0xF53 return id | checkDigit(id)54}5556func packet(typ uint16, payload []byte) []byte {57 var b bytes.Buffer58 _ = binary.Write(&b, binary.BigEndian, typ)59 _ = binary.Write(&b, binary.BigEndian, uint16(len(payload)))60 b.Write(payload)61 _ = binary.Write(&b, binary.LittleEndian, crc32.ChecksumIEEE(b.Bytes()))62 return b.Bytes()63}6465func tlv(b *bytes.Buffer, tag byte, value []byte) {66 b.WriteByte(tag)67 if n := len(value); n < 128 {68 b.WriteByte(byte(n))69 } else {70 b.WriteByte(byte(n&0x7F | 0x80))71 b.WriteByte(byte(n >> 7))72 }73 b.Write(value)74}7576func u32(v uint32) []byte {77 b := make([]byte, 4)78 binary.BigEndian.PutUint32(b, v)79 return b80}8182// parsePacket checks a packet's length and CRC and returns its type and83// payload.84func parsePacket(pkt []byte) (typ uint16, payload []byte, ok bool) {85 if len(pkt) < 8 {86 return 0, nil, false87 }88 n := int(binary.BigEndian.Uint16(pkt[2:]))89 if 4+n+4 > len(pkt) || crc32.ChecksumIEEE(pkt[:4+n]) != binary.LittleEndian.Uint32(pkt[4+n:]) {90 return 0, nil, false91 }92 return binary.BigEndian.Uint16(pkt), pkt[4 : 4+n], true93}9495// eachTag calls f with each tag of a payload and its value, whose length96// takes one byte, or two from 128 on. It reports whether the payload was97// well formed.98func eachTag(p []byte, f func(tag byte, v []byte)) bool {99 for len(p) > 0 {100 if len(p) < 2 {101 return false102 }103 tag, l, hdr := p[0], int(p[1]), 2104 if l&0x80 != 0 {105 if len(p) < 3 {106 return false107 }108 l, hdr = l&0x7F|int(p[2])<<7, 3109 }110 if len(p) < hdr+l {111 return false112 }113 f(tag, p[hdr:hdr+l])114 p = p[hdr+l:]115 }116 return true117}118119// parseRequest reports whether pkt is a discover request this device should120// answer (a tuner or wildcard search, for our ID or any ID).121func parseRequest(pkt []byte, id uint32) bool {122 typ, p, ok := parsePacket(pkt)123 if !ok || typ != typeDiscoverRq {124 return false125 }126 answer := true127 ok = eachTag(p, func(tag byte, v []byte) {128 if len(v) != 4 {129 return130 }131 x := binary.BigEndian.Uint32(v)132 if tag == tagDeviceType && x != wildcard && x != deviceTuner {133 answer = false134 }135 if tag == tagDeviceID && x != wildcard && x != id {136 answer = false137 }138 })139 return ok && answer140}141142func reply(id uint32, tuners int, base string) []byte {143 var b bytes.Buffer144 tlv(&b, tagDeviceType, u32(deviceTuner))145 tlv(&b, tagDeviceID, u32(id))146 tlv(&b, tagTunerCount, []byte{byte(tuners)})147 tlv(&b, tagBaseURL, []byte(base))148 tlv(&b, tagLineupURL, []byte(base+"/lineup.json"))149 return packet(typeDiscoverRp, b.Bytes())150}151152// Discover answers HDHomeRun discovery broadcasts until ctx ends. port is153// the HTTP port advertised in the base URL. Requests from this host's own154// addresses are ignored, so looking for the real tuner from this machine155// (Find) never turns up Airwaves itself.156func (s *Server) Discover(ctx context.Context, port int) error {157 conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: discoverPort})158 if err != nil {159 return err160 }161 go func() {162 <-ctx.Done()163 conn.Close()164 }()165 buf := make([]byte, 1500)166 for {167 n, from, err := conn.ReadFromUDP(buf)168 if err != nil {169 if ctx.Err() != nil {170 return nil171 }172 return err173 }174 if isLocal(from.IP) || !parseRequest(buf[:n], s.DeviceID) {175 continue176 }177 local := localIPFor(from.IP)178 if local == nil {179 continue180 }181 base := "http://" + net.JoinHostPort(local.String(), strconv.Itoa(port))182 if _, err := conn.WriteToUDP(reply(s.DeviceID, s.Tuners(), base), from); err != nil {183 log.Printf("hdhr discovery reply: %v", err)184 }185 }186}187188func isLocal(ip net.IP) bool {189 addrs, _ := net.InterfaceAddrs()190 for _, a := range addrs {191 if n, ok := a.(*net.IPNet); ok && n.IP.Equal(ip) {192 return true193 }194 }195 return ip.IsLoopback()196}197198// localIPFor finds this host's address on the same subnet as peer.199func localIPFor(peer net.IP) net.IP {200 addrs, _ := net.InterfaceAddrs()201 for _, a := range addrs {202 if n, ok := a.(*net.IPNet); ok && n.IP.To4() != nil && n.Contains(peer) {203 return n.IP204 }205 }206 return nil207}