Compare commits

..

10 Commits

Author SHA1 Message Date
Alexey Khit f442aab176 Fix RTSP from RTMP stream 2022-09-05 20:04:30 +03:00
Alexey Khit 0e71bd4dcb Adds low delay for any ffmpeg source 2022-09-04 23:02:54 +03:00
Alexey Khit e3618d70c3 Adds support MPA codec 2022-09-04 22:34:39 +03:00
Alexey Khit 99c4a3e34a Update RTSP Setup link logic 2022-09-04 21:43:32 +03:00
Alexey Khit b78de349ab Hide streams from Hass by default 2022-09-02 20:52:39 +03:00
Alexey Khit b4990b1e90 Fix support RTSPtoWebRTC API 2022-09-02 20:52:22 +03:00
Alexey Khit 687bdadba6 Update docs about HomeKit 2022-09-01 16:11:47 +03:00
Alexey Khit c0c96cfcdb Adds support HomeKit cameras! 2022-09-01 16:11:34 +03:00
Alexey Khit 4f92608f33 Fix webrtc close when it starts from ws 2022-08-31 23:09:01 +03:00
Alexey Khit ac49bbef4d Update readme 2022-08-28 06:44:18 +03:00
40 changed files with 3351 additions and 105 deletions
+2
View File
@@ -4,3 +4,5 @@
.tmp/
go2rtc.yaml
go2rtc.json
+30 -7
View File
@@ -1,11 +1,14 @@
# go2rtc
Ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
Ultimate camera streaming application with support RTSP, WebRTC, HomeKit, FFmpeg, RTMP, etc.
![](assets/go2rtc.png)
- zero-dependency and zero-config [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, ARM)
- zero-delay for all supported protocols (lowest possible streaming latency)
- streaming from `RTSP`, `RTMP`, `MJPEG`, `HLS`, `USB Cameras`, `files` and [other sources](#module-streams)
- streaming to `RTSP` or `WebRTC` (any modern browser)
- streaming from [RTSP](#source-rtsp), [RTMP](#source-rtmp), [MJPEG](#source-ffmpeg), [HLS](#source-ffmpeg), [USB Cameras](#source-ffmpeg-device), [files](#source-ffmpeg) and [other sources](#module-streams)
- streaming to [RTSP](#module-rtsp), [WebRTC](#module-webrtc) or [MSE](#module-api)
- first project in the World with support streaming from [HomeKit Cameras](#source-homekit)
- low CPU load for supported codecs
- on the fly transcoding for unsupported codecs via [FFmpeg](#source-ffmpeg)
- multi-source 2-way [codecs negotiation](#codecs-negotiation)
@@ -22,6 +25,7 @@ Ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, e
- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) idea from [@aler9](https://github.com/aler9)
- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea
- [MediaSoup](https://mediasoup.org/) framework routing idea
- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)
## Codecs negotiation
@@ -48,7 +52,7 @@ streams:
**go2rtc** automatically match codecs for you browser and all your stream sources. This called **multi-source 2-way codecs negotiation**. And this is one of the main features of this app.
![](codecs.svg)
![](assets/codecs.svg)
**PS.** You can select `PCMU` or `PCMA` codec in camera setting and don't use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.
@@ -73,8 +77,8 @@ streams:
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.exe` - Windows 64-bit
- `go2rtc_win32.exe` - Windows 32-bit
- `go2rtc_win64.zip` - Windows 64-bit
- `go2rtc_win32.zip` - Windows 32-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
- `go2rtc_linux_arm64` - Linux ARM 64-bit (ex. Raspberry 64-bit OS)
@@ -144,6 +148,7 @@ Available source types:
- [ffmpeg](#source-ffmpeg) - FFmpeg integration (`MJPEG`, `HLS`, `files` and source types)
- [ffmpeg:device](#source-ffmpeg-device) - local USB Camera or Webcam
- [exec](#source-exec) - advanced FFmpeg and GStreamer integration
- [homekit](#source-homekit) - streaming from HomeKit Camera
- [hass](#source-hass) - Home Assistant integration
**PS.** You can use sources like `MJPEG`, `HLS` and others via FFmpeg integration.
@@ -249,11 +254,28 @@ streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
```
#### Source: HomeKit
**Important:**
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home) - you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc - you can't pair it with iPhone
- HomeKit device should be in same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between device and go2rtc
go2rtc support import paired HomeKit devices from [Home Assistant](#source-hass). So you can use HomeKit camera with Hass and go2rtc simultaneously. If you using Hass, I recommend pairing devices with it, it will give you more options.
You can pair device with go2rtc on the HomeKit page. If you can't see your devices - reload the page. Also try reboot your HomeKit device (power off). If you still can't see it - you have a problems with mDNS.
If you see a device but it does not have a pair button - it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge etc). You need to delete device from that ecosystem, and it will be available for pairing. If you cannot unpair device, you will have to reset it.
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
#### Source: Hass
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
- support ONLY [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
- support [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
```yaml
hass:
@@ -261,6 +283,7 @@ hass:
streams:
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
aqara_g3: hass:Camera-Hub-G3-AB12
```
### Module: API
View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

+61
View File
@@ -0,0 +1,61 @@
package store
import (
"encoding/json"
"github.com/rs/zerolog/log"
"os"
)
const name = "go2rtc.json"
var store map[string]interface{}
func load() {
data, _ := os.ReadFile(name)
if data != nil {
if err := json.Unmarshal(data, &store); err != nil {
// TODO: log
log.Warn().Err(err).Msg("[app] read storage")
}
}
if store == nil {
store = make(map[string]interface{})
}
}
func save() error {
data, err := json.Marshal(store)
if err != nil {
return err
}
return os.WriteFile(name, data, 0644)
}
func GetRaw(key string) interface{} {
if store == nil {
load()
}
return store[key]
}
func GetDict(key string) map[string]interface{} {
raw := GetRaw(key)
if raw != nil {
return raw.(map[string]interface{})
}
return make(map[string]interface{})
}
func Set(key string, v interface{}) error {
if store == nil {
load()
}
store[key] = v
return save()
}
+1 -1
View File
@@ -22,7 +22,7 @@ func Init() {
// inputs
"file": "-re -stream_loop -1 -i {input}",
"http": "-i {input}",
"http": "-fflags nobuffer -flags low_delay -i {input}",
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
// output
+30 -20
View File
@@ -6,12 +6,12 @@ import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"net/http"
"net/url"
"os"
"path"
"strings"
@@ -63,22 +63,22 @@ func Init() {
}
urls[entrie.Title] = entrie.Options.StreamSource
//case "homekit_controller":
// if entrie.Data.ClientID == "" {
// continue
// }
// urls[entrie.Title] = fmt.Sprintf(
// "homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
// entrie.Data.DeviceHost, entrie.Data.DevicePort,
// entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
// entrie.Data.DeviceID, entrie.Data.DevicePublic,
// )
case "homekit_controller":
if entrie.Data.ClientID == "" {
continue
}
urls[entrie.Title] = fmt.Sprintf(
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
entrie.Data.DeviceHost, entrie.Data.DevicePort,
entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
entrie.Data.DeviceID, entrie.Data.DevicePublic,
)
default:
continue
}
streams.Get("hass:" + entrie.Title)
//streams.Get("hass:" + entrie.Title)
}
}
@@ -90,7 +90,13 @@ func handler(w http.ResponseWriter, r *http.Request) {
return
}
url := r.FormValue("url")
src := r.FormValue("url")
src, err := url.QueryUnescape(src)
if err != nil {
log.Error().Err(err).Msg("[api.hass] query unescape")
return
}
str := r.FormValue("sdp64")
offer, err := base64.StdEncoding.DecodeString(str)
@@ -99,16 +105,20 @@ func handler(w http.ResponseWriter, r *http.Request) {
return
}
// TODO: fixme
if strings.HasPrefix(url, "rtsp://") {
port := ":" + rtsp.Port + "/"
i := strings.Index(url, port)
if i > 0 {
url = url[i+len(port):]
// check if stream links to our rtsp server
if strings.HasPrefix(src, "rtsp://") {
i := strings.IndexByte(src[7:], '/')
if i > 0 && streams.Has(src[8+i:]) {
src = src[8+i:]
}
}
stream := streams.Get(url)
stream := streams.Get(src)
if stream == nil {
log.Error().Str("url", src).Msg("[api.hass] unsupported source")
return
}
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
if err != nil {
log.Error().Err(err).Msg("[api.hass] exchange SDP")
+157
View File
@@ -0,0 +1,157 @@
package homekit
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
"net/http"
"net/url"
"strings"
)
func apiHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
items := make([]interface{}, 0)
for name, src := range store.GetDict("streams") {
if src := src.(string); strings.HasPrefix(src, "homekit") {
u, err := url.Parse(src)
if err != nil {
continue
}
device := Device{
Name: name,
Addr: u.Host,
Paired: true,
}
items = append(items, device)
}
}
for info := range mdns.GetAll() {
if !strings.HasSuffix(info.Name, mdns.Suffix) {
continue
}
name := info.Name[:len(info.Name)-len(mdns.Suffix)]
device := Device{
Name: strings.ReplaceAll(name, "\\", ""),
Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port),
}
for _, field := range info.InfoFields {
switch field[:2] {
case "id":
device.ID = field[3:]
case "md":
device.Model = field[3:]
case "sf":
device.Paired = field[3] == '0'
}
}
items = append(items, device)
}
data, err := json.Marshal(items)
if err != nil {
log.Error().Err(err).Msg("[api.homekit]")
return
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.homekit]")
}
case "POST":
// TODO: post params...
id := r.URL.Query().Get("id")
pin := r.URL.Query().Get("pin")
client, err := homekit.Pair(id, pin)
if err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] pair")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
name := r.URL.Query().Get("name")
dict := store.GetDict("streams")
dict[name] = client.URL()
if err = store.Set("streams", dict); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] save to store")
// response error
_, err = w.Write([]byte(err.Error()))
}
streams.New(name, client.URL())
case "DELETE":
src := r.URL.Query().Get("src")
dict := store.GetDict("streams")
for name, rawURL := range dict {
if name != src {
continue
}
client, err := homekit.NewClient(rawURL.(string))
if err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] new client")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
if err = client.Dial(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] client dial")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
go client.Handle()
if err = client.ListPairings(); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
return
}
if err = client.DeletePairing(client.ClientID); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] unpair")
// response error
_, err = w.Write([]byte(err.Error()))
}
delete(dict, name)
if err = store.Set("streams", dict); err != nil {
// log error
log.Error().Err(err).Msg("[api.homekit] store set")
// response error
_, err = w.Write([]byte(err.Error()))
}
return
}
}
}
type Device struct {
ID string `json:"id"`
Name string `json:"name"`
Addr string `json:"addr"`
Model string `json:"model"`
Paired bool `json:"paired"`
//Type string `json:"type"`
}
+39
View File
@@ -0,0 +1,39 @@
package homekit
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
)
func Init() {
log = app.GetLogger("homekit")
streams.HandleFunc("homekit", streamHandler)
api.HandleFunc("/api/homekit", apiHandler)
}
var log zerolog.Logger
func streamHandler(url string) (streamer.Producer, error) {
client, err := homekit.NewClient(url)
if err != nil {
return nil, err
}
if err = client.Dial(); err != nil {
return nil, err
}
// start gorutine for reading responses from camera
go func() {
if err = client.Handle(); err != nil {
log.Warn().Err(err).Msg("[homekit] client")
}
}()
return &Producer{client: client}, nil
}
+189
View File
@@ -0,0 +1,189 @@
package homekit
import (
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
"net"
"strconv"
)
type Producer struct {
streamer.Element
client *homekit.Client
medias []*streamer.Media
tracks []*streamer.Track
sessions []*pkg.Session
}
func (c *Producer) GetMedias() []*streamer.Media {
if c.medias == nil {
c.medias = c.getMedias()
}
return c.medias
}
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track := &streamer.Track{Codec: codec, Direction: media.Direction}
c.tracks = append(c.tracks, track)
return track
}
func (c *Producer) Start() error {
if c.tracks == nil {
return errors.New("producer without tracks")
}
// get our server local IP-address
host, _, err := net.SplitHostPort(c.client.LocalAddr())
if err != nil {
return err
}
// get our server SRTP port
port, err := strconv.Atoi(srtp.Port)
if err != nil {
return err
}
// setup HomeKit stream session
hkSession := camera.NewSession()
hkSession.SetLocalEndpoint(host, uint16(port))
// create client for processing camera accessory
cam := camera.NewClient(c.client)
// try to start HomeKit stream
if err = cam.StartStream2(hkSession); err != nil {
panic(err) // TODO: fixme
}
// SRTP Video Session
vs := &pkg.Session{
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcVideo,
Track: c.tracks[0],
}
if err = vs.SetKeys(
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
); err != nil {
return err
}
// SRTP Audio Session
as := &pkg.Session{
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
RemoteSSRC: hkSession.Answer.SsrcAudio,
Track: &streamer.Track{},
}
if err = as.SetKeys(
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
); err != nil {
return err
}
srtp.AddSession(vs)
srtp.AddSession(as)
c.sessions = []*pkg.Session{vs, as}
return nil
}
func (c *Producer) Stop() error {
err := c.client.Close()
for _, session := range c.sessions {
srtp.RemoveSession(session)
}
return err
}
func (c *Producer) getMedias() []*streamer.Media {
var medias []*streamer.Media
accs, err := c.client.GetAccessories()
acc := accs[0]
if err != nil {
panic(err)
}
// get supported video config (not really necessary)
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
v1 := &rtp.VideoStreamConfiguration{}
if err = char.ReadTLV8(v1); err != nil {
panic(err)
}
for _, hkCodec := range v1.Codecs {
codec := &streamer.Codec{ClockRate: 90000}
switch hkCodec.Type {
case rtp.VideoCodecType_H264:
codec.Name = streamer.CodecH264
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
media := &streamer.Media{
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
v2 := &rtp.AudioStreamConfiguration{}
if err = char.ReadTLV8(v2); err != nil {
panic(err)
}
for _, hkCodec := range v2.Codecs {
codec := &streamer.Codec{
Channels: uint16(hkCodec.Parameters.Channels),
}
switch hkCodec.Type {
case rtp.AudioCodecType_AAC_ELD:
codec.Name = streamer.CodecAAC
default:
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
}
switch hkCodec.Parameters.Samplerate {
case rtp.AudioCodecSampleRate8Khz:
codec.ClockRate = 8000
case rtp.AudioCodecSampleRate16Khz:
codec.ClockRate = 16000
case rtp.AudioCodecSampleRate24Khz:
codec.ClockRate = 24000
default:
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
}
media := &streamer.Media{
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
medias = append(medias, media)
}
return medias
}
+59
View File
@@ -0,0 +1,59 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/pkg/srtp"
"github.com/rs/zerolog"
"net"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
} `yaml:"srtp"`
}
// default config
cfg.Mod.Listen = ":8443"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
log = app.GetLogger("srtp")
// create SRTP server (endpoint) for receiving video from HomeKit camera
conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
if err != nil {
log.Warn().Err(err).Msg("[srtp] listen")
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
// run server
go func() {
server = &srtp.Server{}
if err = server.Serve(conn); err != nil {
log.Warn().Err(err).Msg("[srtp] serve")
}
}()
}
var log zerolog.Logger
var server *srtp.Server
var Port string
func AddSession(session *srtp.Session) {
server.AddSession(session)
}
func RemoveSession(session *srtp.Session) {
server.RemoveSession(session)
}
+22 -8
View File
@@ -2,6 +2,7 @@ package streams
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/app/store"
"github.com/rs/zerolog"
)
@@ -17,21 +18,34 @@ func Init() {
for name, item := range cfg.Mod {
streams[name] = NewStream(item)
}
for name, item := range store.GetDict("streams") {
streams[name] = NewStream(item)
}
}
func Get(name string) *Stream {
if stream, ok := streams[name]; ok {
func Get(src string) *Stream {
if stream, ok := streams[src]; ok {
return stream
}
if HasProducer(name) {
log.Info().Str("url", name).Msg("[streams] create new stream")
stream := NewStream(name)
streams[name] = stream
return stream
if !HasProducer(src) {
return nil
}
return nil
log.Info().Str("url", src).Msg("[streams] create new stream")
stream := NewStream(src)
streams[src] = stream
return stream
}
func Has(src string) bool {
return streams[src] != nil
}
func New(name string, source interface{}) {
streams[name] = NewStream(source)
}
func Delete(name string) {
+3 -3
View File
@@ -84,8 +84,8 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
conn.UserAgent = ctx.Request.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case streamer.EventType:
if msg == streamer.StateNull {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
case *streamer.Message:
@@ -152,7 +152,7 @@ func ExchangeSDP(
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateClosed{
if msg == pion.PeerConnectionStateClosed {
stream.RemoveConsumer(conn)
}
}
+12 -1
View File
@@ -3,39 +3,50 @@ module github.com/AlexxIT/go2rtc
go 1.17
require (
github.com/brutella/hap v0.0.17
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/mdns v1.0.5
github.com/pion/ice/v2 v2.2.6
github.com/pion/interceptor v0.1.11
github.com/pion/rtcp v1.2.9
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.5
github.com/pion/srtp/v2 v2.0.10
github.com/pion/stun v0.3.5
github.com/pion/webrtc/v3 v3.1.43
github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.7.1
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/brutella/dnssd v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
replace (
+31
View File
@@ -1,7 +1,11 @@
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78=
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg=
github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -9,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -30,6 +36,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -42,6 +50,9 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -128,7 +139,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -140,6 +156,8 @@ golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -152,7 +170,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
@@ -164,6 +186,8 @@ golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -176,7 +200,10 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -190,13 +217,17 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+5
View File
@@ -7,10 +7,12 @@ import (
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/homekit"
"github.com/AlexxIT/go2rtc/cmd/mse"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/srtp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"os"
@@ -33,6 +35,9 @@ func main() {
webrtc.Init()
mse.Init()
srtp.Init()
homekit.Init()
ngrok.Init()
debug.Init()
+61 -39
View File
@@ -1,6 +1,7 @@
package h264
import (
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
@@ -19,62 +20,83 @@ func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//println(packet.SequenceNumber, packet.Payload[0]&0x1F, packet.Payload[0], packet.Payload[1], packet.Marker, packet.Timestamp)
//nalUnitType := packet.Payload[0] & 0x1F
//fmt.Printf(
// "[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
// track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
// packet.PayloadType, packet.SSRC,
//)
data, err := depack.Unmarshal(packet.Payload)
if len(data) == 0 || err != nil {
// NALu packets can be split in different ways:
// - single type 7 and type 8 packets
// - join type 7 and type 8 packet (type 24)
// - split type 5 on multiple 28 packets
// - split type 5 on multiple separate 28 packets
units, err := depack.Unmarshal(packet.Payload)
if len(units) == 0 || err != nil {
return nil
}
naluType := NALUType(data)
//println(naluType, len(data))
for {
i := int(binary.BigEndian.Uint32(units)) + 4
unitAVC := units[:i]
switch naluType {
case NALUTypeSPS:
//println("new SPS")
sps = data
return nil
case NALUTypePPS:
//println("new PPS")
pps = data
return nil
}
unitType := NALUType(unitAVC)
switch unitType {
case NALUTypeSPS:
//println("new SPS")
sps = unitAVC
return nil
case NALUTypePPS:
//println("new PPS")
pps = unitAVC
return nil
}
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if !packet.Marker {
buffer = append(buffer, data...)
return nil
}
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if !packet.Marker {
buffer = append(buffer, unitAVC...)
return nil
}
if buffer != nil {
buffer = append(buffer, data...)
data = buffer
buffer = nil
}
if buffer != nil {
buffer = append(buffer, unitAVC...)
unitAVC = buffer
buffer = nil
}
var clone rtp.Packet
var clone rtp.Packet
if naluType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
if unitType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
clone.Payload = unitAVC
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = data
return push(&clone)
if len(units) == i {
return nil
}
units = units[i:]
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
# Homekit
> PS. Character = Characteristic
**Device** - HomeKit end device (swith, camera, etc)
- mDNS name: `MyCamera._hap._tcp.local.`
- DeviceID - mac-like: `0E:AA:CE:2B:35:71`
- HomeKit device is described by:
- one or more `Accessories` - has `AID` and `Services`
- `Services` - has `IID`, `Type` and `Characters`
- `Characters` - has `IID`, `Type`, `Format` and `Value`
**Client** - HomeKit client (iPhone, iPad, MacBook or opensource library)
- ClientID - static random UUID
- ClientPublic/ClientPrivate - static random 32 byte keypair
- can pair with Device (exchange ClientID/ClientPublic, ServerID/ServerPublic using Pin)
- can auth to Device using ClientPrivate
- holding persistant Secure connection to device
- can read device Accessories
- can read and write device Characters
- can subscribe on device Characters change (Event)
**Server** - HomeKit server (soft on end device or opensource library)
- ServerID - same as DeviceID (using for Client auth)
- ServerPublic/ServerPrivate - static random 32 byte keypair
## Useful links
- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)
- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)
+62
View File
@@ -0,0 +1,62 @@
package homekit
type Accessory struct {
AID int `json:"aid"`
Services []*Service `json:"services"`
}
type Accessories struct {
Accessories []*Accessory `json:"accessories"`
}
type Characters struct {
Characters []*Character `json:"characteristics"`
}
func (a *Accessory) GetService(servType string) *Service {
for _, serv := range a.Services {
if serv.Type == servType {
return serv
}
}
return nil
}
func (a *Accessory) GetCharacter(charType string) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.Type == charType {
return char
}
}
}
return nil
}
func (a *Accessory) GetCharacterByID(iid int) *Character {
for _, serv := range a.Services {
for _, char := range serv.Characters {
if char.IID == iid {
return char
}
}
}
return nil
}
type Service struct {
IID int `json:"iid"`
Type string `json:"type"`
Primary bool `json:"primary,omitempty"`
Hidden bool `json:"hidden,omitempty"`
Characters []*Character `json:"characteristics"`
}
func (s *Service) GetCharacter(charType string) *Character {
for _, char := range s.Characters {
if char.Type == charType {
return char
}
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package camera
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/homekit"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/rtp"
)
type Client struct {
client *homekit.Client
}
func NewClient(client *homekit.Client) *Client {
return &Client{client: client}
}
func (c *Client) StartStream2(ses *Session) (err error) {
// Step 1. Check if camera ready (free) to stream
var srv *homekit.Service
if srv, err = c.GetFreeStream(); err != nil {
return err
}
if srv == nil {
return errors.New("no free streams")
}
if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil {
return
}
return c.SetConfig(srv, ses.Config)
}
// GetFreeStream search free streaming service.
// Usual every HomeKit camera can stream only to two clients simultaniosly.
// So it has two similar services for streaming.
func (c *Client) GetFreeStream() (srv *homekit.Service, err error) {
var accs []*homekit.Accessory
if accs, err = c.client.GetAccessories(); err != nil {
return
}
for _, srv = range accs[0].Services {
for _, char := range srv.Characters {
if char.Type == characteristic.TypeStreamingStatus {
status := rtp.StreamingStatus{}
if err = char.ReadTLV8(&status); err != nil {
return
}
if status.Status == rtp.SessionStatusSuccess {
return
}
}
}
}
return nil, nil
}
func (c *Client) SetupEndpoins(
srv *homekit.Service, req *rtp.SetupEndpoints,
) (res *rtp.SetupEndpointsResponse, err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSetupEndpoints)
char.Event = nil
// encode new character value
if err = char.Write(req); err != nil {
return
}
// write (put) new endpoint value to device
if err = c.client.PutCharacters(char); err != nil {
return
}
// get new endpoint value from device (response)
if err = c.client.GetCharacter(char); err != nil {
return
}
// decode new endpoint value
res = &rtp.SetupEndpointsResponse{}
if err = char.ReadTLV8(res); err != nil {
return
}
return
}
func (c *Client) SetConfig(srv *homekit.Service, config *rtp.StreamConfiguration) (err error) {
// get setup endpoint character ID
char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration)
char.Event = nil
// encode new character value
if err = char.Write(config); err != nil {
panic(err)
}
// write (put) new character value to device
return c.client.PutCharacters(char)
}
+103
View File
@@ -0,0 +1,103 @@
package camera
import (
cryptorand "crypto/rand"
"encoding/binary"
"github.com/brutella/hap/rtp"
)
type Session struct {
Offer *rtp.SetupEndpoints
Answer *rtp.SetupEndpointsResponse
Config *rtp.StreamConfiguration
}
func NewSession() *Session {
sessionID := RandomBytes(16)
s := &Session{
Offer: &rtp.SetupEndpoints{
SessionId: sessionID,
Video: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
Audio: rtp.CryptoSuite{
MasterKey: RandomBytes(16),
MasterSalt: RandomBytes(14),
},
},
Config: &rtp.StreamConfiguration{
Command: rtp.SessionControlCommand{
Identifier: sessionID,
Type: rtp.SessionControlCommandTypeStart,
},
Video: rtp.VideoParameters{
CodecType: rtp.VideoCodecType_H264,
CodecParams: rtp.VideoCodecParameters{
Profiles: []rtp.VideoCodecProfile{
{Id: rtp.VideoCodecProfileMain},
},
Levels: []rtp.VideoCodecLevel{
{Level: rtp.VideoCodecLevel4},
},
Packetizations: []rtp.VideoCodecPacketization{
{Mode: rtp.VideoCodecPacketizationModeNonInterleaved},
},
},
Attributes: rtp.VideoCodecAttributes{
Width: 1920, Height: 1080, Framerate: 30,
},
RTP: rtp.RTPParams{
PayloadType: 99,
Ssrc: RandomUint32(),
Bitrate: 299,
Interval: 0.5,
ComfortNoisePayloadType: 98,
MTU: 0,
},
},
Audio: rtp.AudioParameters{
CodecType: rtp.AudioCodecType_AAC_ELD,
CodecParams: rtp.AudioCodecParameters{
Channels: 1,
Bitrate: rtp.AudioCodecBitrateVariable,
Samplerate: rtp.AudioCodecSampleRate16Khz,
PacketTime: 30,
},
RTP: rtp.RTPParams{
PayloadType: 110,
Ssrc: RandomUint32(),
Bitrate: 24,
Interval: 5,
MTU: 13,
},
ComfortNoise: false,
},
},
}
return s
}
func (s *Session) SetLocalEndpoint(host string, port uint16) {
s.Offer.ControllerAddr = rtp.Addr{
IPAddr: host,
VideoRtpPort: port,
AudioRtpPort: port,
}
}
func (s *Session) SetVideo() {
}
func RandomBytes(size int) []byte {
data := make([]byte, size)
_, _ = cryptorand.Read(data)
return data
}
func RandomUint32() uint32 {
data := make([]byte, 4)
_, _ = cryptorand.Read(data)
return binary.BigEndian.Uint32(data)
}
+133
View File
@@ -0,0 +1,133 @@
package homekit
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/brutella/hap/characteristic"
"github.com/brutella/hap/tlv8"
"io"
"net/http"
)
type Character struct {
AID int `json:"aid,omitempty"`
IID int `json:"iid"`
Type string `json:"type,omitempty"`
Format string `json:"format,omitempty"`
Value interface{} `json:"value,omitempty"`
Event interface{} `json:"ev,omitempty"`
Perms []string `json:"perms,omitempty"`
Description string `json:"description,omitempty"`
//MaxDataLen int `json:"maxDataLen"`
listeners map[io.Writer]bool
}
func (c *Character) AddListener(w io.Writer) {
// TODO: sync.Mutex
if c.listeners == nil {
c.listeners = map[io.Writer]bool{}
}
c.listeners[w] = true
}
func (c *Character) RemoveListener(w io.Writer) {
delete(c.listeners, w)
if len(c.listeners) == 0 {
c.listeners = nil
}
}
func (c *Character) NotifyListeners(ignore io.Writer) error {
if c.listeners == nil {
return nil
}
data, err := c.GenerateEvent()
if err != nil {
return err
}
for w, _ := range c.listeners {
if w == ignore {
continue
}
if _, err = w.Write(data); err != nil {
// error not a problem - just remove listener
c.RemoveListener(w)
}
}
return nil
}
// GenerateEvent with raw HTTP headers
func (c *Character) GenerateEvent() (data []byte, err error) {
chars := Characters{
Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}},
}
if data, err = json.Marshal(chars); err != nil {
return
}
res := http.Response{
StatusCode: http.StatusOK,
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header{"Content-Type": []string{MimeJSON}},
ContentLength: int64(len(data)),
Body: io.NopCloser(bytes.NewReader(data)),
}
buf := bytes.NewBuffer([]byte{0})
if err = res.Write(buf); err != nil {
return
}
copy(buf.Bytes(), "EVENT")
return buf.Bytes(), err
}
// Set new value and NotifyListeners
func (c *Character) Set(v interface{}) (err error) {
if err = c.Write(v); err != nil {
return
}
return c.NotifyListeners(nil)
}
// Write new value with right format
func (c *Character) Write(v interface{}) (err error) {
switch c.Format {
case characteristic.FormatTLV8:
var data []byte
if data, err = tlv8.Marshal(v); err != nil {
return
}
c.Value = base64.StdEncoding.EncodeToString(data)
case characteristic.FormatBool:
switch v.(type) {
case bool:
c.Value = v.(bool)
case float64:
c.Value = v.(float64) != 0
}
}
return
}
// ReadTLV8 value to right struct
func (c *Character) ReadTLV8(v interface{}) (err error) {
var data []byte
if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil {
return
}
return tlv8.Unmarshal(data, v)
}
func (c *Character) ReadBool() bool {
return c.Value.(bool)
}
+732
View File
@@ -0,0 +1,732 @@
package homekit
import (
"bufio"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"io"
"net"
"net/http"
"net/url"
"strings"
)
// Client for HomeKit. DevicePublic can be null.
type Client struct {
streamer.Element
DeviceAddress string // including port
DeviceID string
DevicePublic []byte
ClientID string
ClientPrivate []byte
OnEvent func(res *http.Response)
Output func(msg interface{})
conn net.Conn
secure *Secure
httpResponse chan *bufio.Reader
}
func NewClient(rawURL string) (*Client, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
c := &Client{
DeviceAddress: u.Host,
DeviceID: query.Get("device_id"),
DevicePublic: DecodeKey(query.Get("device_public")),
ClientID: query.Get("client_id"),
ClientPrivate: DecodeKey(query.Get("client_private")),
}
return c, nil
}
func Pair(deviceID, pin string) (*Client, error) {
entry := mdns.GetEntry(deviceID)
if entry == nil {
return nil, errors.New("can't find device via mDNS")
}
c := &Client{
DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port),
DeviceID: deviceID,
ClientID: GenerateUUID(),
ClientPrivate: GenerateKey(),
}
var mfi bool
for _, field := range entry.InfoFields {
if field[:2] == "ff" {
if field[3] == '1' {
mfi = true
}
break
}
}
return c, c.Pair(mfi, pin)
}
func (c *Client) ClientPublic() []byte {
return c.ClientPrivate[32:]
}
func (c *Client) URL() string {
return fmt.Sprintf(
"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x",
c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,
)
}
func (c *Client) DialAndServe() error {
if err := c.Dial(); err != nil {
return err
}
return c.Handle()
}
func (c *Client) Dial() error {
// update device host before dial
if host := mdns.GetAddress(c.DeviceID); host != "" {
c.DeviceAddress = host
}
var err error
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return err
}
// STEP M1: send our session public to device
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
// 1. generate payload
// important not include other fields
requestM1 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
}{
State: hap.M1,
PublicKey: sessionPublic[:],
}
// 2. pack payload to TLV8
buf, err := tlv8.Marshal(requestM1)
if err != nil {
return err
}
// 3. send request
resp, err := c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M2: unpack deviceID from response
responseM2 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil {
return err
}
// 1. generate session shared key
var deviceSessionPublic [32]byte
copy(deviceSessionPublic[:], responseM2.PublicKey)
sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic)
sessionKey, err := hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
)
// 2. decrypt M2 response with session key
msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16]
var mac [16]byte
copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg02"), msg, mac, nil,
)
// 3. unpack payload from TLV8
payloadM2 := PairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM2); err != nil {
return err
}
// 4. verify signature for M2 response with device public
// device session + device id + our session
if c.DevicePublic != nil {
buf = nil
buf = append(buf, responseM2.PublicKey[:]...)
buf = append(buf, []byte(payloadM2.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
c.DevicePublic[:], buf, payloadM2.Signature,
) {
return errors.New("device public signature invalid")
}
}
// STEP M3: send our clientID to device
// 1. generate signature with our private key
// (our session + our ID + device session)
buf = nil
buf = append(buf, sessionPublic[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, responseM2.PublicKey[:]...)
signature, err := ed25519.Signature(c.ClientPrivate[:], buf)
if err != nil {
return err
}
// 2. generate payload
payloadM3 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
Signature: signature,
}
// 3. pack payload to TLV8
buf, err = tlv8.Marshal(payloadM3)
if err != nil {
return err
}
// 4. encrypt payload with session key
msg, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg03"), buf, nil,
)
// 4. generate request
requestM3 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M3,
EncryptedData: append(msg, mac[:]...),
}
// 5. pack payload to TLV8
buf, err = tlv8.Marshal(requestM3)
if err != nil {
return err
}
resp, err = c.Post(UriPairVerify, buf)
if err != nil {
return err
}
// STEP M4. Read response
responseM4 := PairVerifyPayload{}
if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil {
return err
}
// 1. check response state
if responseM4.State != 4 || responseM4.Status != 0 {
return fmt.Errorf("wrong M4 response: %+v", responseM4)
}
c.secure, err = NewSecure(sessionShared, false)
//c.secure.Buffer = bytes.NewBuffer(nil)
c.secure.Conn = c.conn
c.httpResponse = make(chan *bufio.Reader, 10)
return err
}
// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c
func (c *Client) Pair(mfi bool, pin string) (err error) {
pin = strings.ReplaceAll(pin, "-", "")
if len(pin) != 8 {
return fmt.Errorf("wrong PIN format: %s", pin)
}
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:]
c.conn, err = net.Dial("tcp", c.DeviceAddress)
if err != nil {
return
}
// STEP M1. Generate request
reqM1 := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
State: hap.M1,
}
if mfi {
reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0
}
buf, err := tlv8.Marshal(reqM1)
if err != nil {
return
}
// STEP M1. Send request
res, err := c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M2. Read response
resM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"` // server public key, aka session.B
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil {
return
}
if resM2.State != 2 || resM2.Error > 0 {
return fmt.Errorf("wrong M2: %+v", resM2)
}
// STEP M3. Generate session using pin
username := []byte("Pair-Setup")
SRP, err := srp.NewSRP(
"rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username),
)
if err != nil {
return
}
SRP.SaltLength = 16
// username: "Pair-Setup"
// password: PIN (with dashes)
session := SRP.NewClientSession(username, []byte(pin))
sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey)
if err != nil {
return
}
// STEP M3. Generate request
reqM3 := struct {
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
PublicKey: session.GetA(), // client public key, aka session.A
Proof: session.ComputeAuthenticator(),
State: hap.M3,
}
buf, err = tlv8.Marshal(reqM3)
if err != nil {
return err
}
// STEP M3. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M4. Read response
resM4 := struct {
Proof []byte `tlv8:"4"` // server proof
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil {
return
}
if resM4.Error == 2 {
return fmt.Errorf("wrong PIN: %s", pin)
}
if resM4.State != 4 || resM4.Error > 0 {
return fmt.Errorf("wrong M4: %+v", resM4)
}
// STEP M4. Verify response
if !session.VerifyServerAuthenticator(resM4.Proof) {
return errors.New("verify server auth fail")
}
// STEP M5. Generate signature
saltKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
)
if err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(c.ClientID)...)
buf = append(buf, c.ClientPublic()...)
signature, err := ed25519.Signature(c.ClientPrivate, buf)
if err != nil {
return
}
// STEP M5. Generate payload
msgM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: c.ClientID,
PublicKey: c.ClientPublic(),
Signature: signature,
}
buf, err = tlv8.Marshal(msgM5)
if err != nil {
return
}
// STEP M5. Encrypt payload
sessionKey, err := hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
)
if err != nil {
return
}
buf, mac, _ := chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg05"), buf, nil,
)
// STEP M5. Generate request
reqM5 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
EncryptedData: append(buf, mac[:]...),
State: hap.M5,
}
buf, err = tlv8.Marshal(reqM5)
if err != nil {
return err
}
// STEP M5. Send request
res, err = c.Post(UriPairSetup, buf)
if err != nil {
return
}
// STEP M6. Read response
resM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
}{}
if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil {
return
}
if resM6.State != 6 || resM6.Error > 0 {
return fmt.Errorf("wrong M6: %+v", resM2)
}
// STEP M6. Decrypt payload
msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16]
copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC)
buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg06"), msg, mac, nil,
)
if err != nil {
return
}
msgM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &msgM6); err != nil {
return
}
// STEP M6. Verify payload
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, msgM6.Identifier...)
buf = append(buf, msgM6.PublicKey...)
if !ed25519.ValidateSignature(
msgM6.PublicKey[:], buf, msgM6.Signature,
) {
return errors.New("wrong server signature")
}
if c.DeviceID != string(msgM6.Identifier) {
return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier)
}
c.DevicePublic = msgM6.PublicKey
return nil
}
func (c *Client) Close() error {
if c.conn == nil {
return nil
}
conn := c.conn
c.conn = nil
return conn.Close()
}
func (c *Client) GetAccessories() ([]*Accessory, error) {
res, err := c.Get("/accessories")
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
p := Accessories{}
if err = json.Unmarshal(data, &p); err != nil {
return nil, err
}
for _, accs := range p.Accessories {
for _, serv := range accs.Services {
for _, char := range serv.Characters {
char.AID = accs.AID
}
}
}
return p.Accessories, nil
}
func (c *Client) GetCharacters(query string) ([]*Character, error) {
res, err := c.Get("/characteristics?id=" + query)
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return nil, err
}
return ch.Characters, nil
}
func (c *Client) GetCharacter(char *Character) error {
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
chars, err := c.GetCharacters(query)
if err != nil {
return err
}
char.Value = chars[0].Value
return nil
}
func (c *Client) PutCharacters(characters ...*Character) (err error) {
for i, char := range characters {
if char.Event != nil {
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
} else {
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
}
characters[i] = char
}
var data []byte
if data, err = json.Marshal(Characters{characters}); err != nil {
return
}
var res *http.Response
if res, err = c.Put("/characteristics", data); err != nil {
return
}
if res.StatusCode >= 400 {
return errors.New("wrong response status")
}
return
}
func (c *Client) GetImage(width, height int) ([]byte, error) {
res, err := c.Post(
"/resource", []byte(fmt.Sprintf(
`{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`,
width, height,
)),
)
if err != nil {
return nil, err
}
return io.ReadAll(res.Body)
}
//func (c *Client) onEventData(r io.Reader) error {
// if c.OnEvent == nil {
// return nil
// }
//
// data, err := io.ReadAll(r)
//
// ch := Characters{}
// if err = json.Unmarshal(data, &ch); err != nil {
// return err
// }
//
// c.OnEvent(ch.Characters)
//
// return nil
//}
func (c *Client) ListPairings() error {
pReq := struct {
Method byte `tlv8:"0"`
State byte `tlv8:"6"`
}{
Method: hap.MethodListPairings,
State: hap.M1,
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
// TODO: don't know how to fix array of items
var pRes struct {
State byte `tlv8:"6"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {
pReq := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
Permission byte `tlv8:"11"`
}{
Method: hap.MethodAddPairing,
Identifier: clientID,
PublicKey: clientPublic,
State: hap.M1,
Permission: hap.PermissionUser,
}
if admin {
pReq.Permission = hap.PermissionAdmin
}
data, err := tlv8.Marshal(pReq)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var pRes struct {
State byte `tlv8:"6"`
Unknown byte `tlv8:"7"`
}
if err = tlv8.Unmarshal(data, &pRes); err != nil {
return err
}
return nil
}
func (c *Client) DeletePairing(id string) error {
reqM1 := struct {
State byte `tlv8:"6"`
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
}{
State: hap.M1,
Method: hap.MethodDeletePairing,
Identifier: id,
}
data, err := tlv8.Marshal(reqM1)
if err != nil {
return err
}
res, err := c.Post("/pairings", data)
if err != nil {
return err
}
data, err = io.ReadAll(res.Body)
var resM2 struct {
State byte `tlv8:"6"`
}
if err = tlv8.Unmarshal(data, &resM2); err != nil {
return err
}
if resM2.State != hap.M2 {
return errors.New("wrong state")
}
return nil
}
func (c *Client) LocalAddr() string {
return c.conn.LocalAddr().String()
}
func DecodeKey(s string) []byte {
if s == "" {
return nil
}
data, err := hex.DecodeString(s)
if err != nil {
return nil
}
return data
}
+90
View File
@@ -0,0 +1,90 @@
package homekit
import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
)
const DeviceAID = 1 // TODO: fix someday
func GenerateID(name string) string {
sum := sha512.Sum512([]byte(name))
return fmt.Sprintf(
"%02X:%02X:%02X:%02X:%02X:%02X",
sum[0], sum[1], sum[2], sum[3], sum[4], sum[5],
)
}
func GenerateUUID() string {
//12345678-9012-3456-7890-123456789012
data := make([]byte, 16)
_, _ = rand.Read(data)
s := hex.EncodeToString(data)
return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
}
type PairVerifyPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Status byte `tlv8:"7"`
Signature []byte `tlv8:"10"`
}
//func (c *Character) Unmarshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := base64.StdEncoding.DecodeString(c.Value.(string))
// if err != nil {
// return err
// }
// return tlv8.Unmarshal(data, value)
// }
// return nil
//}
//func (c *Character) Marshal(value interface{}) error {
// switch c.Format {
// case characteristic.FormatTLV8:
// data, err := tlv8.Marshal(value)
// if err != nil {
// return err
// }
// c.Value = base64.StdEncoding.EncodeToString(data)
// }
// return nil
//}
func (c *Character) String() string {
data, err := json.Marshal(c)
if err != nil {
return "ERROR"
}
return string(data)
}
func UnmarshalEvent(res *http.Response) (char *Character, err error) {
var data []byte
if data, err = io.ReadAll(res.Body); err != nil {
return
}
ch := Characters{}
if err = json.Unmarshal(data, &ch); err != nil {
return
}
if len(ch.Characters) > 1 {
panic("not implemented")
}
char = ch.Characters[0]
return
}
+246
View File
@@ -0,0 +1,246 @@
package homekit
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/textproto"
"strconv"
)
const (
MimeTLV8 = "application/pairing+tlv8"
MimeJSON = "application/hap+json"
UriPairSetup = "/pair-setup"
UriPairVerify = "/pair-verify"
UriPairings = "/pairings"
UriAccessories = "/accessories"
UriCharacteristics = "/characteristics"
UriResource = "/resource"
)
func (c *Client) Write(p []byte) (r io.Reader, err error) {
if c.secure == nil {
if _, err = c.conn.Write(p); err == nil {
r = bufio.NewReader(c.conn)
}
} else {
if _, err = c.secure.Write(p); err == nil {
r = <-c.httpResponse
}
}
return
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
if c.secure == nil {
// insecure requests
if err := req.Write(c.conn); err != nil {
return nil, err
}
return http.ReadResponse(bufio.NewReader(c.conn), req)
}
// secure support write interface to connection
if err := req.Write(c.secure); err != nil {
return nil, err
}
// get decrypted buffer from connection
buf := <-c.httpResponse
return http.ReadResponse(buf, req)
}
func (c *Client) Get(uri string) (*http.Response, error) {
req, err := http.NewRequest(
"GET", "http://"+c.DeviceAddress+uri, nil,
)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Client) Post(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"POST", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case "/pair-verify", "/pairings":
req.Header.Set("Content-Type", MimeTLV8)
case UriResource:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Client) Put(uri string, data []byte) (*http.Response, error) {
req, err := http.NewRequest(
"PUT", "http://"+c.DeviceAddress+uri,
bytes.NewReader(data),
)
if err != nil {
return nil, err
}
switch uri {
case UriCharacteristics:
req.Header.Set("Content-Type", MimeJSON)
}
return c.Do(req)
}
func (c *Client) Handle() (err error) {
defer func() {
if c.conn == nil {
err = nil
}
}()
b := make([]byte, 512000)
for {
var total, content int
header := -1
for {
var n1 int
n1, err = c.secure.Read(b[total:])
if err != nil {
return err
}
if n1 == 0 {
return io.EOF
}
total += n1
// TODO: rewrite
if header == -1 {
// step 1. wait whole header
header = bytes.Index(b[:total], []byte("\r\n\r\n"))
if header < 0 {
continue
}
header += 4
// step 2. check content-length
i1 := bytes.Index(b[:total], []byte("Content-Length: "))
if i1 < 0 {
break
}
i1 += 16
i2 := bytes.IndexByte(b[i1:total], '\r')
content, err = strconv.Atoi(string(b[i1 : i1+i2]))
if err != nil {
break
}
}
if total >= header+content {
break
}
}
// copy slice to buffer
buf := bytes.NewBuffer(make([]byte, 0, total))
buf.Write(b[:total])
r := bufio.NewReader(buf)
// EVENT/1.0 200 OK
if b[0] == 'E' {
if c.OnEvent == nil {
continue
}
tp := textproto.NewReader(r)
var s string
if s, err = tp.ReadLine(); err != nil {
return err
}
if s != "EVENT/1.0 200 OK" {
return errors.New("wrong response")
}
var mimeHeader textproto.MIMEHeader
if mimeHeader, err = tp.ReadMIMEHeader(); err != nil {
return err
}
var cl int
if cl, err = strconv.Atoi(
mimeHeader.Get("Content-Length"),
); err != nil {
return err
}
res := http.Response{
StatusCode: 200,
Proto: "EVENT/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Header: http.Header(mimeHeader),
ContentLength: int64(cl),
Body: io.NopCloser(r),
}
c.OnEvent(&res)
continue
}
//if bytes.Index(b, []byte("image/jpeg")) > 0 {
// if total, err = c.secure.Read(b); err != nil {
// return
// }
// buf.Write(b[:total])
//}
c.httpResponse <- r
}
}
func WriteStatusCode(w io.Writer, statusCode int) (err error) {
body := []byte(fmt.Sprintf(
"HTTP/1.1 %d %s\n\n", statusCode, http.StatusText(statusCode),
))
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteResponse(
w io.Writer, statusCode int, contentType string, body []byte,
) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 %d %s\nContent-Type: %s\nContent-Length: %d\n\n",
statusCode, http.StatusText(statusCode), contentType, len(body),
)
body = append([]byte(header), body...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
func WriteChunked(w io.Writer, contentType string, body []byte) (err error) {
header := fmt.Sprintf(
"HTTP/1.1 200 OK\nContent-Type: %s\nTransfer-Encoding: chunked\n\n%x\n",
contentType, len(body),
)
body = append([]byte(header), body...)
body = append(body, "\n0\n\n"...)
//print("<<<", string(body), "<<<\n")
_, err = w.Write(body)
return
}
+42
View File
@@ -0,0 +1,42 @@
package mdns
import (
"fmt"
"github.com/hashicorp/mdns"
"strings"
)
const Suffix = "._hap._tcp.local."
func GetAll() chan *mdns.ServiceEntry {
entries := make(chan *mdns.ServiceEntry)
params := &mdns.QueryParam{
Service: "_hap._tcp", Entries: entries, DisableIPv6: true,
}
go func() {
_ = mdns.Query(params)
close(entries)
}()
return entries
}
func GetAddress(deviceID string) string {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port)
}
}
return ""
}
func GetEntry(deviceID string) *mdns.ServiceEntry {
for entry := range GetAll() {
if strings.Contains(entry.Info, deviceID) {
return entry
}
}
return nil
}
+53
View File
@@ -0,0 +1,53 @@
package mdns
import (
"github.com/hashicorp/mdns"
"net"
)
const HostHeaderTail = "._hap._tcp.local"
func NewServer(name string, port int, ips []net.IP, txt []string) (*mdns.Server, error) {
if ips == nil || ips[0] == nil {
ips = LocalIPs()
}
// important to set hostName manually with any value and `.local.` tail
// important to set ips manually
service, _ := mdns.NewMDNSService(
name, "_hap._tcp", "", name+".local.", port, ips, txt,
)
return mdns.NewServer(&mdns.Config{Zone: service})
}
func LocalIPs() []net.IP {
ifaces, err := net.Interfaces()
if err != nil {
return nil
}
var ips []net.IP
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
var addrs []net.Addr
if addrs, err = iface.Addrs(); err != nil {
continue
}
for _, addr := range addrs {
switch addr := addr.(type) {
case *net.IPNet:
ips = append(ips, addr.IP)
case *net.IPAddr:
ips = append(ips, addr.IP)
}
}
}
return ips
}
+410
View File
@@ -0,0 +1,410 @@
package homekit
import (
"bufio"
"crypto/sha512"
"errors"
"github.com/brutella/hap"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/curve25519"
"github.com/brutella/hap/ed25519"
"github.com/brutella/hap/hkdf"
"github.com/brutella/hap/tlv8"
"github.com/tadglines/go-pkgs/crypto/srp"
"net"
"net/http"
)
type pairSetupPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
Proof []byte `tlv8:"4"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Error byte `tlv8:"7"`
RetryDelay byte `tlv8:"8"`
Certificate []byte `tlv8:"9"`
Signature []byte `tlv8:"10"`
Permissions byte `tlv8:"11"`
FragmentData []byte `tlv8:"13"`
FragmentLast []byte `tlv8:"14"`
}
func (s *Server) PairSetupHandler(
conn net.Conn, req *http.Request,
) (clientID string, err error) {
// STEP 1. Request from iPhone
payloadM1 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
// generate our session public and salt using PIN
username := []byte("Pair-Setup")
var SRP *srp.SRP
if SRP, err = srp.NewSRP(
"rfc5054.3072", sha512.New,
keyDerivativeFuncRFC2945(username),
); err != nil {
return
}
SRP.SaltLength = 16
var salt, verifier []byte
if salt, verifier, err = SRP.ComputeVerifier([]byte(s.Pin)); err != nil {
return
}
session := SRP.NewServerSession(username, salt, verifier)
// STEP 2. Response to iPhone
payloadM2 := struct {
Salt []byte `tlv8:"2"`
PublicKey []byte `tlv8:"3"`
State byte `tlv8:"6"`
}{
State: hap.M2,
PublicKey: session.GetB(),
Salt: salt,
}
var buf []byte
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
payloadM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM3); err != nil {
return
}
if payloadM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
// important to compute key before verify client
var sessionShared []byte
if sessionShared, err = session.ComputeKey(payloadM3.PublicKey); err != nil {
return
}
// support skip pin verify (any pin accepted)
if s.Pin != "" && !session.VerifyClientAuthenticator(payloadM3.Proof) {
err = errors.New("client proof is invalid")
return
}
serverProof := session.ComputeAuthenticator(payloadM3.Proof)
// STEP 4. Response to iPhone
payloadM4 := struct {
Proof []byte `tlv8:"4"`
State byte `tlv8:"6"`
}{
State: hap.M4, Proof: serverProof,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP 5. Request from iPhone
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM5 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM5); err != nil {
return
}
if encryptedM5.State != hap.M5 {
err = errors.New("wrong state")
return
}
msg := encryptedM5.EncryptedData[:len(encryptedM5.EncryptedData)-16]
var mac [16]byte
copy(mac[:], encryptedM5.EncryptedData[len(msg):]) // 16 byte (MAC)
// decrypt message using session shared
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Encrypt-Salt"),
[]byte("Pair-Setup-Encrypt-Info"),
); err != nil {
return
}
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PS-Msg05"), msg, mac, nil,
); err != nil {
return
}
// unpack message from TLV8
payloadM5 := struct {
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{}
if err = tlv8.Unmarshal(buf, &payloadM5); err != nil {
return
}
// 3. verify client ID and Public
var saltKey [32]byte
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"),
[]byte("Pair-Setup-Controller-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, payloadM5.Identifier...)
buf = append(buf, payloadM5.PublicKey[:]...)
if !ed25519.ValidateSignature(
payloadM5.PublicKey[:], buf, payloadM5.Signature,
) {
err = errors.New("wrong client signature")
return
}
// 4. generate signature to our ID adn Public
if saltKey, err = hkdf.Sha512(
sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"),
[]byte("Pair-Setup-Accessory-Sign-Info"),
); err != nil {
return
}
buf = nil
buf = append(buf, saltKey[:]...)
buf = append(buf, []byte(s.ServerID)...)
buf = append(buf, s.ServerPrivate[32:]...) // ServerPublic
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate, buf); err != nil {
return
}
// 5. pack our ID and Public
payloadM6 := struct {
Identifier []byte `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Signature []byte `tlv8:"10"`
}{
Identifier: []byte(s.ServerID),
PublicKey: s.ServerPrivate[32:],
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM6); err != nil {
return
}
// 6. encrypt message
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PS-Msg06"), buf, nil,
)
// STEP 6. Response to iPhone
encryptedM6 := struct {
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
}{
State: hap.M6,
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM6); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
if s.Pairings != nil {
s.Pairings[payloadM5.Identifier] = append(
payloadM5.PublicKey, 1, // adds admin (1) flag
)
}
clientID = payloadM5.Identifier
return
}
func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {
return func(salt, pin []byte) []byte {
h := sha512.New()
h.Write(username)
h.Write([]byte(":"))
h.Write(pin)
t2 := h.Sum(nil)
h.Reset()
h.Write(salt)
h.Write(t2)
return h.Sum(nil)
}
}
type pairVerifyPayload struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
State byte `tlv8:"6"`
Signature []byte `tlv8:"10"`
}
func (s *Server) PairVerifyHandler(
conn net.Conn, req *http.Request,
) (secure *Secure, err error) {
// STEP M1. Request from iPhone
payloadM1 := pairVerifyPayload{}
if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil {
return
}
if payloadM1.State != hap.M1 {
err = errors.New("wrong state")
return
}
var clientPublic [32]byte
copy(clientPublic[:], payloadM1.PublicKey)
// Generate the key pair.
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
sessionShared := curve25519.SharedSecret(sessionPrivate, clientPublic)
var sessionKey [32]byte
if sessionKey, err = hkdf.Sha512(
sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"),
[]byte("Pair-Verify-Encrypt-Info"),
); err != nil {
return
}
var buf []byte
buf = append(buf, sessionPublic[:]...)
buf = append(buf, s.ServerID...)
buf = append(buf, clientPublic[:]...)
var signature []byte
if signature, err = ed25519.Signature(s.ServerPrivate[:], buf); err != nil {
return
}
// STEP M2. Response to iPhone
payloadM2 := struct {
Identifier string `tlv8:"1"`
Signature []byte `tlv8:"10"`
}{
Identifier: s.ServerID,
Signature: signature,
}
if buf, err = tlv8.Marshal(payloadM2); err != nil {
return
}
var mac [16]byte
buf, mac, _ = chacha20poly1305.EncryptAndSeal(
sessionKey[:], []byte("PV-Msg02"), buf, nil,
)
encryptedM2 := struct {
State byte `tlv8:"6"`
PublicKey []byte `tlv8:"3"`
EncryptedData []byte `tlv8:"5"`
}{
State: hap.M2,
PublicKey: sessionPublic[:],
EncryptedData: append(buf, mac[:]...),
}
if buf, err = tlv8.Marshal(encryptedM2); err != nil {
return
}
if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil {
return
}
// STEP M3. Request from iPhone
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
encryptedM3 := pairSetupPayload{}
if err = tlv8.UnmarshalReader(req.Body, &encryptedM3); err != nil {
return
}
if encryptedM3.State != hap.M3 {
err = errors.New("wrong state")
return
}
buf = encryptedM3.EncryptedData[:len(encryptedM3.EncryptedData)-16]
copy(mac[:], encryptedM3.EncryptedData[len(buf):]) // 16 byte (MAC)
if buf, err = chacha20poly1305.DecryptAndVerify(
sessionKey[:], []byte("PV-Msg03"), buf, mac, nil,
); err != nil {
return
}
payloadM3 := pairVerifyPayload{}
if err = tlv8.Unmarshal(buf, &payloadM3); err != nil {
return
}
if s.Pairings != nil {
pairing := s.Pairings[payloadM3.Identifier]
if pairing == nil {
err = errors.New("not paired yet")
return
}
buf = nil
buf = append(buf, clientPublic[:]...)
buf = append(buf, []byte(payloadM3.Identifier)...)
buf = append(buf, sessionPublic[:]...)
if !ed25519.ValidateSignature(
pairing[:32], buf, payloadM3.Signature,
) {
err = errors.New("signature invalid")
return
}
}
// STEP M4. Response to iPhone
payloadM4 := struct {
State byte `tlv8:"6"`
}{
State: hap.M4,
}
if buf, err = tlv8.Marshal(payloadM4); err != nil {
return
}
err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf)
if secure, err = NewSecure(sessionShared, true); err != nil {
return
}
secure.Conn = conn
return
}
+137
View File
@@ -0,0 +1,137 @@
package homekit
import (
"encoding/binary"
"github.com/brutella/hap/chacha20poly1305"
"github.com/brutella/hap/hkdf"
"net"
"sync"
)
type Secure struct {
Conn net.Conn
encryptKey [32]byte
decryptKey [32]byte
encryptCount uint64
decryptCount uint64
mx sync.Mutex
}
func NewSecure(sharedKey [32]byte, isServer bool) (*Secure, error) {
salt := []byte("Control-Salt")
key1, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Read-Encryption-Key"),
)
if err != nil {
return nil, err
}
key2, err := hkdf.Sha512(
sharedKey[:], salt, []byte("Control-Write-Encryption-Key"),
)
if err != nil {
return nil, err
}
if isServer {
return &Secure{encryptKey: key1, decryptKey: key2}, nil
} else {
return &Secure{encryptKey: key2, decryptKey: key1}, nil
}
}
func (s *Secure) Read(b []byte) (n int, err error) {
for {
var length uint16
if err = binary.Read(s.Conn, binary.LittleEndian, &length); err != nil {
return
}
var enc = make([]byte, length)
if err = binary.Read(s.Conn, binary.LittleEndian, &enc); err != nil {
return
}
var mac [16]byte
if err = binary.Read(s.Conn, binary.LittleEndian, &mac); err != nil {
return
}
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.decryptCount)
s.decryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, length)
var msg []byte
if msg, err = chacha20poly1305.DecryptAndVerify(
s.decryptKey[:], nonce[:], enc, mac, bLength,
); err != nil {
return
}
n += copy(b[n:], msg)
// Finish when all bytes fit in b
if length < packetLengthMax {
//fmt.Printf(">>>%s>>>\n", b[:n])
return
}
}
}
func (s *Secure) Write(b []byte) (n int, err error) {
s.mx.Lock()
defer s.mx.Unlock()
var packetLen = len(b)
for {
if packetLen > packetLengthMax {
packetLen = packetLengthMax
}
//fmt.Printf("<<<%s<<<\n", b[:packetLen])
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], s.encryptCount)
s.encryptCount++
bLength := make([]byte, 2)
binary.LittleEndian.PutUint16(bLength, uint16(packetLen))
var enc []byte
var mac [16]byte
enc, mac, err = chacha20poly1305.EncryptAndSeal(
s.encryptKey[:], nonce[:], b[:packetLen], bLength[:],
)
if err != nil {
return
}
enc = append(bLength, enc...)
enc = append(enc, mac[:]...)
if _, err = s.Conn.Write(enc); err != nil {
return
}
n += packetLen
if packetLen == packetLengthMax {
b = b[packetLengthMax:]
packetLen = len(b)
} else {
break
}
}
return
}
const (
// packetLengthMax is the max length of encrypted packets
packetLengthMax = 0x400
)
+155
View File
@@ -0,0 +1,155 @@
package homekit
import (
"bufio"
"crypto/ed25519"
"github.com/brutella/hap"
"github.com/brutella/hap/tlv8"
"io"
"net"
"net/http"
)
type Server struct {
// Pin can't be null because server proof will be wrong
Pin string `json:"-"`
ServerID string `json:"server_id"`
// 32 bytes private key + 32 bytes public key
ServerPrivate []byte `json:"server_private"`
// Pairings can be nil for disable pair verify check
// ClientID: 32 bytes client public + 1 byte (isAdmin)
Pairings map[string][]byte `json:"pairings"`
DefaultPlainHandler func(w io.Writer, r *http.Request) error
DefaultSecureHandler func(w io.Writer, r *http.Request) error
OnPairChange func(clientID string, clientPublic []byte) `json:"-"`
OnRequest func(w io.Writer, r *http.Request) `json:"-"`
}
func GenerateKey() []byte {
_, key, _ := ed25519.GenerateKey(nil)
return key
}
func NewServer(name string) *Server {
return &Server{
ServerID: GenerateID(name),
ServerPrivate: GenerateKey(),
Pairings: map[string][]byte{},
}
}
func (s *Server) Serve(address string) (err error) {
var ln net.Listener
if ln, err = net.Listen("tcp", address); err != nil {
return
}
for {
var conn net.Conn
if conn, err = ln.Accept(); err != nil {
continue
}
go func() {
//fmt.Printf("[%s] new connection\n", conn.RemoteAddr().String())
s.Accept(conn)
//fmt.Printf("[%s] close connection\n", conn.RemoteAddr().String())
}()
}
}
func (s *Server) Accept(conn net.Conn) (err error) {
defer conn.Close()
var req *http.Request
r := bufio.NewReader(conn)
if req, err = http.ReadRequest(r); err != nil {
return
}
return s.HandleRequest(conn, req)
}
func (s *Server) HandleRequest(conn net.Conn, req *http.Request) (err error) {
if s.OnRequest != nil {
s.OnRequest(conn, req)
}
switch req.URL.Path {
case UriPairSetup:
if _, err = s.PairSetupHandler(conn, req); err != nil {
return
}
case UriPairVerify:
var secure *Secure
if secure, err = s.PairVerifyHandler(conn, req); err != nil {
return
}
err = s.HandleSecure(secure)
default:
if s.DefaultPlainHandler != nil {
err = s.DefaultPlainHandler(conn, req)
}
}
return
}
func (s *Server) HandleSecure(secure *Secure) (err error) {
r := bufio.NewReader(secure)
for {
var req *http.Request
if req, err = http.ReadRequest(r); err != nil {
return
}
if s.OnRequest != nil {
s.OnRequest(secure, req)
}
switch req.URL.Path {
case UriPairings:
s.HandlePairings(secure, req)
default:
if err = s.DefaultSecureHandler(secure, req); err != nil {
return
}
}
}
}
func (s *Server) HandlePairings(w io.Writer, r *http.Request) {
req := struct {
Method byte `tlv8:"0"`
Identifier string `tlv8:"1"`
PublicKey []byte `tlv8:"3"`
Permission byte `tlv8:"11"`
State byte `tlv8:"6"`
}{}
if err := tlv8.UnmarshalReader(r.Body, &req); err != nil {
panic(err)
}
switch req.Method {
case hap.MethodAddPairing, hap.MethodDeletePairing:
res := struct {
State byte `tlv8:"6"`
}{
State: hap.M2,
}
data, err := tlv8.Marshal(res)
if err != nil {
panic(err)
}
if err = WriteResponse(w, http.StatusOK, MimeJSON, data); err != nil {
panic(err)
}
}
}
+9 -2
View File
@@ -331,11 +331,18 @@ func (c *Conn) SetupMedia(
return nil, fmt.Errorf("wrong media: %v", media)
}
trackURL, err := url.Parse(media.Control)
rawURL := media.Control
if !strings.Contains(rawURL, "://") {
rawURL = c.URL.String()
if !strings.HasSuffix(rawURL, "/") {
rawURL += "/"
}
rawURL += media.Control
}
trackURL, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
trackURL = c.URL.ResolveReference(trackURL)
req := &tcp.Request{
Method: MethodSetup,
+61
View File
@@ -0,0 +1,61 @@
package srtp
import (
"encoding/binary"
"net"
)
// Server using same UDP port for SRTP and for SRTCP as the iPhone does
// this is not really necessary but anyway
type Server struct {
sessions map[uint32]*Session
}
func (s *Server) AddSession(session *Session) {
if s.sessions == nil {
s.sessions = map[uint32]*Session{}
}
s.sessions[session.RemoteSSRC] = session
}
func (s *Server) RemoveSession(session *Session) {
delete(s.sessions, session.RemoteSSRC)
}
func (s *Server) Serve(conn net.PacketConn) error {
buf := make([]byte, 2048)
for {
n, addr, err := conn.ReadFrom(buf)
if err != nil {
return err
}
// Multiplexing RTP Data and Control Packets on a Single Port
// https://datatracker.ietf.org/doc/html/rfc5761
// this is default position for SSRC in RTP packet
ssrc := binary.BigEndian.Uint32(buf[8:])
session, ok := s.sessions[ssrc]
if ok {
if session.Write == nil {
session.Write = func(b []byte) (int, error) {
return conn.WriteTo(b, addr)
}
}
if err = session.HandleRTP(buf[:n]); err != nil {
return err
}
} else {
// this is default position for SSRC in RTCP packet
ssrc = binary.BigEndian.Uint32(buf[4:])
if session, ok = s.sessions[ssrc]; !ok {
continue // skip unknown ssrc
}
if err = session.HandleRTCP(buf[:n]); err != nil {
return err
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
package srtp
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"github.com/pion/srtp/v2"
)
type Session struct {
LocalSSRC uint32 // outgoing SSRC
RemoteSSRC uint32 // incoming SSRC
localCtx *srtp.Context // write context
remoteCtx *srtp.Context // read context
Write func(b []byte) (int, error)
Track *streamer.Track
}
func (s *Session) SetKeys(
localKey, localSalt, remoteKey, remoteSalt []byte,
) (err error) {
if s.localCtx, err = srtp.CreateContext(
localKey, localSalt, GuessProfile(localKey),
); err != nil {
return
}
s.remoteCtx, err = srtp.CreateContext(
remoteKey, remoteSalt, GuessProfile(remoteKey),
)
return
}
func (s *Session) HandleRTP(data []byte) (err error) {
if data, err = s.remoteCtx.DecryptRTP(nil, data, nil); err != nil {
return
}
packet := &rtp.Packet{}
if err = packet.Unmarshal(data); err != nil {
return
}
_ = s.Track.WriteRTP(packet)
//s.Output(core.RTP{Channel: s.Channel, Packet: packet})
return
}
func (s *Session) HandleRTCP(data []byte) (err error) {
header := &rtcp.Header{}
if data, err = s.remoteCtx.DecryptRTCP(nil, data, header); err != nil {
return
}
var packets []rtcp.Packet
if packets, err = rtcp.Unmarshal(data); err != nil {
return
}
_ = packets
//s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets})
if header.Type == rtcp.TypeSenderReport {
err = s.KeepAlive()
}
return
}
func (s *Session) KeepAlive() (err error) {
var data []byte
// we can send empty receiver response, but should send it to hold the connection
rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC}
if data, err = rep.Marshal(); err != nil {
return
}
if data, err = s.localCtx.EncryptRTCP(nil, data, nil); err != nil {
return
}
_, err = s.Write(data)
return
}
func GuessProfile(masterKey []byte) srtp.ProtectionProfile {
switch len(masterKey) {
case 16:
return srtp.ProtectionProfileAes128CmHmacSha1_80
case 32:
return srtp.ProtectionProfileAes256CmHmacSha1_80
}
return 0
}
+7 -3
View File
@@ -30,13 +30,14 @@ const (
CodecAAC = "MPEG4-GENERIC"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
CodecMPA = "MPA" // payload: 14
)
func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722:
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMPA:
return KindAudio
}
return ""
@@ -256,11 +257,14 @@ func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
if c.Name == "" {
switch payloadType {
case "0":
c.Name = "PCMU"
c.Name = CodecPCMU
c.ClockRate = 8000
case "8":
c.Name = "PCMA"
c.Name = CodecPCMA
c.ClockRate = 8000
case "14":
c.Name = CodecMPA
c.ClockRate = 44100
default:
c.Name = payloadType
}
+1
View File
@@ -50,4 +50,5 @@ pc.ontrack = ev => {
## Useful links
- https://www.webrtc-experiment.com/DetectRTC/
- https://divtable.com/table-styler/
+13 -20
View File
@@ -42,11 +42,8 @@
</style>
</head>
<body>
<div class="header">
<input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a>
</div>
<table id="devices">
<script src="main.js"></script>
<table>
<thead>
<tr>
<th>Kind</th>
@@ -61,22 +58,18 @@
0, location.pathname.lastIndexOf("/")
);
function reload() {
fetch(`${baseUrl}/api/devices`).then(r => {
r.json().then(data => {
let html = '';
data.forEach(function (item) {
html += `<tr><td>${item.kind}</td><td>${item.title}</td></tr>`;
})
let content = document.getElementById('devices').getElementsByTagName('tbody')[0];
content.innerHTML = html
});
fetch(`${baseUrl}/api/devices`)
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =
data.reduce((html, item) => {
return html + `<tr>
<td>${item.kind}</td>
<td>${item.title}</td>
</tr>`;
}, '');
})
}
reload();
.catch(console.error);
</script>
</body>
</html>
+111
View File
@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>go2rtc</title>
<style>
table {
background-color: white;
text-align: left;
border-collapse: collapse;
}
table td, table th {
border: 1px solid black;
padding: 5px 5px;
}
table tbody td {
font-size: 13px;
}
table thead {
background: #CFCFCF;
background: linear-gradient(to bottom, #dbdbdb 0%, #d3d3d3 66%, #CFCFCF 100%);
border-bottom: 3px solid black;
}
table thead th {
font-size: 15px;
font-weight: bold;
color: black;
text-align: center;
}
.header {
padding: 5px 5px;
}
</style>
</head>
<body>
<script src="main.js"></script>
<div class="header">
<label for="pin">PIN</label>
<input id="pin" type="text">
</div>
<table>
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Model</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
fetch(`${baseUrl}/api/homekit`)
.then(r => r.json())
.then(data => {
document.querySelector("body > table > tbody").innerHTML =
data.reduce((res, item) => {
let commands = '';
if (item.id === "") {
commands = `<a href="#" onclick="unpair('${item.name}')">unpair</a>`;
} else if (item.paired === false) {
commands = `<a href="#" onclick="pair('${item.id}','${item.name}')">pair</a>`;
}
return res + `<tr>
<td>${item.name}</td>
<td>${item.addr}</td>
<td>${item.model}</td>
<td>${commands}</td>
</tr>`;
}, '');
})
.catch(console.error);
function pair(id, name) {
const pin = document.querySelector("#pin").value;
fetch(`${baseUrl}/api/homekit?id=${id}&name=${name}&pin=${pin}`, {method: 'POST'})
.then(r => r.text())
.then(data => {
if (data.length > 0) alert(data);
else window.location.reload();
})
.catch(console.error);
}
function unpair(src) {
fetch(`${baseUrl}/api/homekit?src=${src}`, {method: 'DELETE'})
.then(r => r.text())
.then(data => {
if (data.length > 0) alert(data);
else window.location.reload();
})
.catch(console.error);
}
</script>
</body>
</html>
+1 -1
View File
@@ -42,10 +42,10 @@
</style>
</head>
<body>
<script src="main.js"></script>
<div class="header">
<input id="src" type="text" placeholder="url">
<a id="add" href="#">add</a>
<a href="devices.html">devices</a>
</div>
<table id="streams">
<thead>
+52
View File
@@ -0,0 +1,52 @@
// main menu
document.body.innerHTML = `
<style>
ul {
list-style: none;
margin: 0 auto;
}
a {
text-decoration: none;
font-family: 'Lora', serif;
transition: .5s linear;
}
i {
margin-right: 10px;
}
nav {
display: block;
/*width: 660px;*/
margin: 0 auto 10px;
}
nav ul {
padding: 1em 0;
background: #ECDAD6;
}
nav a {
padding: 1em;
background: rgba(177, 152, 145, .3);
border-right: 1px solid #b19891;
color: #695753;
}
nav a:hover {
background: #b19891;
}
nav li {
display: inline;
}
</style>
<nav>
<ul>
<li><a href="index.html">Streams</a></li>
<li><a href="devices.html">Devices</a></li>
<li><a href="homekit.html">HomeKit</a></li>
</ul>
</nav>
` + document.body.innerHTML;
+1
View File
@@ -3,4 +3,5 @@ package www
import "embed"
//go:embed *.html
//go:embed *.js
var Static embed.FS