From 994e0dc526ab9cddd3685805df39c42143ae027c Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 21 Oct 2025 12:26:24 +0300 Subject: [PATCH 01/34] Improve homekit tlv8 parsing --- pkg/hap/client.go | 4 ++-- pkg/hap/client_pairing.go | 12 ++++++------ pkg/hap/server.go | 5 ++--- pkg/hap/server_pairing.go | 28 +++++++++++++++++++++------- pkg/hap/tlv8/tlv8.go | 13 +++++++++++-- pkg/homekit/server.go | 2 +- 6 files changed, 43 insertions(+), 21 deletions(-) diff --git a/pkg/hap/client.go b/pkg/hap/client.go index 2c1f7dd3..2801dd9f 100644 --- a/pkg/hap/client.go +++ b/pkg/hap/client.go @@ -124,7 +124,7 @@ func (c *Client) Dial() (err error) { EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM2); err != nil { return err } if cipherM2.State != StateM2 { @@ -209,7 +209,7 @@ func (c *Client) Dial() (err error) { var plainM4 struct { State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil { return } if plainM4.State != StateM4 { diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go index baec7be5..a58526d9 100644 --- a/pkg/hap/client_pairing.go +++ b/pkg/hap/client_pairing.go @@ -107,7 +107,7 @@ func (c *Client) Pair(feature, pin string) (err error) { State byte `tlv8:"6"` Error byte `tlv8:"7"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return } if plainM2.State != StateM2 { @@ -159,7 +159,7 @@ func (c *Client) Pair(feature, pin string) (err error) { EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices) } - if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil { return } if plainM4.State != StateM4 { @@ -232,7 +232,7 @@ func (c *Client) Pair(feature, pin string) (err error) { State byte `tlv8:"6"` Error byte `tlv8:"7"` }{} - if err = tlv8.UnmarshalReader(res.Body, &cipherM6); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM6); err != nil { return } if cipherM6.State != StateM6 || cipherM6.Error != 0 { @@ -296,7 +296,7 @@ func (c *Client) ListPairings() error { State byte `tlv8:"6"` Permission byte `tlv8:"11"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return err } @@ -329,7 +329,7 @@ func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) e State byte `tlv8:"6"` Unknown byte `tlv8:"7"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return err } @@ -354,7 +354,7 @@ func (c *Client) DeletePairing(id string) error { var plainM2 struct { State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return err } if plainM2.State != StateM2 { diff --git a/pkg/hap/server.go b/pkg/hap/server.go index 2a912324..a71ab7aa 100644 --- a/pkg/hap/server.go +++ b/pkg/hap/server.go @@ -6,7 +6,6 @@ import ( "encoding/base64" "errors" "fmt" - "io" "net" "net/http" @@ -55,7 +54,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co PublicKey string `tlv8:"3"` State byte `tlv8:"6"` } - if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil { + if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { return err } if plainM1.State != StateM1 { @@ -125,7 +124,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(req.Body, &cipherM3); err != nil { + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil { return err } if cipherM3.State != StateM3 { diff --git a/pkg/hap/server_pairing.go b/pkg/hap/server_pairing.go index 77895c10..571ba7a2 100644 --- a/pkg/hap/server_pairing.go +++ b/pkg/hap/server_pairing.go @@ -5,7 +5,6 @@ import ( "crypto/sha512" "errors" "fmt" - "io" "net" "net/http" @@ -25,18 +24,33 @@ const ( PairMethodList ) -func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - if req.Header.Get("Content-Type") != MimeTLV8 { - return errors.New("hap: wrong content type") +func (s *Server) HandleConn(conn net.Conn) error { + rd := bufio.NewReader(conn) + req, err := http.ReadRequest(rd) + if err != nil { + return err } + rw := bufio.NewReadWriter(rd, bufio.NewWriter(conn)) + + switch req.RequestURI { + case PathPairSetup: + return s.PairSetup(req, rw, conn) + case PathPairVerify: + return s.PairVerify(req, rw, conn) + } + + return errors.New("hap: unsupported request uri: " + req.RequestURI) +} + +func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { // STEP 1. Request from iPhone var plainM1 struct { Method byte `tlv8:"0"` State byte `tlv8:"6"` Flags uint32 `tlv8:"19"` } - if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil { + if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { return err } if plainM1.State != StateM1 { @@ -87,7 +101,7 @@ func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Con Proof string `tlv8:"4"` State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(req.Body, &plainM3); err != nil { + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil { return err } if plainM3.State != StateM3 { @@ -129,7 +143,7 @@ func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Con EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(req.Body, &cipherM5); err != nil { + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil { return err } if cipherM5.State != StateM5 { diff --git a/pkg/hap/tlv8/tlv8.go b/pkg/hap/tlv8/tlv8.go index 7af27ea4..6efe20a6 100644 --- a/pkg/hap/tlv8/tlv8.go +++ b/pkg/hap/tlv8/tlv8.go @@ -170,11 +170,20 @@ func UnmarshalBase64(in any, out any) error { return Unmarshal(data, out) } -func UnmarshalReader(r io.Reader, v any) error { - data, err := io.ReadAll(r) +func UnmarshalReader(r io.Reader, n int64, v any) error { + var data []byte + var err error + + if n > 0 { + data = make([]byte, n) + _, err = io.ReadFull(r, data) + } else { + data, err = io.ReadAll(r) + } if err != nil { return err } + return Unmarshal(data, v) } diff --git a/pkg/homekit/server.go b/pkg/homekit/server.go index 20cfc59d..2d00deab 100644 --- a/pkg/homekit/server.go +++ b/pkg/homekit/server.go @@ -139,7 +139,7 @@ func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Re Permissions byte `tlv8:"11"` }{} - if err := tlv8.UnmarshalReader(req.Body, &cmd); err != nil { + if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &cmd); err != nil { return nil, err } From ff18283d11c072075d5237256868b53f8b165e0d Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 21 Oct 2025 12:27:38 +0300 Subject: [PATCH 02/34] Improve homekit secure conn buffers --- pkg/hap/client.go | 4 +++- pkg/hap/secure/secure.go | 19 ++++++++----------- pkg/hap/server.go | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pkg/hap/client.go b/pkg/hap/client.go index 2801dd9f..bde85277 100644 --- a/pkg/hap/client.go +++ b/pkg/hap/client.go @@ -216,8 +216,10 @@ func (c *Client) Dial() (err error) { return newResponseError(cipherM3, plainM4) } + rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn)) + // like tls.Client wrapper over net.Conn - if c.Conn, err = secure.Client(c.Conn, sessionShared, true); err != nil { + if c.Conn, err = secure.Client(c.Conn, rw, sessionShared, true); err != nil { return } // new reader for new conn diff --git a/pkg/hap/secure/secure.go b/pkg/hap/secure/secure.go index 576ee127..a42c7dea 100644 --- a/pkg/hap/secure/secure.go +++ b/pkg/hap/secure/secure.go @@ -14,9 +14,7 @@ import ( type Conn struct { conn net.Conn - - rd *bufio.Reader - wr *bufio.Writer + rw *bufio.ReadWriter encryptKey []byte decryptKey []byte @@ -26,7 +24,7 @@ type Conn struct { SharedKey []byte } -func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { +func Client(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) { key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key") if err != nil { return nil, err @@ -39,8 +37,7 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { c := &Conn{ conn: conn, - rd: bufio.NewReaderSize(conn, 32*1024), - wr: bufio.NewWriterSize(conn, 32*1024), + rw: rw, SharedKey: sharedKey, } @@ -69,14 +66,14 @@ func (c *Conn) Read(b []byte) (n int, err error) { } verify := make([]byte, 2) // verify = plain message size - if _, err = io.ReadFull(c.rd, verify); err != nil { + if _, err = io.ReadFull(c.rw, verify); err != nil { return } n = int(binary.LittleEndian.Uint16(verify)) ciphertext := make([]byte, n+Overhead) - if _, err = io.ReadFull(c.rd, ciphertext); err != nil { + if _, err = io.ReadFull(c.rw, ciphertext); err != nil { return } @@ -100,7 +97,7 @@ func (c *Conn) Write(b []byte) (n int, err error) { } binary.LittleEndian.PutUint16(verify, uint16(size)) - if _, err = c.wr.Write(verify); err != nil { + if _, err = c.rw.Write(verify); err != nil { return } @@ -112,7 +109,7 @@ func (c *Conn) Write(b []byte) (n int, err error) { return } - if _, err = c.wr.Write(buf[:size+Overhead]); err != nil { + if _, err = c.rw.Write(buf[:size+Overhead]); err != nil { return } @@ -120,7 +117,7 @@ func (c *Conn) Write(b []byte) (n int, err error) { n += size } - err = c.wr.Flush() + err = c.rw.Flush() return } diff --git a/pkg/hap/server.go b/pkg/hap/server.go index a71ab7aa..99c86f6b 100644 --- a/pkg/hap/server.go +++ b/pkg/hap/server.go @@ -166,7 +166,7 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co return err } - if conn, err = secure.Client(conn, sessionShared, false); err != nil { + if conn, err = secure.Client(conn, rw, sessionShared, false); err != nil { return err } From e4359ac217da128f26a082bf46d20d7d7c289c6f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Nov 2025 21:24:47 +0300 Subject: [PATCH 03/34] Rename HomeKit structures according to specs --- internal/homekit/server.go | 4 +- pkg/hap/camera/README.md | 3 + pkg/hap/camera/accessory.go | 26 +++---- pkg/hap/camera/accessory_test.go | 78 +++++++++---------- pkg/hap/camera/ch114_supported_video.go | 20 ++--- pkg/hap/camera/ch115_supported_audio.go | 26 +++---- ...6_rtp_config.go => ch116_supported_rtp.go} | 6 +- pkg/hap/camera/ch117_selected_stream.go | 8 +- pkg/hap/camera/ch118_setup_endpoints.go | 33 ++++---- pkg/hap/camera/ch120_streaming_status.go | 2 +- pkg/hap/camera/ch130_data_stream_transport.go | 11 +++ pkg/hap/camera/ch131_data_stream.go | 4 +- pkg/hap/camera/ch205.go | 18 +++++ pkg/hap/camera/ch206.go | 20 +++++ pkg/hap/camera/ch207.go | 19 +++++ pkg/hap/camera/ch209.go | 9 +++ pkg/hap/camera/stream.go | 22 +++--- pkg/homekit/consumer.go | 20 ++--- pkg/homekit/helpers.go | 18 ++--- pkg/homekit/producer.go | 4 +- pkg/homekit/proxy.go | 4 +- 21 files changed, 221 insertions(+), 134 deletions(-) create mode 100644 pkg/hap/camera/README.md rename pkg/hap/camera/{ch116_rtp_config.go => ch116_supported_rtp.go} (60%) create mode 100644 pkg/hap/camera/ch130_data_stream_transport.go create mode 100644 pkg/hap/camera/ch205.go create mode 100644 pkg/hap/camera/ch206.go create mode 100644 pkg/hap/camera/ch207.go create mode 100644 pkg/hap/camera/ch209.go diff --git a/internal/homekit/server.go b/internal/homekit/server.go index 6c8b37ae..d4d81456 100644 --- a/internal/homekit/server.go +++ b/internal/homekit/server.go @@ -86,7 +86,7 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a switch char.Type { case camera.TypeSetupEndpoints: - var offer camera.SetupEndpoints + var offer camera.SetupEndpointsRequest if err := tlv8.UnmarshalBase64(value, &offer); err != nil { return } @@ -95,7 +95,7 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a s.consumer.SetOffer(&offer) case camera.TypeSelectedStreamConfiguration: - var conf camera.SelectedStreamConfig + var conf camera.SelectedStreamConfiguration if err := tlv8.UnmarshalBase64(value, &conf); err != nil { return } diff --git a/pkg/hap/camera/README.md b/pkg/hap/camera/README.md new file mode 100644 index 00000000..c6c6f236 --- /dev/null +++ b/pkg/hap/camera/README.md @@ -0,0 +1,3 @@ +## Useful links + +- https://github.com/bauer-andreas/secure-video-specification diff --git a/pkg/hap/camera/accessory.go b/pkg/hap/camera/accessory.go index 973983ec..37724497 100644 --- a/pkg/hap/camera/accessory.go +++ b/pkg/hap/camera/accessory.go @@ -49,17 +49,17 @@ func ServiceCameraRTPStreamManagement() *hap.Service { val120, _ := tlv8.MarshalBase64(StreamingStatus{ Status: StreamingStatusAvailable, }) - val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileMain}, Level: []byte{VideoCodecLevel31, VideoCodecLevel40}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, // important for iPhones {Width: 320, Height: 240, Framerate: 15}, // apple watch @@ -67,23 +67,23 @@ func ServiceCameraRTPStreamManagement() *hap.Service { }, }, }) - val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfig{ - Codecs: []AudioCodec{ + val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfiguration{ + Codecs: []AudioCodecConfiguration{ { CodecType: AudioCodecTypeOpus, - CodecParams: []AudioParams{ + CodecParams: []AudioCodecParameters{ { - Channels: 1, - Bitrate: AudioCodecBitrateVariable, - SampleRate: []byte{AudioCodecSampleRate16Khz}, + Channels: 1, + BitrateMode: AudioCodecBitrateVariable, + SampleRate: []byte{AudioCodecSampleRate16Khz}, }, }, }, }, - ComfortNoise: 0, + ComfortNoiseSupport: 0, }) - val116, _ := tlv8.MarshalBase64(SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, + val116, _ := tlv8.MarshalBase64(SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, }) service := &hap.Service{ diff --git a/pkg/hap/camera/accessory_test.go b/pkg/hap/camera/accessory_test.go index 3f5dcd71..53c99a49 100644 --- a/pkg/hap/camera/accessory_test.go +++ b/pkg/hap/camera/accessory_test.go @@ -63,19 +63,19 @@ func TestAqaraG3(t *testing.T) { { name: "114", value: "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - actual: &SupportedVideoStreamConfig{}, - expect: &SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + actual: &SupportedVideoStreamConfiguration{}, + expect: &SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileMain}, Level: []byte{VideoCodecLevel31, VideoCodecLevel40}, CVOEnabled: []byte{0}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, {Width: 640, Height: 360, Framerate: 30}, @@ -94,29 +94,29 @@ func TestAqaraG3(t *testing.T) { { name: "115", value: "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - actual: &SupportedAudioStreamConfig{}, - expect: &SupportedAudioStreamConfig{ - Codecs: []AudioCodec{ + actual: &SupportedAudioStreamConfiguration{}, + expect: &SupportedAudioStreamConfiguration{ + Codecs: []AudioCodecConfiguration{ { CodecType: AudioCodecTypeAACELD, - CodecParams: []AudioParams{ + CodecParams: []AudioCodecParameters{ { - Channels: 1, - Bitrate: AudioCodecBitrateVariable, - SampleRate: []byte{AudioCodecSampleRate16Khz}, + Channels: 1, + BitrateMode: AudioCodecBitrateVariable, + SampleRate: []byte{AudioCodecSampleRate16Khz}, }, }, }, }, - ComfortNoise: 0, + ComfortNoiseSupport: 0, }, }, { name: "116", value: "AgEAAAACAQEAAAIBAg==", - actual: &SupportedRTPConfig{}, - expect: &SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoNone}, + actual: &SupportedRTPConfiguration{}, + expect: &SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoDisabled}, }, }, } @@ -130,18 +130,18 @@ func TestHomebridge(t *testing.T) { { name: "114", value: "AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAkABAgK0AAMBHgAAAwsBAkABAgLwAAMBDwAAAwsBAkABAgLwAAMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAoAHAgI4BAMBHgAAAwsBAkAGAgKwBAMBHg==", - actual: &SupportedVideoStreamConfig{}, - expect: &SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + actual: &SupportedVideoStreamConfiguration{}, + expect: &SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileConstrainedBaseline, VideoCodecProfileMain, VideoCodecProfileHigh}, Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 320, Height: 180, Framerate: 30}, {Width: 320, Height: 240, Framerate: 15}, @@ -162,9 +162,9 @@ func TestHomebridge(t *testing.T) { { name: "116", value: "AgEA", - actual: &SupportedRTPConfig{}, - expect: &SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, + actual: &SupportedRTPConfiguration{}, + expect: &SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, }, }, } @@ -178,18 +178,18 @@ func TestScrypted(t *testing.T) { { name: "114", value: "AVIBAQACEwEBAQIBAAAAAgEBAAACAQIDAQADCwECAA8CAnAIAwEeAAADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECQAECAvAAAwEP", - actual: &SupportedVideoStreamConfig{}, - expect: &SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + actual: &SupportedVideoStreamConfiguration{}, + expect: &SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileMain}, Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 3840, Height: 2160, Framerate: 30}, {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, @@ -202,15 +202,15 @@ func TestScrypted(t *testing.T) { { name: "115", value: "AScBAQMCIgEBAQIBAAMBAAAAAwEAAAADAQEAAAMBAQAAAwECAAADAQICAQA=", - actual: &SupportedAudioStreamConfig{}, - expect: &SupportedAudioStreamConfig{ - Codecs: []AudioCodec{ + actual: &SupportedAudioStreamConfiguration{}, + expect: &SupportedAudioStreamConfiguration{ + Codecs: []AudioCodecConfiguration{ { CodecType: AudioCodecTypeOpus, - CodecParams: []AudioParams{ + CodecParams: []AudioCodecParameters{ { - Channels: 1, - Bitrate: AudioCodecBitrateVariable, + Channels: 1, + BitrateMode: AudioCodecBitrateVariable, SampleRate: []byte{ AudioCodecSampleRate8Khz, AudioCodecSampleRate8Khz, AudioCodecSampleRate16Khz, AudioCodecSampleRate16Khz, @@ -220,15 +220,15 @@ func TestScrypted(t *testing.T) { }, }, }, - ComfortNoise: 0, + ComfortNoiseSupport: 0, }, }, { name: "116", value: "AgEAAAACAQI=", - actual: &SupportedRTPConfig{}, - expect: &SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoNone}, + actual: &SupportedRTPConfiguration{}, + expect: &SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoDisabled}, }, }, } diff --git a/pkg/hap/camera/ch114_supported_video.go b/pkg/hap/camera/ch114_supported_video.go index 196f0286..ec70dc61 100644 --- a/pkg/hap/camera/ch114_supported_video.go +++ b/pkg/hap/camera/ch114_supported_video.go @@ -2,15 +2,15 @@ package camera const TypeSupportedVideoStreamConfiguration = "114" -type SupportedVideoStreamConfig struct { - Codecs []VideoCodec `tlv8:"1"` +type SupportedVideoStreamConfiguration struct { + Codecs []VideoCodecConfiguration `tlv8:"1"` } -type VideoCodec struct { - CodecType byte `tlv8:"1"` - CodecParams []VideoParams `tlv8:"2"` - VideoAttrs []VideoAttrs `tlv8:"3"` - RTPParams []RTPParams `tlv8:"4"` +type VideoCodecConfiguration struct { + CodecType byte `tlv8:"1"` + CodecParams []VideoCodecParameters `tlv8:"2"` + VideoAttrs []VideoCodecAttributes `tlv8:"3"` + RTPParams []RTPParams `tlv8:"4"` } //goland:noinspection ALL @@ -31,15 +31,15 @@ const ( VideoCodecCvoSuppported = 1 ) -type VideoParams struct { +type VideoCodecParameters struct { ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0 PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported - CVOID []byte `tlv8:"5"` // ??? + CVOID []byte `tlv8:"5"` // ID for CVO RTP extensio } -type VideoAttrs struct { +type VideoCodecAttributes struct { Width uint16 `tlv8:"1"` Height uint16 `tlv8:"2"` Framerate uint8 `tlv8:"3"` diff --git a/pkg/hap/camera/ch115_supported_audio.go b/pkg/hap/camera/ch115_supported_audio.go index efb0d881..f7ba9b44 100644 --- a/pkg/hap/camera/ch115_supported_audio.go +++ b/pkg/hap/camera/ch115_supported_audio.go @@ -2,9 +2,9 @@ package camera const TypeSupportedAudioStreamConfiguration = "115" -type SupportedAudioStreamConfig struct { - Codecs []AudioCodec `tlv8:"1"` - ComfortNoise byte `tlv8:"2"` +type SupportedAudioStreamConfiguration struct { + Codecs []AudioCodecConfiguration `tlv8:"1"` + ComfortNoiseSupport byte `tlv8:"2"` } //goland:noinspection ALL @@ -31,16 +31,16 @@ const ( RTPTimeAACLD24 = 40 // 24000/1000*40=960 ) -type AudioCodec struct { - CodecType byte `tlv8:"1"` - CodecParams []AudioParams `tlv8:"2"` - RTPParams []RTPParams `tlv8:"3"` - ComfortNoise []byte `tlv8:"4"` +type AudioCodecConfiguration struct { + CodecType byte `tlv8:"1"` + CodecParams []AudioCodecParameters `tlv8:"2"` + RTPParams []RTPParams `tlv8:"3"` + ComfortNoise []byte `tlv8:"4"` } -type AudioParams struct { - Channels uint8 `tlv8:"1"` - Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant - SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 - RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60 +type AudioCodecParameters struct { + Channels uint8 `tlv8:"1"` + BitrateMode byte `tlv8:"2"` // 0 - variable, 1 - constant + SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 + RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60 } diff --git a/pkg/hap/camera/ch116_rtp_config.go b/pkg/hap/camera/ch116_supported_rtp.go similarity index 60% rename from pkg/hap/camera/ch116_rtp_config.go rename to pkg/hap/camera/ch116_supported_rtp.go index fb4be550..f0ca0db9 100644 --- a/pkg/hap/camera/ch116_rtp_config.go +++ b/pkg/hap/camera/ch116_supported_rtp.go @@ -6,9 +6,9 @@ const TypeSupportedRTPConfiguration = "116" const ( CryptoAES_CM_128_HMAC_SHA1_80 = 0 CryptoAES_CM_256_HMAC_SHA1_80 = 1 - CryptoNone = 2 + CryptoDisabled = 2 ) -type SupportedRTPConfig struct { - CryptoType []byte `tlv8:"2"` +type SupportedRTPConfiguration struct { + SRTPCryptoType []byte `tlv8:"2"` } diff --git a/pkg/hap/camera/ch117_selected_stream.go b/pkg/hap/camera/ch117_selected_stream.go index aa0c7038..d94ba96b 100644 --- a/pkg/hap/camera/ch117_selected_stream.go +++ b/pkg/hap/camera/ch117_selected_stream.go @@ -2,10 +2,10 @@ package camera const TypeSelectedStreamConfiguration = "117" -type SelectedStreamConfig struct { - Control SessionControl `tlv8:"1"` - VideoCodec VideoCodec `tlv8:"2"` - AudioCodec AudioCodec `tlv8:"3"` +type SelectedStreamConfiguration struct { + Control SessionControl `tlv8:"1"` + VideoCodec VideoCodecConfiguration `tlv8:"2"` + AudioCodec AudioCodecConfiguration `tlv8:"3"` } //goland:noinspection ALL diff --git a/pkg/hap/camera/ch118_setup_endpoints.go b/pkg/hap/camera/ch118_setup_endpoints.go index 9405de4a..e0f426c0 100644 --- a/pkg/hap/camera/ch118_setup_endpoints.go +++ b/pkg/hap/camera/ch118_setup_endpoints.go @@ -2,25 +2,32 @@ package camera const TypeSetupEndpoints = "118" -type SetupEndpoints struct { - SessionID string `tlv8:"1"` - Status []byte `tlv8:"2"` - Address Addr `tlv8:"3"` - VideoCrypto CryptoSuite `tlv8:"4"` - AudioCrypto CryptoSuite `tlv8:"5"` - VideoSSRC []uint32 `tlv8:"6"` - AudioSSRC []uint32 `tlv8:"7"` +type SetupEndpointsRequest struct { + SessionID string `tlv8:"1"` + Address Address `tlv8:"3"` + VideoCrypto SRTPCryptoSuite `tlv8:"4"` + AudioCrypto SRTPCryptoSuite `tlv8:"5"` } -type Addr struct { +type SetupEndpointsResponse struct { + SessionID string `tlv8:"1"` + Status byte `tlv8:"2"` + Address Address `tlv8:"3"` + VideoCrypto SRTPCryptoSuite `tlv8:"4"` + AudioCrypto SRTPCryptoSuite `tlv8:"5"` + VideoSSRC uint32 `tlv8:"6"` + AudioSSRC uint32 `tlv8:"7"` +} + +type Address struct { IPVersion byte `tlv8:"1"` IPAddr string `tlv8:"2"` VideoRTPPort uint16 `tlv8:"3"` AudioRTPPort uint16 `tlv8:"4"` } -type CryptoSuite struct { - CryptoType byte `tlv8:"1"` - MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) - MasterSalt string `tlv8:"3"` // 14 byte +type SRTPCryptoSuite struct { + CryptoSuite byte `tlv8:"1"` + MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) + MasterSalt string `tlv8:"3"` // 14 byte } diff --git a/pkg/hap/camera/ch120_streaming_status.go b/pkg/hap/camera/ch120_streaming_status.go index 2fe53911..e617df27 100644 --- a/pkg/hap/camera/ch120_streaming_status.go +++ b/pkg/hap/camera/ch120_streaming_status.go @@ -9,6 +9,6 @@ type StreamingStatus struct { //goland:noinspection ALL const ( StreamingStatusAvailable = 0 - StreamingStatusBusy = 1 + StreamingStatusInUse = 1 StreamingStatusUnavailable = 2 ) diff --git a/pkg/hap/camera/ch130_data_stream_transport.go b/pkg/hap/camera/ch130_data_stream_transport.go new file mode 100644 index 00000000..808f822d --- /dev/null +++ b/pkg/hap/camera/ch130_data_stream_transport.go @@ -0,0 +1,11 @@ +package camera + +const TypeSupportedDataStreamTransportConfiguration = "130" + +type SupportedDataStreamTransportConfiguration struct { + Configs []TransferTransportConfiguration `tlv8:"1"` +} + +type TransferTransportConfiguration struct { + TransportType byte `tlv8:"1"` +} diff --git a/pkg/hap/camera/ch131_data_stream.go b/pkg/hap/camera/ch131_data_stream.go index 067b01b4..4f4ab49f 100644 --- a/pkg/hap/camera/ch131_data_stream.go +++ b/pkg/hap/camera/ch131_data_stream.go @@ -2,13 +2,13 @@ package camera const TypeSetupDataStreamTransport = "131" -type SetupDataStreamRequest struct { +type SetupDataStreamTransportRequest struct { SessionCommandType byte `tlv8:"1"` TransportType byte `tlv8:"2"` ControllerKeySalt string `tlv8:"3"` } -type SetupDataStreamResponse struct { +type SetupDataStreamTransportResponse struct { Status byte `tlv8:"1"` TransportTypeSessionParameters struct { TCPListeningPort uint16 `tlv8:"1"` diff --git a/pkg/hap/camera/ch205.go b/pkg/hap/camera/ch205.go new file mode 100644 index 00000000..431db7b0 --- /dev/null +++ b/pkg/hap/camera/ch205.go @@ -0,0 +1,18 @@ +package camera + +const TypeSupportedCameraRecordingConfiguration = "205" + +type SupportedCameraRecordingConfiguration struct { + PrebufferLength uint32 `tlv8:"1"` + EventTriggerOptions uint64 `tlv8:"2"` + MediaContainerConfigurations `tlv8:"3"` +} + +type MediaContainerConfigurations struct { + MediaContainerType uint8 `tlv8:"1"` + MediaContainerParameters `tlv8:"2"` +} + +type MediaContainerParameters struct { + FragmentLength uint32 `tlv8:"1"` +} diff --git a/pkg/hap/camera/ch206.go b/pkg/hap/camera/ch206.go new file mode 100644 index 00000000..89219fa7 --- /dev/null +++ b/pkg/hap/camera/ch206.go @@ -0,0 +1,20 @@ +package camera + +const TypeSupportedVideoRecordingConfiguration = "206" + +type SupportedVideoRecordingConfiguration struct { + CodecConfigs []VideoRecordingCodecConfiguration `tlv8:"1"` +} + +type VideoRecordingCodecConfiguration struct { + CodecType uint8 `tlv8:"1"` + CodecParams VideoRecordingCodecParameters `tlv8:"2"` + CodecAttrs VideoCodecAttributes `tlv8:"3"` +} + +type VideoRecordingCodecParameters struct { + ProfileID uint8 `tlv8:"1"` + Level uint8 `tlv8:"2"` + Bitrate uint32 `tlv8:"3"` + IFrameInterval uint32 `tlv8:"4"` +} diff --git a/pkg/hap/camera/ch207.go b/pkg/hap/camera/ch207.go new file mode 100644 index 00000000..5d389923 --- /dev/null +++ b/pkg/hap/camera/ch207.go @@ -0,0 +1,19 @@ +package camera + +const TypeSupportedAudioRecordingConfiguration = "207" + +type SupportedAudioRecordingConfiguration struct { + CodecConfigs []AudioRecordingCodecConfiguration `tlv8:"1"` +} + +type AudioRecordingCodecConfiguration struct { + CodecType byte `tlv8:"1"` + CodecParams []AudioRecordingCodecParameters `tlv8:"2"` +} + +type AudioRecordingCodecParameters struct { + Channels uint8 `tlv8:"1"` + BitrateMode []byte `tlv8:"2"` + SampleRate []byte `tlv8:"3"` + MaxAudioBitrate []uint32 `tlv8:"4"` +} diff --git a/pkg/hap/camera/ch209.go b/pkg/hap/camera/ch209.go new file mode 100644 index 00000000..c51359fb --- /dev/null +++ b/pkg/hap/camera/ch209.go @@ -0,0 +1,9 @@ +package camera + +const TypeSelectedCameraRecordingConfiguration = "209" + +type SelectedCameraRecordingConfiguration struct { + GeneralConfig SupportedCameraRecordingConfiguration `tlv8:"1"` + VideoConfig SupportedVideoRecordingConfiguration `tlv8:"2"` + AudioConfig SupportedAudioRecordingConfiguration `tlv8:"3"` +} diff --git a/pkg/hap/camera/stream.go b/pkg/hap/camera/stream.go index 23d53c39..bda67920 100644 --- a/pkg/hap/camera/stream.go +++ b/pkg/hap/camera/stream.go @@ -15,7 +15,7 @@ type Stream struct { } func NewStream( - client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, + client *hap.Client, videoCodec *VideoCodecConfiguration, audioCodec *AudioCodecConfiguration, videoSession, audioSession *srtp.Session, bitrate int, ) (*Stream, error) { stream := &Stream{ @@ -58,7 +58,7 @@ func NewStream( } audioCodec.ComfortNoise = []byte{0} - config := &SelectedStreamConfig{ + config := &SelectedStreamConfiguration{ Control: SessionControl{ SessionID: stream.id, Command: SessionCommandStart, @@ -103,19 +103,19 @@ func (s *Stream) GetFreeStream() error { } func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error { - req := SetupEndpoints{ + req := SetupEndpointsRequest{ SessionID: s.id, - Address: Addr{ + Address: Address{ IPVersion: 0, IPAddr: videoSession.Local.Addr, VideoRTPPort: videoSession.Local.Port, AudioRTPPort: audioSession.Local.Port, }, - VideoCrypto: CryptoSuite{ + VideoCrypto: SRTPCryptoSuite{ MasterKey: string(videoSession.Local.MasterKey), MasterSalt: string(videoSession.Local.MasterSalt), }, - AudioCrypto: CryptoSuite{ + AudioCrypto: SRTPCryptoSuite{ MasterKey: string(audioSession.Local.MasterKey), MasterSalt: string(audioSession.Local.MasterSalt), }, @@ -129,7 +129,7 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err return err } - var res SetupEndpoints + var res SetupEndpointsResponse if err := s.client.GetCharacter(char); err != nil { return err } @@ -142,7 +142,7 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err Port: res.Address.VideoRTPPort, MasterKey: []byte(res.VideoCrypto.MasterKey), MasterSalt: []byte(res.VideoCrypto.MasterSalt), - SSRC: res.VideoSSRC[0], + SSRC: res.VideoSSRC, } audioSession.Remote = &srtp.Endpoint{ @@ -150,13 +150,13 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err Port: res.Address.AudioRTPPort, MasterKey: []byte(res.AudioCrypto.MasterKey), MasterSalt: []byte(res.AudioCrypto.MasterSalt), - SSRC: res.AudioSSRC[0], + SSRC: res.AudioSSRC, } return nil } -func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error { +func (s *Stream) SetStreamConfig(config *SelectedStreamConfiguration) error { char := s.service.GetCharacter(TypeSelectedStreamConfiguration) if err := char.Write(config); err != nil { return err @@ -169,7 +169,7 @@ func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error { } func (s *Stream) Close() error { - config := &SelectedStreamConfig{ + config := &SelectedStreamConfiguration{ Control: SessionControl{ SessionID: s.id, Command: SessionCommandEnd, diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index ea83146f..05204218 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -59,7 +59,7 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { } } -func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) { +func (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) { c.sessionID = offer.SessionID c.videoSession = &srtp.Session{ Remote: &srtp.Endpoint{ @@ -79,32 +79,32 @@ func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) { } } -func (c *Consumer) GetAnswer() *camera.SetupEndpoints { +func (c *Consumer) GetAnswer() *camera.SetupEndpointsResponse { c.videoSession.Local = c.srtpEndpoint() c.audioSession.Local = c.srtpEndpoint() - return &camera.SetupEndpoints{ + return &camera.SetupEndpointsResponse{ SessionID: c.sessionID, - Status: []byte{0}, - Address: camera.Addr{ + Status: camera.StreamingStatusAvailable, + Address: camera.Address{ IPAddr: c.videoSession.Local.Addr, VideoRTPPort: c.videoSession.Local.Port, AudioRTPPort: c.audioSession.Local.Port, }, - VideoCrypto: camera.CryptoSuite{ + VideoCrypto: camera.SRTPCryptoSuite{ MasterKey: string(c.videoSession.Local.MasterKey), MasterSalt: string(c.videoSession.Local.MasterSalt), }, - AudioCrypto: camera.CryptoSuite{ + AudioCrypto: camera.SRTPCryptoSuite{ MasterKey: string(c.audioSession.Local.MasterKey), MasterSalt: string(c.audioSession.Local.MasterSalt), }, - VideoSSRC: []uint32{c.videoSession.Local.SSRC}, - AudioSSRC: []uint32{c.audioSession.Local.SSRC}, + VideoSSRC: c.videoSession.Local.SSRC, + AudioSSRC: c.audioSession.Local.SSRC, } } -func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool { +func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfiguration) bool { if c.sessionID != conf.Control.SessionID { return false } diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go index f5a17319..2a2268d6 100644 --- a/pkg/homekit/helpers.go +++ b/pkg/homekit/helpers.go @@ -13,7 +13,7 @@ var videoCodecs = [...]string{core.CodecH264} var videoProfiles = [...]string{"4200", "4D00", "6400"} var videoLevels = [...]string{"1F", "20", "28"} -func videoToMedia(codecs []camera.VideoCodec) *core.Media { +func videoToMedia(codecs []camera.VideoCodecConfiguration) *core.Media { media := &core.Media{ Kind: core.KindVideo, Direction: core.DirectionRecvonly, } @@ -39,7 +39,7 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media { var audioCodecs = [...]string{core.CodecPCMU, core.CodecPCMA, core.CodecELD, core.CodecOpus} var audioSampleRates = [...]uint32{8000, 16000, 24000} -func audioToMedia(codecs []camera.AudioCodec) *core.Media { +func audioToMedia(codecs []camera.AudioCodecConfiguration) *core.Media { media := &core.Media{ Kind: core.KindAudio, Direction: core.DirectionRecvonly, } @@ -67,7 +67,7 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media { return media } -func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec { +func trackToVideo(track *core.Receiver, video0 *camera.VideoCodecConfiguration) *camera.VideoCodecConfiguration { profileID := video0.CodecParams[0].ProfileID[0] level := video0.CodecParams[0].Level[0] attrs := video0.VideoAttrs[0] @@ -96,19 +96,19 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video } } - return &camera.VideoCodec{ + return &camera.VideoCodecConfiguration{ CodecType: video0.CodecType, - CodecParams: []camera.VideoParams{ + CodecParams: []camera.VideoCodecParameters{ { ProfileID: []byte{profileID}, Level: []byte{level}, }, }, - VideoAttrs: []camera.VideoAttrs{attrs}, + VideoAttrs: []camera.VideoCodecAttributes{attrs}, } } -func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.AudioCodec { +func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodecConfiguration) *camera.AudioCodecConfiguration { codecType := audio0.CodecType channels := audio0.CodecParams[0].Channels sampleRate := audio0.CodecParams[0].SampleRate[0] @@ -131,9 +131,9 @@ func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.Audio } } - return &camera.AudioCodec{ + return &camera.AudioCodecConfiguration{ CodecType: codecType, - CodecParams: []camera.AudioParams{ + CodecParams: []camera.AudioCodecParameters{ { Channels: channels, SampleRate: []byte{sampleRate}, diff --git a/pkg/homekit/producer.go b/pkg/homekit/producer.go index 451b9882..3351a736 100644 --- a/pkg/homekit/producer.go +++ b/pkg/homekit/producer.go @@ -22,8 +22,8 @@ type Client struct { hap *hap.Client srtp *srtp.Server - videoConfig camera.SupportedVideoStreamConfig - audioConfig camera.SupportedAudioStreamConfig + videoConfig camera.SupportedVideoStreamConfiguration + audioConfig camera.SupportedAudioStreamConfiguration videoSession *srtp.Session audioSession *srtp.Session diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go index be233042..0e378b49 100644 --- a/pkg/homekit/proxy.go +++ b/pkg/homekit/proxy.go @@ -74,7 +74,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { _ = json.Unmarshal(body, &v) for _, char := range v.Value { if char.IID == hdsCharIID { - var hdsReq camera.SetupDataStreamRequest + var hdsReq camera.SetupDataStreamTransportRequest _ = tlv8.UnmarshalBase64(char.Value, &hdsReq) hdsConSalt = hdsReq.ControllerKeySalt break @@ -110,7 +110,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { _ = json.Unmarshal(body, &v) for i, char := range v.Value { if char.IID == hdsCharIID { - var hdsRes camera.SetupDataStreamResponse + var hdsRes camera.SetupDataStreamTransportResponse _ = tlv8.UnmarshalBase64(char.Value, &hdsRes) hdsAccSalt := hdsRes.AccessoryKeySalt From 96919bf9e3527850624ff7921842535b97a3fcfd Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Nov 2025 21:25:23 +0300 Subject: [PATCH 04/34] Add support uint64 to tlv8 --- pkg/hap/tlv8/tlv8.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/hap/tlv8/tlv8.go b/pkg/hap/tlv8/tlv8.go index 6efe20a6..7b397b99 100644 --- a/pkg/hap/tlv8/tlv8.go +++ b/pkg/hap/tlv8/tlv8.go @@ -112,6 +112,10 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) { v := value.Uint() return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil + case reflect.Uint64: + v := value.Uint() + return binary.LittleEndian.AppendUint64(append(b, tag, 8), v), nil + case reflect.Float32: v := math.Float32bits(float32(value.Float())) return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil @@ -310,6 +314,12 @@ func unmarshalValue(v []byte, value reflect.Value) error { } value.SetUint(uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24) + case reflect.Uint64: + if len(v) != 8 { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + value.SetUint(binary.LittleEndian.Uint64(v)) + case reflect.Float32: f := math.Float32frombits(binary.LittleEndian.Uint32(v)) value.SetFloat(float64(f)) From 81cfcf877a5e74044bb30bcec71f97541cb2dd18 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Nov 2025 21:28:53 +0300 Subject: [PATCH 05/34] Fix HomeKit proxy EVENTs --- pkg/hap/client_http.go | 17 +++++++++++++++++ pkg/homekit/proxy.go | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/hap/client_http.go b/pkg/hap/client_http.go index 360f48bc..7f8314f8 100644 --- a/pkg/hap/client_http.go +++ b/pkg/hap/client_http.go @@ -82,3 +82,20 @@ func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) { return res, nil } + +func WriteEvent(w io.Writer, res *http.Response) error { + return res.Write(&eventWriter{w: w}) +} + +type eventWriter struct { + w io.Writer + done bool +} + +func (e *eventWriter) Write(p []byte) (n int, err error) { + if !e.done { + p = append([]byte("EVENT/1.0"), p[8:]...) + e.done = true + } + return e.w.Write(p) +} diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go index 0e378b49..ac2f14d7 100644 --- a/pkg/homekit/proxy.go +++ b/pkg/homekit/proxy.go @@ -149,7 +149,7 @@ func (p *Proxy) handleAcc() error { } if res.Proto == hap.ProtoEvent { - if err = res.Write(p.con); err != nil { + if err = hap.WriteEvent(p.con, res); err != nil { return err } continue From 158f9d3a08ebb17f96bc47f28cfb27dc56a26807 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Nov 2025 21:58:44 +0300 Subject: [PATCH 06/34] Code refactoring for HomeKit server --- internal/homekit/api.go | 105 +++++---- internal/homekit/homekit.go | 76 ++----- internal/homekit/server.go | 299 ++++++++++++++++-------- pkg/hap/client.go | 10 +- pkg/hap/client_pairing.go | 5 +- pkg/hap/{secure/secure.go => conn.go} | 47 +++- pkg/hap/hds/hds.go | 33 ++- pkg/hap/server.go | 314 ++++++++++++++++++++++---- pkg/hap/server_pairing.go | 266 ---------------------- pkg/homekit/consumer.go | 6 +- pkg/homekit/log/debug.go | 45 ++++ pkg/homekit/producer.go | 16 +- pkg/homekit/proxy.go | 54 +++-- pkg/homekit/server.go | 57 +---- www/add.html | 15 +- 15 files changed, 742 insertions(+), 606 deletions(-) rename pkg/hap/{secure/secure.go => conn.go} (74%) delete mode 100644 pkg/hap/server_pairing.go create mode 100644 pkg/homekit/log/debug.go diff --git a/internal/homekit/api.go b/internal/homekit/api.go index 9f76c2d6..5d2b38d2 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -3,6 +3,7 @@ package homekit import ( "errors" "fmt" + "io" "net/http" "net/url" "strings" @@ -14,56 +15,84 @@ import ( "github.com/AlexxIT/go2rtc/pkg/mdns" ) -func apiHandler(w http.ResponseWriter, r *http.Request) { +func apiDiscovery(w http.ResponseWriter, r *http.Request) { + sources, err := discovery() + if err != nil { + api.Error(w, err) + return + } + + urls := findHomeKitURLs() + for id, u := range urls { + deviceID := u.Query().Get("device_id") + for _, source := range sources { + if strings.Contains(source.URL, deviceID) { + source.Location = id + break + } + } + } + + for _, source := range sources { + if source.Location == "" { + source.Location = " " + } + } + + api.ResponseSources(w, sources) +} + +func apiHomekit(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch r.Method { case "GET": - sources, err := discovery() - if err != nil { - api.Error(w, err) - return + if id := r.Form.Get("id"); id != "" { + api.ResponsePrettyJSON(w, servers[id]) + } else { + api.ResponsePrettyJSON(w, servers) } - urls := findHomeKitURLs() - for id, u := range urls { - deviceID := u.Query().Get("device_id") - for _, source := range sources { - if strings.Contains(source.URL, deviceID) { - source.Location = id - break - } - } - } - - for _, source := range sources { - if source.Location == "" { - source.Location = " " - } - } - - api.ResponseSources(w, sources) - case "POST": - if err := r.ParseMultipartForm(1024); err != nil { - api.Error(w, err) - return - } - - if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil { - api.Error(w, err) + id := r.Form.Get("id") + rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin") + if err := apiPair(id, rawURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } case "DELETE": - if err := r.ParseMultipartForm(1024); err != nil { - api.Error(w, err) - return - } - - if err := apiUnpair(r.Form.Get("id")); err != nil { - api.Error(w, err) + id := r.Form.Get("id") + if err := apiUnpair(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } } } +func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) { + src := r.URL.Query().Get("src") + stream := streams.Get(src) + rawURL := findHomeKitURL(stream.Sources()) + + client, err := hap.Dial(rawURL) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer client.Close() + + res, err := client.Get(hap.PathAccessories) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", api.MimeJSON) + _, _ = io.Copy(w, res.Body) +} + func discovery() ([]*api.Source, error) { var sources []*api.Source diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index b4237211..a6b02bf2 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -2,8 +2,6 @@ package homekit import ( "errors" - "io" - "net" "net/http" "strings" @@ -35,12 +33,15 @@ func Init() { streams.HandleFunc("homekit", streamHandler) - api.HandleFunc("api/homekit", apiHandler) + api.HandleFunc("api/homekit", apiHomekit) + api.HandleFunc("api/homekit/accessories", apiHomekitAccessories) + api.HandleFunc("api/discovery/homekit", apiDiscovery) if cfg.Mod == nil { return } + hosts = map[string]*server{} servers = map[string]*server{} var entries []*mdns.ServiceEntry @@ -66,33 +67,14 @@ func Init() { srv := &server{ stream: id, - srtp: srtp.Server, pairings: conf.Pairings, } srv.hap = &hap.Server{ - Pin: pin, - DeviceID: deviceID, - DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id), - GetPair: srv.GetPair, - AddPair: srv.AddPair, - Handler: homekit.ServerHandler(srv), - } - - if url := findHomeKitURL(stream.Sources()); url != "" { - // 1. Act as transparent proxy for HomeKit camera - dial := func() (net.Conn, error) { - client, err := homekit.Dial(url, srtp.Server) - if err != nil { - return nil, err - } - return client.Conn(), nil - } - srv.hap.Handler = homekit.ProxyHandler(srv, dial) - } else { - // 2. Act as basic HomeKit camera - srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version) - srv.hap.Handler = homekit.ServerHandler(srv) + Pin: pin, + DeviceID: deviceID, + DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id), + GetClientPublic: srv.GetPair, } srv.mdns = &mdns.ServiceEntry{ @@ -114,15 +96,24 @@ func Init() { srv.UpdateStatus() + if url := findHomeKitURL(stream.Sources()); url != "" { + // 1. Act as transparent proxy for HomeKit camera + srv.proxyURL = url + } else { + // 2. Act as basic HomeKit camera + srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version) + } + host := srv.mdns.Host(mdns.ServiceHAP) - servers[host] = srv + hosts[host] = srv + servers[id] = srv + + log.Trace().Msgf("[homekit] new server: %s", srv.mdns) } api.HandleFunc(hap.PathPairSetup, hapHandler) api.HandleFunc(hap.PathPairVerify, hapHandler) - log.Trace().Msgf("[homekit] mdns: %s", entries) - go func() { if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil { log.Error().Err(err).Caller().Send() @@ -131,6 +122,7 @@ func Init() { } var log zerolog.Logger +var hosts map[string]*server var servers map[string]*server func streamHandler(rawURL string) (core.Producer, error) { @@ -149,45 +141,27 @@ func streamHandler(rawURL string) (core.Producer, error) { } func resolve(host string) *server { - if len(servers) == 1 { - for _, srv := range servers { + if len(hosts) == 1 { + for _, srv := range hosts { return srv } } - if srv, ok := servers[host]; ok { + if srv, ok := hosts[host]; ok { return srv } return nil } func hapHandler(w http.ResponseWriter, r *http.Request) { - conn, rw, err := w.(http.Hijacker).Hijack() - if err != nil { - return - } - - defer conn.Close() - // Can support multiple HomeKit cameras on single port ONLY for Apple devices. // Doesn't support Home Assistant and any other open source projects // because they don't send the host header in requests. srv := resolve(r.Host) if srv == nil { log.Error().Msg("[homekit] unknown host: " + r.Host) - _ = hap.WriteBackoff(rw) return } - - switch r.RequestURI { - case hap.PathPairSetup: - err = srv.hap.PairSetup(r, rw, conn) - case hap.PathPairVerify: - err = srv.hap.PairVerify(r, rw, conn) - } - - if err != nil && err != io.EOF { - log.Error().Err(err).Caller().Send() - } + srv.Handle(w, r) } func findHomeKitURL(sources []string) string { diff --git a/internal/homekit/server.go b/internal/homekit/server.go index d4d81456..57e97287 100644 --- a/internal/homekit/server.go +++ b/internal/homekit/server.go @@ -4,10 +4,16 @@ import ( "crypto/ed25519" "crypto/sha512" "encoding/hex" + "encoding/json" + "errors" "fmt" + "io" "net" + "net/http" "net/url" + "slices" "strings" + "sync" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/ffmpeg" @@ -16,23 +22,133 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/camera" + "github.com/AlexxIT/go2rtc/pkg/hap/hds" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" "github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mdns" - "github.com/AlexxIT/go2rtc/pkg/srtp" ) type server struct { - stream string // stream name from YAML - hap *hap.Server // server for HAP connection and encryption - mdns *mdns.ServiceEntry - srtp *srtp.Server - accessory *hap.Accessory // HAP accessory - pairings []string // pairings list + hap *hap.Server // server for HAP connection and encryption + mdns *mdns.ServiceEntry - streams map[string]*homekit.Consumer - consumer *homekit.Consumer + pairings []string // pairings list + conns []any + mu sync.Mutex + + accessory *hap.Accessory // HAP accessory + consumer *homekit.Consumer + proxyURL string + stream string // stream name from YAML +} + +func (s *server) MarshalJSON() ([]byte, error) { + v := struct { + Name string `json:"name"` + DeviceID string `json:"device_id"` + Paired int `json:"paired"` + Conns []any `json:"connections"` + }{ + Name: s.mdns.Name, + DeviceID: s.mdns.Info[hap.TXTDeviceID], + Paired: len(s.pairings), + Conns: s.conns, + } + return json.Marshal(v) +} + +func (s *server) Handle(w http.ResponseWriter, r *http.Request) { + conn, rw, err := w.(http.Hijacker).Hijack() + if err != nil { + return + } + + defer conn.Close() + + // Fix reading from Body after Hijack. + r.Body = io.NopCloser(rw) + + switch r.RequestURI { + case hap.PathPairSetup: + id, key, err := s.hap.PairSetup(r, rw) + if err != nil { + log.Error().Err(err).Caller().Send() + return + } + + s.AddPair(id, key, hap.PermissionAdmin) + + case hap.PathPairVerify: + id, key, err := s.hap.PairVerify(r, rw) + if err != nil { + log.Debug().Err(err).Caller().Send() + return + } + + log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr()) + + controller, err := hap.NewConn(conn, rw, key, false) + if err != nil { + log.Error().Err(err).Caller().Send() + return + } + + s.AddConn(controller) + defer s.DelConn(controller) + + var handler homekit.HandlerFunc + + switch { + case s.accessory != nil: + handler = homekit.ServerHandler(s) + case s.proxyURL != "": + client, err := hap.Dial(s.proxyURL) + if err != nil { + log.Error().Err(err).Caller().Send() + return + } + handler = homekit.ProxyHandler(s, client.Conn) + } + + // If your iPhone goes to sleep, it will be an EOF error. + if err = handler(controller); err != nil && !errors.Is(err, io.EOF) { + log.Error().Err(err).Caller().Send() + return + } + } +} + +type logger struct { + v any +} + +func (l logger) String() string { + switch v := l.v.(type) { + case *hap.Conn: + return "hap " + v.RemoteAddr().String() + case *hds.Conn: + return "hds " + v.RemoteAddr().String() + case *homekit.Consumer: + return "rtp " + v.RemoteAddr + } + return "unknown" +} + +func (s *server) AddConn(v any) { + log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v}) + s.mu.Lock() + s.conns = append(s.conns, v) + s.mu.Unlock() +} + +func (s *server) DelConn(v any) { + log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v}) + s.mu.Lock() + if i := slices.Index(s.conns, v); i >= 0 { + s.conns = slices.Delete(s.conns, i, i+1) + } + s.mu.Unlock() } func (s *server) UpdateStatus() { @@ -44,12 +160,68 @@ func (s *server) UpdateStatus() { } } +func (s *server) pairIndex(id string) int { + id = "client_id=" + id + for i, pairing := range s.pairings { + if strings.HasPrefix(pairing, id) { + return i + } + } + return -1 +} + +func (s *server) GetPair(id string) []byte { + s.mu.Lock() + defer s.mu.Unlock() + + if i := s.pairIndex(id); i >= 0 { + query, _ := url.ParseQuery(s.pairings[i]) + b, _ := hex.DecodeString(query.Get("client_public")) + return b + } + return nil +} + +func (s *server) AddPair(id string, public []byte, permissions byte) { + log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions) + + s.mu.Lock() + if s.pairIndex(id) < 0 { + s.pairings = append(s.pairings, fmt.Sprintf( + "client_id=%s&client_public=%x&permissions=%d", id, public, permissions, + )) + s.UpdateStatus() + s.PatchConfig() + } + s.mu.Unlock() +} + +func (s *server) DelPair(id string) { + log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id) + + s.mu.Lock() + if i := s.pairIndex(id); i >= 0 { + s.pairings = append(s.pairings[:i], s.pairings[i+1:]...) + s.UpdateStatus() + s.PatchConfig() + } + s.mu.Unlock() +} + +func (s *server) PatchConfig() { + if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil { + log.Error().Err(err).Msgf( + "[homekit] can't save %s pairings=%v", s.stream, s.pairings, + ) + } +} + func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory { return []*hap.Accessory{s.accessory} } func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { - log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid) + log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid) char := s.accessory.GetCharacterByID(iid) if char == nil { @@ -59,11 +231,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { switch char.Type { case camera.TypeSetupEndpoints: - if s.consumer == nil { + consumer := s.consumer + if consumer == nil { return nil } - answer := s.consumer.GetAnswer() + answer := consumer.GetAnswer() v, err := tlv8.MarshalBase64(answer) if err != nil { return nil @@ -76,7 +249,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { } func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) { - log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value) + log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value) char := s.accessory.GetCharacterByID(iid) if char == nil { @@ -91,8 +264,9 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a return } - s.consumer = homekit.NewConsumer(conn, srtp2.Server) - s.consumer.SetOffer(&offer) + consumer := homekit.NewConsumer(conn, srtp2.Server) + consumer.SetOffer(&offer) + s.consumer = consumer case camera.TypeSelectedStreamConfiguration: var conf camera.SelectedStreamConfiguration @@ -100,47 +274,49 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a return } - log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command) + log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command) switch conf.Control.Command { case camera.SessionCommandEnd: - if consumer := s.streams[conf.Control.SessionID]; consumer != nil { - _ = consumer.Stop() + for _, consumer := range s.conns { + if consumer, ok := consumer.(*homekit.Consumer); ok { + if consumer.SessionID() == conf.Control.SessionID { + _ = consumer.Stop() + return + } + } } case camera.SessionCommandStart: - if s.consumer == nil { + consumer := s.consumer + if consumer == nil { return } - if !s.consumer.SetConfig(&conf) { + if !consumer.SetConfig(&conf) { log.Warn().Msgf("[homekit] wrong config") return } - if s.streams == nil { - s.streams = map[string]*homekit.Consumer{} - } - - s.streams[conf.Control.SessionID] = s.consumer + s.AddConn(consumer) stream := streams.Get(s.stream) - if err := stream.AddConsumer(s.consumer); err != nil { + if err := stream.AddConsumer(consumer); err != nil { return } go func() { - _, _ = s.consumer.WriteTo(nil) - stream.RemoveConsumer(s.consumer) + _, _ = consumer.WriteTo(nil) + stream.RemoveConsumer(consumer) - delete(s.streams, conf.Control.SessionID) + s.DelConn(consumer) }() } } } func (s *server) GetImage(conn net.Conn, width, height int) []byte { - log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height) + log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height) stream := streams.Get(s.stream) cons := magic.NewKeyframe() @@ -166,69 +342,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte { return b } -func (s *server) GetPair(conn net.Conn, id string) []byte { - log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id) - - for _, pairing := range s.pairings { - if !strings.Contains(pairing, id) { - continue - } - - query, err := url.ParseQuery(pairing) - if err != nil { - continue - } - - if query.Get("client_id") != id { - continue - } - - s := query.Get("client_public") - b, _ := hex.DecodeString(s) - return b - } - return nil -} - -func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) { - log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions) - - query := url.Values{ - "client_id": []string{id}, - "client_public": []string{hex.EncodeToString(public)}, - "permissions": []string{string('0' + permissions)}, - } - if s.GetPair(conn, id) == nil { - s.pairings = append(s.pairings, query.Encode()) - s.UpdateStatus() - s.PatchConfig() - } -} - -func (s *server) DelPair(conn net.Conn, id string) { - log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id) - - id = "client_id=" + id - for i, pairing := range s.pairings { - if !strings.Contains(pairing, id) { - continue - } - - s.pairings = append(s.pairings[:i], s.pairings[i+1:]...) - s.UpdateStatus() - s.PatchConfig() - break - } -} - -func (s *server) PatchConfig() { - if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil { - log.Error().Err(err).Msgf( - "[homekit] can't save %s pairings=%v", s.stream, s.pairings, - ) - } -} - func calcName(name, seed string) string { if name != "" { return name diff --git a/pkg/hap/client.go b/pkg/hap/client.go index bde85277..ed4faa02 100644 --- a/pkg/hap/client.go +++ b/pkg/hap/client.go @@ -18,7 +18,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/hap/curve25519" "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" "github.com/AlexxIT/go2rtc/pkg/mdns" ) @@ -46,7 +45,7 @@ type Client struct { err error } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -61,6 +60,10 @@ func NewClient(rawURL string) (*Client, error) { ClientPrivate: DecodeKey(query.Get("client_private")), } + if err = c.Dial(); err != nil { + return nil, err + } + return c, nil } @@ -96,6 +99,7 @@ func (c *Client) Dial() (err error) { return false }) + // TODO: close conn on error if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil { return } @@ -219,7 +223,7 @@ func (c *Client) Dial() (err error) { rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn)) // like tls.Client wrapper over net.Conn - if c.Conn, err = secure.Client(c.Conn, rw, sessionShared, true); err != nil { + if c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil { return } // new reader for new conn diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go index a58526d9..f253783d 100644 --- a/pkg/hap/client_pairing.go +++ b/pkg/hap/client_pairing.go @@ -121,9 +121,7 @@ func (c *Client) Pair(feature, pin string) (err error) { username := []byte("Pair-Setup") // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) - pake, err := srp.NewSRP( - "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), - ) + pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username)) if err != nil { return } @@ -132,6 +130,7 @@ func (c *Client) Pair(feature, pin string) (err error) { // username: "Pair-Setup", password: PIN (with dashes) session := pake.NewClientSession(username, []byte(pin)) + sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey)) if err != nil { return diff --git a/pkg/hap/secure/secure.go b/pkg/hap/conn.go similarity index 74% rename from pkg/hap/secure/secure.go rename to pkg/hap/conn.go index a42c7dea..2b039dc8 100644 --- a/pkg/hap/secure/secure.go +++ b/pkg/hap/conn.go @@ -1,13 +1,16 @@ -package secure +package hap import ( "bufio" "encoding/binary" + "encoding/json" "errors" "io" "net" + "sync" "time" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" ) @@ -15,16 +18,33 @@ import ( type Conn struct { conn net.Conn rw *bufio.ReadWriter + wmu sync.Mutex encryptKey []byte decryptKey []byte encryptCnt uint64 decryptCnt uint64 + //ClientID string SharedKey []byte + + recv int + send int } -func Client(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) { +func (c *Conn) MarshalJSON() ([]byte, error) { + conn := core.Connection{ + ID: core.ID(c), + FormatName: "homekit", + Protocol: "hap", + RemoteAddr: c.conn.RemoteAddr().String(), + Recv: c.recv, + Send: c.send, + } + return json.Marshal(conn) +} + +func NewConn(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) { key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key") if err != nil { return nil, err @@ -52,8 +72,8 @@ func Client(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool } const ( - // PacketSizeMax is the max length of encrypted packets - PacketSizeMax = 0x400 + // packetSizeMax is the max length of encrypted packets + packetSizeMax = 0x400 VerifySize = 2 NonceSize = 8 @@ -61,18 +81,18 @@ const ( ) func (c *Conn) Read(b []byte) (n int, err error) { - if cap(b) < PacketSizeMax { + if cap(b) < packetSizeMax { return 0, errors.New("hap: read buffer is too small") } - verify := make([]byte, 2) // verify = plain message size + verify := make([]byte, VerifySize) // verify = plain message size if _, err = io.ReadFull(c.rw, verify); err != nil { return } n = int(binary.LittleEndian.Uint16(verify)) - ciphertext := make([]byte, n+Overhead) + ciphertext := make([]byte, n+Overhead) if _, err = io.ReadFull(c.rw, ciphertext); err != nil { return } @@ -82,18 +102,23 @@ func (c *Conn) Read(b []byte) (n int, err error) { c.decryptCnt++ _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify) + + c.recv += n return } func (c *Conn) Write(b []byte) (n int, err error) { - buf := make([]byte, 0, PacketSizeMax+Overhead) + c.wmu.Lock() + defer c.wmu.Unlock() + + buf := make([]byte, 0, packetSizeMax+Overhead) nonce := make([]byte, NonceSize) verify := make([]byte, VerifySize) for len(b) > 0 { size := len(b) - if size > PacketSizeMax { - size = PacketSizeMax + if size > packetSizeMax { + size = packetSizeMax } binary.LittleEndian.PutUint16(verify, uint16(size)) @@ -118,6 +143,8 @@ func (c *Conn) Write(b []byte) (n int, err error) { } err = c.rw.Flush() + + c.send += n return } diff --git a/pkg/hap/hds/hds.go b/pkg/hap/hds/hds.go index a7b2c74a..60ee05d2 100644 --- a/pkg/hap/hds/hds.go +++ b/pkg/hap/hds/hds.go @@ -4,16 +4,18 @@ package hds import ( "bufio" "encoding/binary" + "encoding/json" "io" "net" "time" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" ) -func Client(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { +func NewConn(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key") if err != nil { return nil, err @@ -49,6 +51,21 @@ type Conn struct { encryptKey []byte decryptCnt uint64 encryptCnt uint64 + + recv int + send int +} + +func (c *Conn) MarshalJSON() ([]byte, error) { + conn := core.Connection{ + ID: core.ID(c), + FormatName: "homekit", + Protocol: "hds", + RemoteAddr: c.conn.RemoteAddr().String(), + Recv: c.recv, + Send: c.send, + } + return json.Marshal(conn) } func (c *Conn) Read(p []byte) (n int, err error) { @@ -59,16 +76,18 @@ func (c *Conn) Read(p []byte) (n int, err error) { n = int(binary.BigEndian.Uint32(verify) & 0xFFFFFF) - ciphertext := make([]byte, n+secure.Overhead) + ciphertext := make([]byte, n+hap.Overhead) if _, err = io.ReadFull(c.rd, ciphertext); err != nil { return } - nonce := make([]byte, secure.NonceSize) + nonce := make([]byte, hap.NonceSize) binary.LittleEndian.PutUint64(nonce, c.decryptCnt) c.decryptCnt++ _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, p[:0], nonce, ciphertext, verify) + + c.recv += n return } @@ -81,11 +100,11 @@ func (c *Conn) Write(b []byte) (n int, err error) { return } - nonce := make([]byte, secure.NonceSize) + nonce := make([]byte, hap.NonceSize) binary.LittleEndian.PutUint64(nonce, c.encryptCnt) c.encryptCnt++ - buf := make([]byte, n+secure.Overhead) + buf := make([]byte, n+hap.Overhead) if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil { return } @@ -95,6 +114,8 @@ func (c *Conn) Write(b []byte) (n int, err error) { } err = c.wr.Flush() + + c.send += n return } diff --git a/pkg/hap/server.go b/pkg/hap/server.go index 99c86f6b..f962a440 100644 --- a/pkg/hap/server.go +++ b/pkg/hap/server.go @@ -6,28 +6,23 @@ import ( "encoding/base64" "errors" "fmt" - "net" "net/http" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/curve25519" "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" + "github.com/tadglines/go-pkgs/crypto/srp" ) -type HandlerFunc func(net.Conn) error - type Server struct { Pin string DeviceID string DevicePrivate []byte - GetPair func(conn net.Conn, id string) []byte - AddPair func(conn net.Conn, id string, public []byte, permissions byte) - - Handler HandlerFunc + // GetClientPublic may be nil, so client validation will be disabled + GetClientPublic func(id string) []byte } func (s *Server) ServerPublic() []byte { @@ -48,37 +43,240 @@ func (s *Server) SetupHash() string { return base64.StdEncoding.EncodeToString(b[:4]) } -func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - // Request from iPhone +func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) { + // STEP 1. Request from iPhone var plainM1 struct { - PublicKey string `tlv8:"3"` - State byte `tlv8:"6"` + State byte `tlv8:"6"` + Method byte `tlv8:"0"` + Flags uint32 `tlv8:"19"` } - if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { - return err + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { + return } if plainM1.State != StateM1 { - return newRequestError(plainM1) + err = newRequestError(plainM1) + return + } + + username := []byte("Pair-Setup") + + // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) + pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username)) + if err != nil { + return + } + + pake.SaltLength = 16 + + salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin)) + if err != nil { + return + } + + session := pake.NewServerSession(username, salt, verifier) + + // STEP 2. Response to iPhone + plainM2 := struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + Salt string `tlv8:"2"` + }{ + State: StateM2, + PublicKey: string(session.GetB()), + Salt: string(salt), + } + body, err := tlv8.Marshal(plainM2) + if err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + // STEP 3. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return + } + + var plainM3 struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + Proof string `tlv8:"4"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil { + return + } + if plainM3.State != StateM3 { + err = newRequestError(plainM3) + return + } + + // important to compute key before verify client + sessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey)) + if err != nil { + return + } + + if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) { + err = errors.New("hap: VerifyClientAuthenticator") + return + } + + proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof + + // STEP 4. Response to iPhone + payloadM4 := struct { + State byte `tlv8:"6"` + Proof string `tlv8:"4"` + }{ + State: StateM4, + Proof: string(proof), + } + if body, err = tlv8.Marshal(payloadM4); err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + // STEP 5. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return + } + var cipherM5 struct { + State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil { + return + } + if cipherM5.State != StateM5 { + err = newRequestError(cipherM5) + return + } + + // decrypt message using session shared + encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") + if err != nil { + return + } + + b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData)) + if err != nil { + return + } + + // unpack message from TLV8 + var plainM5 struct { + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` + } + if err = tlv8.Unmarshal(b, &plainM5); err != nil { + return + } + + // 3. verify client ID and Public + remoteSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", + ) + if err != nil { + return + } + + b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey) + if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) { + err = errors.New("hap: ValidateSignature") + return + } + + // 4. generate signature to our ID and Public + localSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", + ) + if err != nil { + return + } + + b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic + signature, err := ed25519.Signature(s.DevicePrivate, b) + if err != nil { + return + } + + // 5. pack our ID and Public + plainM6 := struct { + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` + }{ + Identifier: s.DeviceID, + PublicKey: string(s.ServerPublic()), + Signature: string(signature), + } + if b, err = tlv8.Marshal(plainM6); err != nil { + return + } + + // 6. encrypt message + b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b) + if err != nil { + return + } + + // STEP 6. Response to iPhone + cipherM6 := struct { + State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` + }{ + State: StateM6, + EncryptedData: string(b), + } + if body, err = tlv8.Marshal(cipherM6); err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + id = plainM5.Identifier + publicKey = []byte(plainM5.PublicKey) + + return +} + +func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) { + // Request from iPhone + var plainM1 struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { + return + } + if plainM1.State != StateM1 { + err = newRequestError(plainM1) + return } // Generate the key pair sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey)) if err != nil { - return err + return } encryptKey, err := hkdf.Sha512( sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", ) if err != nil { - return err + return } b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey) signature, err := ed25519.Signature(s.DevicePrivate, b) if err != nil { - return err + return } // STEP M2. Response to iPhone @@ -90,12 +288,12 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co Signature: string(signature), } if b, err = tlv8.Marshal(plainM2); err != nil { - return err + return } b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b) if err != nil { - return err + return } cipherM2 := struct { @@ -109,30 +307,32 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co } body, err := tlv8.Marshal(cipherM2) if err != nil { - return err + return } if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err + return } // STEP M3. Request from iPhone if req, err = http.ReadRequest(rw.Reader); err != nil { - return err + return } var cipherM3 struct { - EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` } if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil { - return err + return } if cipherM3.State != StateM3 { - return newRequestError(cipherM3) + err = newRequestError(cipherM3) + return } - if b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)); err != nil { - return err + b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)) + if err != nil { + return } var plainM3 struct { @@ -140,17 +340,21 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co Signature string `tlv8:"10"` } if err = tlv8.Unmarshal(b, &plainM3); err != nil { - return err + return } - clientPublic := s.GetPair(conn, plainM3.Identifier) - if clientPublic == nil { - return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier) - } + if s.GetClientPublic != nil { + clientPublic := s.GetClientPublic(plainM3.Identifier) + if clientPublic == nil { + err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier) + return + } - b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) - if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { - return errors.New("new: ValidateSignature") + b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) + if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { + err = errors.New("hap: ValidateSignature") + return + } } // STEP M4. Response to iPhone @@ -160,15 +364,41 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co State: StateM4, } if body, err = tlv8.Marshal(payloadM4); err != nil { - return err + return } if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err + return } - if conn, err = secure.Client(conn, rw, sessionShared, false); err != nil { - return err - } + id = plainM3.Identifier + sessionKey = sessionShared - return s.Handler(conn) + return } + +func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error { + header := fmt.Sprintf( + "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", + statusCode, http.StatusText(statusCode), contentType, len(body), + ) + body = append([]byte(header), body...) + if _, err := w.Write(body); err != nil { + return err + } + return w.Flush() +} + +//func WriteBackoff(rw *bufio.ReadWriter) error { +// plainM2 := struct { +// State byte `tlv8:"6"` +// Error byte `tlv8:"7"` +// }{ +// State: StateM2, +// Error: 3, // BackoffError +// } +// body, err := tlv8.Marshal(plainM2) +// if err != nil { +// return err +// } +// return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) +//} diff --git a/pkg/hap/server_pairing.go b/pkg/hap/server_pairing.go deleted file mode 100644 index 571ba7a2..00000000 --- a/pkg/hap/server_pairing.go +++ /dev/null @@ -1,266 +0,0 @@ -package hap - -import ( - "bufio" - "crypto/sha512" - "errors" - "fmt" - "net" - "net/http" - - "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" - "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" - "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" - "github.com/tadglines/go-pkgs/crypto/srp" -) - -const ( - PairMethodSetup = iota - PairMethodSetupWithAuth - PairMethodVerify - PairMethodAdd - PairMethodRemove - PairMethodList -) - -func (s *Server) HandleConn(conn net.Conn) error { - rd := bufio.NewReader(conn) - req, err := http.ReadRequest(rd) - if err != nil { - return err - } - - rw := bufio.NewReadWriter(rd, bufio.NewWriter(conn)) - - switch req.RequestURI { - case PathPairSetup: - return s.PairSetup(req, rw, conn) - case PathPairVerify: - return s.PairVerify(req, rw, conn) - } - - return errors.New("hap: unsupported request uri: " + req.RequestURI) -} - -func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - // STEP 1. Request from iPhone - var plainM1 struct { - Method byte `tlv8:"0"` - State byte `tlv8:"6"` - Flags uint32 `tlv8:"19"` - } - if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { - return err - } - if plainM1.State != StateM1 { - return newRequestError(plainM1) - } - - username := []byte("Pair-Setup") - - // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) - pake, err := srp.NewSRP( - "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), - ) - if err != nil { - return err - } - - pake.SaltLength = 16 - - salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin)) - - session := pake.NewServerSession(username, salt, verifier) - - // STEP 2. Response to iPhone - plainM2 := struct { - Salt string `tlv8:"2"` - PublicKey string `tlv8:"3"` - State byte `tlv8:"6"` - }{ - State: StateM2, - PublicKey: string(session.GetB()), - Salt: string(salt), - } - body, err := tlv8.Marshal(plainM2) - if err != nil { - return err - } - if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err - } - - // STEP 3. Request from iPhone - if req, err = http.ReadRequest(rw.Reader); err != nil { - return err - } - - var plainM3 struct { - SessionKey string `tlv8:"3"` - Proof string `tlv8:"4"` - State byte `tlv8:"6"` - } - if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil { - return err - } - if plainM3.State != StateM3 { - return newRequestError(plainM3) - } - - // important to compute key before verify client - sessionShared, err := session.ComputeKey([]byte(plainM3.SessionKey)) - if err != nil { - return err - } - - if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) { - return errors.New("hap: VerifyClientAuthenticator") - } - - proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof - - // STEP 4. Response to iPhone - payloadM4 := struct { - Proof string `tlv8:"4"` - State byte `tlv8:"6"` - }{ - Proof: string(proof), - State: StateM4, - } - if body, err = tlv8.Marshal(payloadM4); err != nil { - return err - } - if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err - } - - // STEP 5. Request from iPhone - if req, err = http.ReadRequest(rw.Reader); err != nil { - return err - } - var cipherM5 struct { - EncryptedData string `tlv8:"5"` - State byte `tlv8:"6"` - } - if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil { - return err - } - if cipherM5.State != StateM5 { - return newRequestError(cipherM5) - } - - // decrypt message using session shared - encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") - if err != nil { - return err - } - - b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData)) - if err != nil { - return err - } - - // unpack message from TLV8 - var plainM5 struct { - Identifier string `tlv8:"1"` - PublicKey string `tlv8:"3"` - Signature string `tlv8:"10"` - } - if err = tlv8.Unmarshal(b, &plainM5); err != nil { - return err - } - - // 3. verify client ID and Public - remoteSign, err := hkdf.Sha512( - sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", - ) - if err != nil { - return err - } - - b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey) - if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) { - return errors.New("hap: ValidateSignature") - } - - // 4. generate signature to our ID and Public - localSign, err := hkdf.Sha512( - sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", - ) - if err != nil { - return err - } - - b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic - signature, err := ed25519.Signature(s.DevicePrivate, b) - if err != nil { - return err - } - - // 5. pack our ID and Public - plainM6 := struct { - Identifier string `tlv8:"1"` - PublicKey string `tlv8:"3"` - Signature string `tlv8:"10"` - }{ - Identifier: s.DeviceID, - PublicKey: string(s.ServerPublic()), - Signature: string(signature), - } - if b, err = tlv8.Marshal(plainM6); err != nil { - return err - } - - // 6. encrypt message - b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b) - if err != nil { - return err - } - - // STEP 6. Response to iPhone - cipherM6 := struct { - EncryptedData string `tlv8:"5"` - State byte `tlv8:"6"` - }{ - State: StateM6, - EncryptedData: string(b), - } - if body, err = tlv8.Marshal(cipherM6); err != nil { - return err - } - if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err - } - - s.AddPair(conn, plainM5.Identifier, []byte(plainM5.PublicKey), PermissionAdmin) - - return nil -} - -func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error { - header := fmt.Sprintf( - "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", - statusCode, http.StatusText(statusCode), contentType, len(body), - ) - body = append([]byte(header), body...) - if _, err := w.Write(body); err != nil { - return err - } - return w.Flush() -} - -func WriteBackoff(rw *bufio.ReadWriter) error { - plainM2 := struct { - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - }{ - State: StateM2, - Error: 3, // BackoffError - } - body, err := tlv8.Marshal(plainM2) - if err != nil { - return err - } - return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) -} diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 05204218..c1be7447 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -49,7 +49,7 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { Connection: core.Connection{ ID: core.NewID(), FormatName: "homekit", - Protocol: "udp", + Protocol: "rtp", RemoteAddr: conn.RemoteAddr().String(), Medias: medias, Transport: conn, @@ -59,6 +59,10 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { } } +func (c *Consumer) SessionID() string { + return c.sessionID +} + func (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) { c.sessionID = offer.SessionID c.videoSession = &srtp.Session{ diff --git a/pkg/homekit/log/debug.go b/pkg/homekit/log/debug.go new file mode 100644 index 00000000..1fb60be2 --- /dev/null +++ b/pkg/homekit/log/debug.go @@ -0,0 +1,45 @@ +package log + +import ( + "bytes" + "io" + "log" + "net/http" +) + +func Debug(v any) { + switch v := v.(type) { + case *http.Request: + if v == nil { + return + } + if v.ContentLength != 0 { + b, err := io.ReadAll(v.Body) + if err != nil { + panic(err) + } + v.Body = io.NopCloser(bytes.NewReader(b)) + log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b) + } else { + log.Printf("[homekit] request: %s %s ", v.Method, v.RequestURI) + } + case *http.Response: + if v == nil { + return + } + if v.Header.Get("Content-Type") == "image/jpeg" { + log.Printf("[homekit] response: %d ", v.StatusCode) + return + } + if v.ContentLength != 0 { + b, err := io.ReadAll(v.Body) + if err != nil { + panic(err) + } + v.Body = io.NopCloser(bytes.NewReader(b)) + log.Printf("[homekit] response: %s %d\n%s", v.Proto, v.StatusCode, b) + } else { + log.Printf("[homekit] response: %s %d ", v.Proto, v.StatusCode) + } + } +} diff --git a/pkg/homekit/producer.go b/pkg/homekit/producer.go index 3351a736..04719612 100644 --- a/pkg/homekit/producer.go +++ b/pkg/homekit/producer.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "net" - "net/url" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -34,24 +33,11 @@ type Client struct { } func Dial(rawURL string, server *srtp.Server) (*Client, error) { - u, err := url.Parse(rawURL) + conn, err := hap.Dial(rawURL) if err != nil { return nil, err } - query := u.Query() - conn := &hap.Client{ - DeviceAddress: u.Host, - DeviceID: query.Get("device_id"), - DevicePublic: hap.DecodeKey(query.Get("device_public")), - ClientID: query.Get("client_id"), - ClientPrivate: hap.DecodeKey(query.Get("client_private")), - } - - if err = conn.Dial(); err != nil { - return nil, err - } - client := &Client{ Connection: core.Connection{ ID: core.NewID(), diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go index ac2f14d7..2132266c 100644 --- a/pkg/homekit/proxy.go +++ b/pkg/homekit/proxy.go @@ -4,31 +4,30 @@ import ( "bufio" "bytes" "encoding/json" - "fmt" "io" "net" "net/http" + "time" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/camera" "github.com/AlexxIT/go2rtc/pkg/hap/hds" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) -func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc { +type ServerProxy interface { + ServerPair + AddConn(conn any) + DelConn(conn any) +} + +func ProxyHandler(srv ServerProxy, acc net.Conn) HandlerFunc { return func(con net.Conn) error { defer con.Close() - acc, err := dial() - if err != nil { - return err - } - defer acc.Close() - pr := &Proxy{ - con: con.(*secure.Conn), - acc: acc.(*secure.Conn), + con: con.(*hap.Conn), + acc: acc.(*hap.Conn), res: make(chan *http.Response), } @@ -36,17 +35,17 @@ func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFun go pr.handleAcc() // controller => accessory - return pr.handleCon(pair) + return pr.handleCon(srv) } } type Proxy struct { - con *secure.Conn - acc *secure.Conn + con *hap.Conn + acc *hap.Conn res chan *http.Response } -func (p *Proxy) handleCon(pair ServerPair) error { +func (p *Proxy) handleCon(srv ServerProxy) error { var hdsCharIID uint64 rd := bufio.NewReader(p.con) @@ -61,7 +60,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { switch { case req.Method == "POST" && req.URL.Path == hap.PathPairings: var res *http.Response - if res, err = handlePairings(p.con, req, pair); err != nil { + if res, err = handlePairings(req, srv); err != nil { return err } if err = res.Write(p.con); err != nil { @@ -117,7 +116,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { hdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort) // swtich accPort to conPort - hdsPort, err = p.listenHDS(hdsPort, hdsConSalt+hdsAccSalt) + hdsPort, err = p.listenHDS(srv, hdsPort, hdsConSalt+hdsAccSalt) if err != nil { return err } @@ -166,7 +165,8 @@ func (p *Proxy) handleAcc() error { } } -func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { +func (p *Proxy) listenHDS(srv ServerProxy, accPort int, salt string) (int, error) { + // The TCP port range for HDS must be >= 32768. ln, err := net.ListenTCP("tcp", nil) if err != nil { return 0, err @@ -175,30 +175,36 @@ func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { go func() { defer ln.Close() + _ = ln.SetDeadline(time.Now().Add(30 * time.Second)) + // raw controller conn - con, err := ln.Accept() + conn1, err := ln.Accept() if err != nil { return } - defer con.Close() + + defer conn1.Close() // secured controller conn (controlle=false because we are accessory) - con, err = hds.Client(con, p.con.SharedKey, salt, false) + con, err := hds.NewConn(conn1, p.con.SharedKey, salt, false) if err != nil { return } + srv.AddConn(con) + defer srv.DelConn(con) + accIP := p.acc.RemoteAddr().(*net.TCPAddr).IP // raw accessory conn - acc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", accIP, accPort)) + conn2, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: accIP, Port: accPort}) if err != nil { return } - defer acc.Close() + defer conn2.Close() // secured accessory conn (controller=true because we are controller) - acc, err = hds.Client(acc, p.acc.SharedKey, salt, true) + acc, err := hds.NewConn(conn2, p.acc.SharedKey, salt, true) if err != nil { return } diff --git a/pkg/homekit/server.go b/pkg/homekit/server.go index 2d00deab..75ba2a0f 100644 --- a/pkg/homekit/server.go +++ b/pkg/homekit/server.go @@ -15,15 +15,17 @@ import ( "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) +type HandlerFunc func(net.Conn) error + type Server interface { ServerPair ServerAccessory } type ServerPair interface { - GetPair(conn net.Conn, id string) []byte - AddPair(conn net.Conn, id string, public []byte, permissions byte) - DelPair(conn net.Conn, id string) + GetPair(id string) []byte + AddPair(id string, public []byte, permissions byte) + DelPair(id string) } type ServerAccessory interface { @@ -33,11 +35,11 @@ type ServerAccessory interface { GetImage(conn net.Conn, width, height int) []byte } -func ServerHandler(server Server) hap.HandlerFunc { +func ServerHandler(server Server) HandlerFunc { return handleRequest(func(conn net.Conn, req *http.Request) (*http.Response, error) { switch req.URL.Path { case hap.PathPairings: - return handlePairings(conn, req, server) + return handlePairings(req, server) case hap.PathAccessories: body := hap.JSONAccessories{Value: server.GetAccessories(conn)} @@ -103,7 +105,7 @@ func ServerHandler(server Server) hap.HandlerFunc { }) } -func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) hap.HandlerFunc { +func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) HandlerFunc { return func(conn net.Conn) error { rw := bufio.NewReaderSize(conn, 16*1024) wr := bufio.NewWriterSize(conn, 16*1024) @@ -130,7 +132,7 @@ func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response } } -func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Response, error) { +func handlePairings(req *http.Request, srv ServerPair) (*http.Response, error) { cmd := struct { Method byte `tlv8:"0"` Identifier string `tlv8:"1"` @@ -145,9 +147,9 @@ func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Re switch cmd.Method { case 3: // add - pair.AddPair(conn, cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) + srv.AddPair(cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) case 4: // delete - pair.DelPair(conn, cmd.Identifier) + srv.DelPair(cmd.Identifier) } body := struct { @@ -190,40 +192,3 @@ func makeResponse(mime string, v any) (*http.Response, error) { } return res, nil } - -//func debug(v any) { -// switch v := v.(type) { -// case *http.Request: -// if v == nil { -// return -// } -// if v.ContentLength != 0 { -// b, err := io.ReadAll(v.Body) -// if err != nil { -// panic(err) -// } -// v.Body = io.NopCloser(bytes.NewReader(b)) -// log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b) -// } else { -// log.Printf("[homekit] request: %s %s ", v.Method, v.RequestURI) -// } -// case *http.Response: -// if v == nil { -// return -// } -// if v.Header.Get("Content-Type") == "image/jpeg" { -// log.Printf("[homekit] response: %d ", v.StatusCode) -// return -// } -// if v.ContentLength != 0 { -// b, err := io.ReadAll(v.Body) -// if err != nil { -// panic(err) -// } -// v.Body = io.NopCloser(bytes.NewReader(b)) -// log.Printf("[homekit] response: %d\n%s", v.StatusCode, b) -// } else { -// log.Printf("[homekit] response: %d ", v.StatusCode) -// } -// } -//} diff --git a/www/add.html b/www/add.html index 53d6b3dc..17c05059 100644 --- a/www/add.html +++ b/www/add.html @@ -100,7 +100,7 @@
- +
@@ -112,7 +112,7 @@
+ - - -
-
- - - -
-
- - - - -
-
-
- - - - -
-
- - - - -
-
- - -
-
-
- + const r = await fetch(url, {method: 'PUT'}); + alert(r.ok ? 'OK' : 'ERROR: ' + await r.text()); + }); + - -
-
-
- + +
+
+
+ - -
-
-
- + +
+
+ + + + +
+
+ + +
+
+
+ - - - -
-
- - - - - -
-
-
- - - -
-
- - - - -
-
- - -
-
-
- + + + +
+
+
+ + + + +
+
+
+ + + + +
+
+
+ + + + +
+
+ + + + + +
+
+
+ + + +
+
+
+ + + + +
+
+
+ + + + +
+
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + +
+
+
+ - - -
-
-
- + document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth); + document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth); + - -
-
-
- + +
+
+ + + +
+ +
+
+ - -
-
- - -
-
-
- + +
+
+
+ - -
-
- - - -
- -
-
- - - - -
-
-
- - - - -
-
-
- - + +
+
+
+ + diff --git a/www/editor.html b/www/config.html similarity index 75% rename from www/editor.html rename to www/config.html index cb455f4d..9e1853c2 100644 --- a/www/editor.html +++ b/www/config.html @@ -1,41 +1,36 @@ - go2rtc - File Editor - - + + + go2rtc - Config - -
- -
-
-
- + +
+
+ +
+
+
+ + + diff --git a/www/index.html b/www/index.html index 63fedcec..2f7ac967 100644 --- a/www/index.html +++ b/www/index.html @@ -1,61 +1,49 @@ - - - - - - + + go2rtc + -
-
- - - - - -
- - - - - - - - - - -
OnlineCommands
+ +
+
+ + modes + + + + +
+ + + + + + + + + + +
onlinecommands
+
+
+ + diff --git a/www/links.html b/www/links.html index 3b651762..a54fcf8f 100644 --- a/www/links.html +++ b/www/links.html @@ -1,27 +1,10 @@ + + go2rtc - links - - - - - + +
+ + + }); + -
-

