Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5460e194e8 | |||
| e4f565f343 | |||
| 6b274f2a37 | |||
| f442aab176 | |||
| 0e71bd4dcb | |||
| e3618d70c3 | |||
| 99c4a3e34a | |||
| b78de349ab | |||
| b4990b1e90 | |||
| 687bdadba6 | |||
| c0c96cfcdb | |||
| 4f92608f33 | |||
| ac49bbef4d |
@@ -4,3 +4,5 @@
|
||||
.tmp/
|
||||
|
||||
go2rtc.yaml
|
||||
|
||||
go2rtc.json
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
- 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.
|
||||
|
||||

|
||||

|
||||
|
||||
**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
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+32
-21
@@ -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"
|
||||
@@ -26,7 +26,7 @@ func Init() {
|
||||
|
||||
app.LoadConfig(&conf)
|
||||
|
||||
log = app.GetLogger("api")
|
||||
log = app.GetLogger("hass")
|
||||
|
||||
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
||||
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -63,22 +63,23 @@ 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)
|
||||
log.Info().Str("url", "hass:" + entrie.Title).Msg("[hass] load stream")
|
||||
//streams.Get("hass:" + entrie.Title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +91,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 +106,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")
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -69,7 +69,7 @@ func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("src", src).Msg("[webrtc] new consumer")
|
||||
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
|
||||
|
||||
var err error
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+58
-38
@@ -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,81 @@ 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 len(units) > 0 {
|
||||
i := int(binary.BigEndian.Uint32(units)) + 4
|
||||
unit := units[:i] // NAL Unit with AVC header
|
||||
units = units[i:]
|
||||
|
||||
switch naluType {
|
||||
case NALUTypeSPS:
|
||||
//println("new SPS")
|
||||
sps = data
|
||||
return nil
|
||||
case NALUTypePPS:
|
||||
//println("new PPS")
|
||||
pps = data
|
||||
return nil
|
||||
}
|
||||
unitType := NALUType(unit)
|
||||
//fmt.Printf("[H264] type: %2d, size: %6d\n", unitType, i)
|
||||
switch unitType {
|
||||
case NALUTypeSPS:
|
||||
//println("new SPS")
|
||||
sps = unit
|
||||
continue
|
||||
case NALUTypePPS:
|
||||
//println("new PPS")
|
||||
pps = unit
|
||||
continue
|
||||
}
|
||||
|
||||
// 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, unit...)
|
||||
continue
|
||||
}
|
||||
|
||||
if buffer != nil {
|
||||
buffer = append(buffer, data...)
|
||||
data = buffer
|
||||
buffer = nil
|
||||
}
|
||||
if buffer != nil {
|
||||
buffer = append(buffer, unit...)
|
||||
unit = 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 = unit
|
||||
if err = push(&clone); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
clone = *packet
|
||||
clone.Version = RTPPacketVersionAVC
|
||||
clone.Payload = data
|
||||
return push(&clone)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -50,4 +50,5 @@ pc.ontrack = ev => {
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://www.webrtc-experiment.com/DetectRTC/
|
||||
- https://divtable.com/table-styler/
|
||||
+13
-20
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
@@ -3,4 +3,5 @@ package www
|
||||
import "embed"
|
||||
|
||||
//go:embed *.html
|
||||
//go:embed *.js
|
||||
var Static embed.FS
|
||||
|
||||
Reference in New Issue
Block a user