Play audio

-
-
-
-
- - send / cameras with two way audio support -
- +
+

Play audio

+ + + +
+ + + / cameras with two way audio support +
+ -
-

Publish stream

-
YouTube:  rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
+    
+

Publish stream

+
YouTube:  rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
 Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- - send / Telegram RTMPS server -
- + + + / Telegram RTMPS server +
+ -
-

WebRTC Magic

-
-
-
-
+
+

WebRTC Magic

+ + + + -
-
  • webrtc.html local WebRTC viewer
  • +
    +
  • webrtc.html local WebRTC viewer
  • -
  • - share link - copy link - delete - external WebRTC viewer -
  • -
    - + +
    diff --git a/www/log.html b/www/log.html index 84ec0675..67476603 100644 --- a/www/log.html +++ b/www/log.html @@ -1,69 +1,64 @@ + + go2rtc - Logs - - + -
    - - - -
    -
    - - - - - - - - - - -
    TimeLevelMessage
    + +
    +
    + + + +
    + + + + + + + + + + +
    TimeLevelMessage
    +
    + + diff --git a/www/main.js b/www/main.js index 714c9127..36b04495 100644 --- a/www/main.js +++ b/www/main.js @@ -1,200 +1,134 @@ -// main menu -document.body.innerHTML = ` +document.head.innerHTML += ` - +`; + +document.body.innerHTML = ` +
    + +
    ` + document.body.innerHTML; - -const sunIcon = '☀️'; -const moonIcon = '🌕'; - -document.addEventListener('DOMContentLoaded', () => { - const darkModeToggle = document.getElementById('darkModeToggle'); - const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); - - const isDarkModeEnabled = () => document.body.classList.contains('dark-mode'); - - // Update the toggle button based on the dark mode state - const updateToggleButton = () => { - if (isDarkModeEnabled()) { - darkModeToggle.innerHTML = sunIcon; - darkModeToggle.setAttribute('aria-label', 'Enable light mode'); - } else { - darkModeToggle.innerHTML = moonIcon; - darkModeToggle.setAttribute('aria-label', 'Enable dark mode'); - } - }; - - const updateDarkMode = () => { - if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches && localStorage.getItem('darkMode') !== 'disabled') { - document.body.classList.add('dark-mode'); - } else { - document.body.classList.remove('dark-mode'); - } - updateEditorTheme(); - updateToggleButton(); - }; - - // Update the editor theme based on the dark mode state - const updateEditorTheme = () => { - if (typeof editor !== 'undefined') { - editor.setTheme(isDarkModeEnabled() ? 'ace/theme/tomorrow_night_eighties' : 'ace/theme/github'); - } - }; - - // Initial update for dark mode and toggle button - updateDarkMode(); - - // Listen for changes in the system's color scheme preference - prefersDarkScheme.addEventListener('change', updateDarkMode); // Modern approach - - // Toggle dark mode and update local storage on button click - darkModeToggle.addEventListener('click', () => { - const enabled = document.body.classList.toggle('dark-mode'); - localStorage.setItem('darkMode', enabled ? 'enabled' : 'disabled'); - updateToggleButton(); // Update the button after toggling - updateEditorTheme(); - }); -}); diff --git a/www/network.html b/www/net.html similarity index 87% rename from www/network.html rename to www/net.html index 79875012..8f0f91ac 100644 --- a/www/network.html +++ b/www/net.html @@ -2,31 +2,21 @@ + go2rtc - Network -
    + + +
    + + From ac80f1470ef09093c3b59aae628ffd6f9849883e Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Nov 2025 18:20:53 +0300 Subject: [PATCH 21/34] Add errors output to streams API --- internal/hass/api.go | 4 ++-- internal/hls/ws.go | 2 +- internal/mjpeg/init.go | 4 ++-- internal/mp4/mp4.go | 2 +- internal/mp4/ws.go | 4 ++-- internal/streams/api.go | 8 ++++---- internal/streams/streams.go | 35 ++++++++++++++++++++--------------- internal/webrtc/webrtc.go | 2 +- 8 files changed, 33 insertions(+), 28 deletions(-) diff --git a/internal/hass/api.go b/internal/hass/api.go index e3de23b3..9f110fc8 100644 --- a/internal/hass/api.go +++ b/internal/hass/api.go @@ -30,10 +30,10 @@ func apiStream(w http.ResponseWriter, r *http.Request) { // 1. link to go2rtc stream: rtsp://...:8554/{stream_name} // 2. static link to Hass camera // 3. dynamic link to Hass camera - if streams.Patch(v.Name, v.Channels.First.Url) != nil { + if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil { apiOK(w, r) } else { - http.Error(w, "", http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusBadRequest) } // /stream/{id}/channel/0/webrtc diff --git a/internal/hls/ws.go b/internal/hls/ws.go index 608f515f..00eedfe2 100644 --- a/internal/hls/ws.go +++ b/internal/hls/ws.go @@ -11,7 +11,7 @@ import ( ) func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index 27c557e4..2fa9fa32 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -36,7 +36,7 @@ func Init() { var log zerolog.Logger func handlerKeyframe(w http.ResponseWriter, r *http.Request) { - stream := streams.GetOrPatch(r.URL.Query()) + stream, _ := streams.GetOrPatch(r.URL.Query()) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return @@ -145,7 +145,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) { } func handlerWS(tr *ws.Transport, _ *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index cca5220c..d0a6d971 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -91,7 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { return } - stream := streams.GetOrPatch(query) + stream, _ := streams.GetOrPatch(query) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return diff --git a/internal/mp4/ws.go b/internal/mp4/ws.go index c880fb58..c1afac24 100644 --- a/internal/mp4/ws.go +++ b/internal/mp4/ws.go @@ -11,7 +11,7 @@ import ( ) func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } @@ -43,7 +43,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { } func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } diff --git a/internal/streams/api.go b/internal/streams/api.go index 28f09708..c2b93b91 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -52,8 +52,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { name = src } - if New(name, query["src"]...) == nil { - http.Error(w, "", http.StatusBadRequest) + if _, err := New(name, query["src"]...); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -69,8 +69,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { } // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass - if Patch(name, src) == nil { - http.Error(w, "", http.StatusBadRequest) + if _, err := Patch(name, src); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) } case "POST": diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 633ad2d1..2bc65486 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -1,6 +1,7 @@ package streams import ( + "errors" "net/url" "sync" "time" @@ -48,10 +49,14 @@ func Init() { }) } -func New(name string, sources ...string) *Stream { +func New(name string, sources ...string) (*Stream, error) { for _, source := range sources { - if Validate(source) != nil { - return nil + if !HasProducer(source) { + return nil, errors.New("streams: source not supported") + } + + if err := Validate(source); err != nil { + return nil, err } } @@ -61,10 +66,10 @@ func New(name string, sources ...string) *Stream { streams[name] = stream streamsMu.Unlock() - return stream + return stream, nil } -func Patch(name string, source string) *Stream { +func Patch(name string, source string) (*Stream, error) { streamsMu.Lock() defer streamsMu.Unlock() @@ -76,7 +81,7 @@ func Patch(name string, source string) *Stream { // link (alias) streams[name] to streams[rtspName] streams[name] = stream } - return stream + return stream, nil } } @@ -85,40 +90,40 @@ func Patch(name string, source string) *Stream { // link (alias) streams[name] to streams[source] streams[name] = stream } - return stream + return stream, nil } // check if src has supported scheme if !HasProducer(source) { - return nil + return nil, errors.New("streams: source not supported") } - if Validate(source) != nil { - return nil + if err := Validate(source); err != nil { + return nil, err } // check an existing stream with this name if stream, ok := streams[name]; ok { stream.SetSource(source) - return stream + return stream, nil } // create new stream with this name stream := NewStream(source) streams[name] = stream - return stream + return stream, nil } -func GetOrPatch(query url.Values) *Stream { +func GetOrPatch(query url.Values) (*Stream, error) { // check if src param exists source := query.Get("src") if source == "" { - return nil + return nil, errors.New("streams: source empty") } // check if src is stream name if stream := Get(source); stream != nil { - return stream + return stream, nil } // check if name param provided diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index 11e9db89..eca1e12b 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -95,7 +95,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) { query := tr.Request.URL.Query() if name := query.Get("src"); name != "" { - stream = streams.GetOrPatch(query) + stream, _ = streams.GetOrPatch(query) mode = core.ModePassiveConsumer log.Debug().Str("src", name).Msg("[webrtc] new consumer") } else if name = query.Get("dst"); name != "" { From 3897f10a4dd7337b027566ae9b172d1953ef7578 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 16 Nov 2025 16:32:58 +0100 Subject: [PATCH 22/34] Add api endpoint to return supported schema --- .vscode/launch.json | 19 +++++++ internal/streams/api.go | 10 ++++ internal/streams/api_test.go | 102 +++++++++++++++++++++++++++++++++++ internal/streams/handlers.go | 13 +++++ internal/streams/streams.go | 1 + 5 files changed, 145 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 internal/streams/api_test.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..6242075e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "name": "Debug go2rtc", + "type": "go", + "request": "launch", + "mode": "auto", + "env": { + "CGO_ENABLED": "0" + }, + "program": "main.go", + } + ] +} \ No newline at end of file diff --git a/internal/streams/api.go b/internal/streams/api.go index 28f09708..bd9e7f7c 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -176,3 +176,13 @@ func apiPreload(w http.ResponseWriter, r *http.Request) { http.Error(w, "", http.StatusMethodNotAllowed) } } + +func apiSchemes(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + http.Error(w, "", http.StatusMethodNotAllowed) + return + } + + schemes := GetSupportedSchemes() + api.ResponseJSON(w, schemes) +} diff --git a/internal/streams/api_test.go b/internal/streams/api_test.go new file mode 100644 index 00000000..0a4b4c06 --- /dev/null +++ b/internal/streams/api_test.go @@ -0,0 +1,102 @@ +package streams + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/stretchr/testify/require" +) + +func TestApiSchemes(t *testing.T) { + // Setup: Register some test handlers and redirects + HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil }) + HandleFunc("rtmp", func(url string) (core.Producer, error) { return nil, nil }) + RedirectFunc("http", func(url string) (string, error) { return "", nil }) + + t.Run("GET request returns schemes", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var schemes []string + err := json.Unmarshal(w.Body.Bytes(), &schemes) + require.NoError(t, err) + require.NotEmpty(t, schemes) + + // Check that our test schemes are in the response + require.Contains(t, schemes, "rtsp") + require.Contains(t, schemes, "rtmp") + require.Contains(t, schemes, "http") + }) + + t.Run("POST request returns method not allowed", func(t *testing.T) { + req := httptest.NewRequest("POST", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusMethodNotAllowed, w.Code) + }) + + t.Run("PUT request returns method not allowed", func(t *testing.T) { + req := httptest.NewRequest("PUT", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusMethodNotAllowed, w.Code) + }) + + t.Run("DELETE request returns method not allowed", func(t *testing.T) { + req := httptest.NewRequest("DELETE", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusMethodNotAllowed, w.Code) + }) + + t.Run("PATCH request returns method not allowed", func(t *testing.T) { + req := httptest.NewRequest("PATCH", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusMethodNotAllowed, w.Code) + }) +} + +func TestApiSchemesNoDuplicates(t *testing.T) { + // Setup: Register a scheme in both handlers and redirects + HandleFunc("duplicate", func(url string) (core.Producer, error) { return nil, nil }) + RedirectFunc("duplicate", func(url string) (string, error) { return "", nil }) + + req := httptest.NewRequest("GET", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + var schemes []string + err := json.Unmarshal(w.Body.Bytes(), &schemes) + require.NoError(t, err) + + // Count occurrences of "duplicate" + count := 0 + for _, scheme := range schemes { + if scheme == "duplicate" { + count++ + } + } + + // Should only appear once + require.Equal(t, 1, count, "scheme 'duplicate' should appear exactly once") +} diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 8922bb8d..91efb975 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -2,7 +2,9 @@ package streams import ( "errors" + "maps" "regexp" + "slices" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -16,6 +18,17 @@ func HandleFunc(scheme string, handler Handler) { handlers[scheme] = handler } +func GetSupportedSchemes() []string { + unique := make(map[string]bool) + for scheme := range handlers { + unique[scheme] = true + } + for scheme := range redirects { + unique[scheme] = true + } + return slices.Collect(maps.Keys(unique)) +} + func HasProducer(url string) bool { if i := strings.IndexByte(url, ':'); i > 0 { scheme := url[:i] diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 633ad2d1..8d5a9fe7 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -28,6 +28,7 @@ func Init() { api.HandleFunc("api/streams", apiStreams) api.HandleFunc("api/streams.dot", apiStreamsDOT) api.HandleFunc("api/preload", apiPreload) + api.HandleFunc("api/schemes", apiSchemes) if cfg.Publish == nil && cfg.Preload == nil { return From e2b63a4f6c78f6432b938287ed42e1a45cb976b5 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Sun, 16 Nov 2025 16:40:04 +0100 Subject: [PATCH 23/34] Remove duplicate code --- internal/streams/api_test.go | 42 +++++++++--------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/internal/streams/api_test.go b/internal/streams/api_test.go index 0a4b4c06..414a9380 100644 --- a/internal/streams/api_test.go +++ b/internal/streams/api_test.go @@ -36,40 +36,18 @@ func TestApiSchemes(t *testing.T) { require.Contains(t, schemes, "http") }) - t.Run("POST request returns method not allowed", func(t *testing.T) { - req := httptest.NewRequest("POST", "/api/schemes", nil) - w := httptest.NewRecorder() + t.Run("non-GET requests return method not allowed", func(t *testing.T) { + methods := []string{"POST", "PUT", "DELETE", "PATCH"} + for _, method := range methods { + t.Run(method, func(t *testing.T) { + req := httptest.NewRequest(method, "/api/schemes", nil) + w := httptest.NewRecorder() - apiSchemes(w, req) + apiSchemes(w, req) - require.Equal(t, http.StatusMethodNotAllowed, w.Code) - }) - - t.Run("PUT request returns method not allowed", func(t *testing.T) { - req := httptest.NewRequest("PUT", "/api/schemes", nil) - w := httptest.NewRecorder() - - apiSchemes(w, req) - - require.Equal(t, http.StatusMethodNotAllowed, w.Code) - }) - - t.Run("DELETE request returns method not allowed", func(t *testing.T) { - req := httptest.NewRequest("DELETE", "/api/schemes", nil) - w := httptest.NewRecorder() - - apiSchemes(w, req) - - require.Equal(t, http.StatusMethodNotAllowed, w.Code) - }) - - t.Run("PATCH request returns method not allowed", func(t *testing.T) { - req := httptest.NewRequest("PATCH", "/api/schemes", nil) - w := httptest.NewRecorder() - - apiSchemes(w, req) - - require.Equal(t, http.StatusMethodNotAllowed, w.Code) + require.Equal(t, http.StatusMethodNotAllowed, w.Code) + }) + } }) } From 0bae158e41a16fe8df037e4f355b226d2bbf60ad Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Nov 2025 19:01:06 +0300 Subject: [PATCH 24/34] Code refactoring for #1939 --- .vscode/launch.json | 19 ------------------- internal/streams/api.go | 8 +------- internal/streams/api_test.go | 14 -------------- internal/streams/handlers.go | 16 +++++++++------- 4 files changed, 10 insertions(+), 47 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 6242075e..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - - { - "name": "Debug go2rtc", - "type": "go", - "request": "launch", - "mode": "auto", - "env": { - "CGO_ENABLED": "0" - }, - "program": "main.go", - } - ] -} \ No newline at end of file diff --git a/internal/streams/api.go b/internal/streams/api.go index bd9e7f7c..0cc537c3 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -178,11 +178,5 @@ func apiPreload(w http.ResponseWriter, r *http.Request) { } func apiSchemes(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - http.Error(w, "", http.StatusMethodNotAllowed) - return - } - - schemes := GetSupportedSchemes() - api.ResponseJSON(w, schemes) + api.ResponseJSON(w, SupportedSchemes()) } diff --git a/internal/streams/api_test.go b/internal/streams/api_test.go index 414a9380..2cb93d2a 100644 --- a/internal/streams/api_test.go +++ b/internal/streams/api_test.go @@ -35,20 +35,6 @@ func TestApiSchemes(t *testing.T) { require.Contains(t, schemes, "rtmp") require.Contains(t, schemes, "http") }) - - t.Run("non-GET requests return method not allowed", func(t *testing.T) { - methods := []string{"POST", "PUT", "DELETE", "PATCH"} - for _, method := range methods { - t.Run(method, func(t *testing.T) { - req := httptest.NewRequest(method, "/api/schemes", nil) - w := httptest.NewRecorder() - - apiSchemes(w, req) - - require.Equal(t, http.StatusMethodNotAllowed, w.Code) - }) - } - }) } func TestApiSchemesNoDuplicates(t *testing.T) { diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 91efb975..9433044b 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -2,9 +2,7 @@ package streams import ( "errors" - "maps" "regexp" - "slices" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -18,15 +16,19 @@ func HandleFunc(scheme string, handler Handler) { handlers[scheme] = handler } -func GetSupportedSchemes() []string { - unique := make(map[string]bool) +func SupportedSchemes() []string { + uniqueKeys := make(map[string]struct{}, len(handlers)+len(redirects)) for scheme := range handlers { - unique[scheme] = true + uniqueKeys[scheme] = struct{}{} } for scheme := range redirects { - unique[scheme] = true + uniqueKeys[scheme] = struct{}{} } - return slices.Collect(maps.Keys(unique)) + resultKeys := make([]string, 0, len(uniqueKeys)) + for key := range uniqueKeys { + resultKeys = append(resultKeys, key) + } + return resultKeys } func HasProducer(url string) bool { From cb22ae78339faff17f9d5b2218fafef993f14719 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Nov 2025 18:31:38 +0300 Subject: [PATCH 25/34] Add security notes to readme --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index b6d2ad93..ee647bf3 100644 --- a/README.md +++ b/README.md @@ -879,6 +879,7 @@ api: listen: ":1984" # default ":1984", HTTP API port ("" - disabled) username: "admin" # default "", Basic auth for WebUI password: "pass" # default "", Basic auth for WebUI + local_auth: true # default false, Enable auth check for localhost requests base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api) static_dir: "www" # default "", folder for static files (custom web interface) origin: "*" # default "", allow CORS requests (only * supported) @@ -1201,6 +1202,27 @@ log: ## Security +> [!IMPORTANT] +> If an attacker gains access to the API, you are in danger. Through the API, an attacker can use insecure sources such as echo and exec. And get full access to your server. + +For maximum (paranoid) security, go2rtc has special settings: + +```yaml +app: + # use only allowed modules + modules: [api, rtsp, webrtc, exec, ffmpeg, mjpeg] + +api: + # use only allowed API paths + allow_paths: [/api, /api/streams, /api/webrtc, /api/frame.jpeg] + # enable auth for localhost (used together with username and password) + local_auth: true + +exec: + # use only allowed exec paths + allow_paths: [ffmpeg] +``` + By default, `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as uses port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on. This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config: From 2dc0d58ba73f349e7ff28156068788bc5f99037f Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 16 Nov 2025 19:08:34 +0300 Subject: [PATCH 26/34] Update version to 1.9.12 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index b2959c55..95e59ddd 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,7 @@ import ( ) func main() { - app.Version = "1.9.11" + app.Version = "1.9.12" type module struct { name string From e246e2e75622c1d7c54edebfad7c355155d9d791 Mon Sep 17 00:00:00 2001 From: Alex X Date: Mon, 17 Nov 2025 12:21:45 +0300 Subject: [PATCH 27/34] Fix WebUI for Hass black theme --- www/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/main.js b/www/main.js index 36b04495..c901f300 100644 --- a/www/main.js +++ b/www/main.js @@ -1,6 +1,7 @@ document.head.innerHTML += `