Compare commits

...

17 Commits

Author SHA1 Message Date
Alex X
df95ce39d0 Update version to 1.9.10 2025-09-24 16:34:54 +03:00
Alex X
26f16e392f Update go (build) version to 1.25 and related readme 2025-09-24 16:19:30 +03:00
Alex X
fcc837e570 Merge pull request #1879 from mihailstoynov/patch-2
Update README.md
2025-09-24 11:46:36 +03:00
Alex X
fd682306e7 Fix MultiUDPMuxDefault panic #1646 2025-09-22 18:11:47 +03:00
mihailstoynov
e072842b7a Update README.md
example for VGA and HD streams for ease of use
2025-09-21 22:56:59 +03:00
Alex X
3b976c6812 Improve HomeKit TLV format parser 2025-09-19 15:29:24 +03:00
Alex X
40269328fb Fix insecure PINs for HomeKit server 2025-09-19 15:27:58 +03:00
Alex X
45cbbaf1cf Fixed a race condition when changing the config file 2025-09-19 15:26:54 +03:00
Alex X
3f542a642c Merge pull request #1841 from hugoaboud/master
Security Patch: Sanitize credentials on websocket error messages
2025-09-19 15:24:51 +03:00
Alex X
8b4df5f02c Code refactoring for #1841 2025-09-19 15:21:02 +03:00
Alex X
788afb7189 Fix HomeKit server support on iOS 26 #1843 2025-09-18 23:08:33 +03:00
Alex X
cd7fa5d09c Fix RepairAVCC in some cases 2025-09-10 12:27:13 +03:00
Alex X
beb82045ff Fix yet another broken Content-Base for RTSP #1852 2025-08-29 16:49:05 +03:00
Alex X
33f0fd5fe6 Add lightNVR project to readme 2025-08-29 16:48:53 +03:00
Alex X
850051a299 Merge pull request #1845 from kvikindi/patch-1
(DOC) Update Proxmox Helper Scripts link in README.md
2025-08-25 14:58:44 +03:00
Ragnar Petursson
8f7cbdf60a Update Proxmox Helper Scripts link in README.md
Changed link to current link, as previous repository is inactive as of November 2nd 2024.
2025-08-23 18:09:28 +01:00
Hugo Aboud
4577390130 Sanitize credentials on websocket error messages 2025-08-19 16:16:01 -03:00
19 changed files with 249 additions and 79 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with: { go-version: '1.24' }
with: { go-version: '1.25' }
- name: Build go2rtc_win64
env: { GOOS: windows, GOARCH: amd64 }

View File

@@ -116,7 +116,7 @@ Ultimate camera streaming application with support for RTSP, WebRTC, HomeKit, FF
Download binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):
- `go2rtc_win64.zip` - Windows 10+ 64-bit
- `go2rtc_win32.zip` - Windows 7+ 32-bit
- `go2rtc_win32.zip` - Windows 10+ 32-bit
- `go2rtc_win_arm64.zip` - Windows ARM 64-bit
- `go2rtc_linux_amd64` - Linux 64-bit
- `go2rtc_linux_i386` - Linux 32-bit
@@ -124,7 +124,7 @@ Download binary for your OS from [latest release](https://github.com/AlexxIT/go2
- `go2rtc_linux_arm` - Linux ARM 32-bit (ex. Raspberry 32-bit OS)
- `go2rtc_linux_armv6` - Linux ARMv6 (for old Raspberry 1 and Zero)
- `go2rtc_linux_mipsel` - Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks))
- `go2rtc_mac_amd64.zip` - macOS 10.13+ Intel 64-bit
- `go2rtc_mac_amd64.zip` - macOS 11+ Intel 64-bit
- `go2rtc_mac_arm64.zip` - macOS ARM 64-bit
- `go2rtc_freebsd_amd64.zip` - FreeBSD 64-bit
- `go2rtc_freebsd_arm64.zip` - FreeBSD ARM 64-bit
@@ -531,7 +531,7 @@ streams:
- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)
- use the **cloud password**, this is not the RTSP password! you do not need to add a login!
- you can also use UPPERCASE MD5 hash from your cloud password with `admin` username
- you can also use **UPPERCASE** MD5 hash from your cloud password with `admin` username
- some new camera firmwares require SHA256 instead of MD5
```yaml
@@ -542,6 +542,10 @@ streams:
camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123
# admin username and UPPERCASE SHA256 cloud-password hash
camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123
# VGA stream (the so called substream, the lower resolution one)
camera4: tapo://cloud-password@192.168.1.123?subtype=1
# HD stream (default)
camera5: tapo://cloud-password@192.168.1.123?subtype=0
```
```bash
@@ -1384,6 +1388,7 @@ streams:
- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for controlling Eufy security devices
- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² module
- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring-to-MQTT bridge
- [lightNVR](https://github.com/opensensor/lightNVR)
**Distributions**
@@ -1391,7 +1396,7 @@ streams:
- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc)
- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)
- [NixOS](https://search.nixos.org/packages?query=go2rtc)
- [Proxmox Helper Scripts](https://tteck.github.io/Proxmox/)
- [Proxmox Helper Scripts](https://github.com/community-scripts/ProxmoxVE/)
- [QNAP](https://www.myqnap.org/product/go2rtc/)
- [Synology NAS](https://synocommunity.com/package/go2rtc)
- [Unraid](https://unraid.net/community/apps?q=go2rtc)

View File

@@ -2,7 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.11"
ARG GO_VERSION="1.24"
ARG GO_VERSION="1.25"
# 1. Build go2rtc binary

View File

@@ -4,7 +4,7 @@
# only debian 13 (trixie) has latest ffmpeg
# https://packages.debian.org/trixie/ffmpeg
ARG DEBIAN_VERSION="trixie-slim"
ARG GO_VERSION="1.24-bookworm"
ARG GO_VERSION="1.25-bookworm"
# 1. Build go2rtc binary

View File

@@ -2,7 +2,7 @@
# 0. Prepare images
ARG PYTHON_VERSION="3.13-slim-bookworm"
ARG GO_VERSION="1.24-bookworm"
ARG GO_VERSION="1.25-bookworm"
# 1. Build go2rtc binary

View File

@@ -11,6 +11,7 @@ import (
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
)
@@ -132,7 +133,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
if handler := wsHandlers[msg.Type]; handler != nil {
go func() {
if err = handler(tr, msg); err != nil {
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
errMsg := core.StripUserinfo(err.Error())
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
}
}()
}

View File

@@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/AlexxIT/go2rtc/pkg/shell"
"github.com/AlexxIT/go2rtc/pkg/yaml"
@@ -18,11 +19,16 @@ func LoadConfig(v any) {
}
}
var configMu sync.Mutex
func PatchConfig(path []string, value any) error {
if ConfigPath == "" {
return errors.New("config file disabled")
}
configMu.Lock()
defer configMu.Unlock()
// empty config is OK
b, _ := os.ReadFile(ConfigPath)

View File

@@ -44,7 +44,7 @@ import (
)
func main() {
app.Version = "1.9.9"
app.Version = "1.9.10"
// 1. Core modules: app, api/ws, streams

View File

@@ -118,3 +118,17 @@ func TestName(t *testing.T) {
// stage3
_ = prod2.Stop()
}
func TestStripUserinfo(t *testing.T) {
s := `streams:
test:
- ffmpeg:rtsp://username:password@10.1.2.3:554/stream1
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
`
s = StripUserinfo(s)
require.Equal(t, `streams:
test:
- ffmpeg:rtsp://***@10.1.2.3:554/stream1
- ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy
`, s)
}

View File

@@ -2,6 +2,7 @@ package core
import (
"crypto/rand"
"regexp"
"runtime"
"strconv"
"strings"
@@ -77,3 +78,14 @@ func Caller() string {
_, file, line, _ := runtime.Caller(1)
return file + ":" + strconv.Itoa(line)
}
const (
unreserved = `A-Za-z0-9-._~`
subdelims = `!$&'()*+,;=`
userinfo = unreserved + subdelims + `%:`
)
func StripUserinfo(s string) string {
sanitizer := regexp.MustCompile(`://[` + userinfo + `]+@`)
return sanitizer.ReplaceAllString(s, `://***@`)
}

View File

@@ -16,6 +16,11 @@ func RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {
ps := JoinNALU(sps, pps)
return func(packet *rtp.Packet) {
// this can happen for FLV from FFmpeg
if NALUType(packet.Payload) == NALUTypeSEI {
size := int(binary.BigEndian.Uint32(packet.Payload)) + 4
packet.Payload = packet.Payload[size:]
}
if NALUType(packet.Payload) == NALUTypeIFrame {
packet.Payload = Join(ps, packet.Payload)
}

View File

@@ -12,7 +12,7 @@ func NewAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {
hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),
ServiceCameraRTPStreamManagement(),
//hap.ServiceHAPProtocolInformation(),
//ServiceMicrophone(),
ServiceMicrophone(),
},
}
acc.InitIID()
@@ -30,17 +30,17 @@ func ServiceMicrophone() *hap.Service {
Perms: hap.EVPRPW,
//Descr: "Mute",
},
{
Type: "119",
Format: hap.FormatUInt8,
Value: 100,
Perms: hap.EVPRPW,
//Descr: "Volume",
//Unit: hap.UnitPercentage,
//MinValue: 0,
//MaxValue: 100,
//MinStep: 1,
},
//{
// Type: "119",
// Format: hap.FormatUInt8,
// Value: 100,
// Perms: hap.EVPRPW,
// //Descr: "Volume",
// //Unit: hap.UnitPercentage,
// //MinValue: 0,
// //MaxValue: 100,
// //MinStep: 1,
//},
},
}
}
@@ -62,7 +62,7 @@ func ServiceCameraRTPStreamManagement() *hap.Service {
VideoAttrs: []VideoAttrs{
{Width: 1920, Height: 1080, Framerate: 30},
{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones
{Width: 320, Height: 240, Framerate: 15}, // apple watch
{Width: 320, Height: 240, Framerate: 15}, // apple watch
},
},
},

View File

@@ -71,11 +71,17 @@ type JSONCharacter struct {
Event any `json:"ev,omitempty"`
}
// 4.2.1.2 Invalid Setup Codes
const insecurePINs = "00000000 11111111 22222222 33333333 44444444 55555555 66666666 77777777 88888888 99999999 12345678 87654321"
func SanitizePin(pin string) (string, error) {
s := strings.ReplaceAll(pin, "-", "")
if len(s) != 8 {
return "", errors.New("hap: wrong PIN format: " + pin)
}
if strings.Contains(insecurePINs, s) {
return "", errors.New("hap: insecure PIN: " + pin)
}
// 123-45-678
return s[:3] + "-" + s[3:5] + "-" + s[5:], nil
}

View File

@@ -46,6 +46,8 @@ func Marshal(v any) ([]byte, error) {
}
switch kind {
case reflect.Slice:
return appendSlice(nil, value)
case reflect.Struct:
return appendStruct(nil, value)
}
@@ -53,6 +55,23 @@ func Marshal(v any) ([]byte, error) {
return nil, errors.New("tlv8: not implemented: " + kind.String())
}
// separator the most confusing meaning in the documentation.
// It can have a value of 0x00 or 0xFF or even 0x05.
const separator = 0xFF
func appendSlice(b []byte, value reflect.Value) ([]byte, error) {
for i := 0; i < value.Len(); i++ {
if i > 0 {
b = append(b, separator, 0)
}
var err error
if b, err = appendStruct(b, value.Index(i)); err != nil {
return nil, err
}
}
return b, nil
}
func appendStruct(b []byte, value reflect.Value) ([]byte, error) {
valueType := value.Type()
@@ -121,7 +140,7 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
case reflect.Slice:
for i := 0; i < value.Len(); i++ {
if i > 0 {
b = append(b, 0, 0)
b = append(b, separator, 0)
}
if b, err = appendValue(b, tag, value.Index(i)); err != nil {
return nil, err
@@ -179,64 +198,86 @@ func Unmarshal(data []byte, v any) error {
kind = value.Kind()
}
if kind != reflect.Struct {
return errors.New("tlv8: not implemented: " + kind.String())
switch kind {
case reflect.Slice:
return unmarshalSlice(data, value)
case reflect.Struct:
return unmarshalStruct(data, value)
}
return unmarshalStruct(data, value)
return errors.New("tlv8: not implemented: " + kind.String())
}
func unmarshalStruct(b []byte, value reflect.Value) error {
var waitSlice bool
// unmarshalTLV can return two types of errors:
// - critical and then the value of []byte will be nil
// - not critical and then []byte will contain the value
func unmarshalTLV(b []byte, value reflect.Value) ([]byte, error) {
if len(b) < 2 {
return nil, errors.New("tlv8: wrong size: " + value.Type().Name())
}
for len(b) >= 2 {
t := b[0]
l := int(b[1])
t := b[0]
l := int(b[1])
// array item divider
if t == 0 && l == 0 {
b = b[2:]
waitSlice = true
continue
// array item divider (t == 0x00 || t == 0xFF)
if l == 0 {
return b[2:], errors.New("tlv8: zero item")
}
var v []byte
for {
if len(b) < 2+l {
return nil, errors.New("tlv8: wrong size: " + value.Type().Name())
}
var v []byte
v = append(v, b[2:2+l]...)
b = b[2+l:]
for {
if len(b) < 2+l {
return errors.New("tlv8: wrong size: " + value.Type().Name())
// if size == 255 and same tag - continue read big payload
if l < 255 || len(b) < 2 || b[0] != t {
break
}
l = int(b[1])
}
tag := strconv.Itoa(int(t))
valueField, ok := getStructField(value, tag)
if !ok {
return b, fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
}
if err := unmarshalValue(v, valueField); err != nil {
return nil, err
}
return b, nil
}
func unmarshalSlice(b []byte, value reflect.Value) error {
valueIndex := value.Index(growSlice(value))
for len(b) > 0 {
var err error
if b, err = unmarshalTLV(b, valueIndex); err != nil {
if b != nil {
valueIndex = value.Index(growSlice(value))
continue
}
v = append(v, b[2:2+l]...)
b = b[2+l:]
// if size == 255 and same tag - continue read big payload
if l < 255 || len(b) < 2 || b[0] != t {
break
}
l = int(b[1])
}
tag := strconv.Itoa(int(t))
valueField, ok := getStructField(value, tag)
if !ok {
return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
}
if waitSlice {
if valueField.Kind() != reflect.Slice {
return fmt.Errorf("tlv8: should be slice T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
}
waitSlice = false
}
if err := unmarshalValue(v, valueField); err != nil {
return err
}
}
return nil
}
func unmarshalStruct(b []byte, value reflect.Value) error {
for len(b) > 0 {
var err error
if b, err = unmarshalTLV(b, value); b == nil && err != nil {
return err
}
}
return nil
}

View File

@@ -2,6 +2,7 @@ package tlv8
import (
"encoding/hex"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -107,3 +108,49 @@ func TestInterface(t *testing.T) {
require.Equal(t, src, dst)
}
func TestSlice1(t *testing.T) {
var v struct {
VideoAttrs []struct {
Width uint16 `tlv8:"1"`
Height uint16 `tlv8:"2"`
Framerate uint8 `tlv8:"3"`
} `tlv8:"3"`
}
s := `030b010280070202380403011e ff00 030b010200050202d00203011e`
b1, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
require.NoError(t, err)
err = Unmarshal(b1, &v)
require.NoError(t, err)
require.Len(t, v.VideoAttrs, 2)
b2, err := Marshal(v)
require.NoError(t, err)
require.Equal(t, b1, b2)
}
func TestSlice2(t *testing.T) {
var v []struct {
Width uint16 `tlv8:"1"`
Height uint16 `tlv8:"2"`
Framerate uint8 `tlv8:"3"`
}
s := `010280070202380403011e ff00 010200050202d00203011e`
b1, err := hex.DecodeString(strings.ReplaceAll(s, " ", ""))
require.NoError(t, err)
err = Unmarshal(b1, &v)
require.NoError(t, err)
require.Len(t, v, 2)
b2, err := Marshal(v)
require.NoError(t, err)
require.Equal(t, b1, b2)
}

View File

@@ -116,20 +116,39 @@ func findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) strin
// urlParse fix bugs:
// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/
// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/
// 3. Content-Base: 192.168.253.220:1935/
func urlParse(rawURL string) (*url.URL, error) {
// fix https://github.com/AlexxIT/go2rtc/issues/830
if strings.HasPrefix(rawURL, "rtsp://rtsp://") {
rawURL = rawURL[7:]
}
// fix https://github.com/AlexxIT/go2rtc/issues/1852
if !strings.Contains(rawURL, "://") {
rawURL = "rtsp://" + rawURL
}
u, err := url.Parse(rawURL)
if err != nil && strings.HasSuffix(err.Error(), "after host") {
if i1 := strings.Index(rawURL, "://"); i1 > 0 {
if i2 := strings.IndexByte(rawURL[i1+3:], '/'); i2 > 0 {
return urlParse(rawURL[:i1+3+i2] + ":" + rawURL[i1+3+i2:])
}
if i := indexN(rawURL, '/', 3); i > 0 {
return urlParse(rawURL[:i] + ":" + rawURL[i:])
}
}
return u, err
}
func indexN(s string, c byte, n int) int {
var offset int
for {
i := strings.IndexByte(s[offset:], c)
if i < 0 {
break
}
if n--; n == 0 {
return offset + i
}
offset += i + 1
}
return -1
}

View File

@@ -11,14 +11,20 @@ func TestURLParse(t *testing.T) {
// https://github.com/AlexxIT/WebRTC/issues/395
base := "rtsp://::ffff:192.168.1.123/onvif/profile.1/"
u, err := urlParse(base)
assert.Empty(t, err)
assert.NoError(t, err)
assert.Equal(t, "::ffff:192.168.1.123:", u.Host)
// https://github.com/AlexxIT/go2rtc/issues/208
base = "rtsp://rtsp://turret2-cam.lan:554/stream1/"
u, err = urlParse(base)
assert.Empty(t, err)
assert.NoError(t, err)
assert.Equal(t, "turret2-cam.lan:554", u.Host)
// https://github.com/AlexxIT/go2rtc/issues/1852
base = "192.168.253.220:1935/"
u, err = urlParse(base)
assert.NoError(t, err)
assert.Equal(t, "192.168.253.220:1935", u.Host)
}
func TestBugSDP1(t *testing.T) {

View File

@@ -125,13 +125,20 @@ func NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error
networks = append(networks, ice.NetworkType(ntype))
}
udpMux, _ = ice.NewMultiUDPMuxFromPort(
var err error
if udpMux, err = ice.NewMultiUDPMuxFromPort(
port,
ice.UDPMuxFromPortWithInterfaceFilter(interfaceFilter),
ice.UDPMuxFromPortWithIPFilter(ipFilter),
ice.UDPMuxFromPortWithNetworks(networks...),
)
} else if ln, err := net.ListenPacket("udp", address); err == nil {
); err != nil {
return nil, err
}
} else {
ln, err := net.ListenPacket("udp", address)
if err != nil {
return nil, err
}
udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})
}
s.SetICEUDPMux(udpMux)

View File

@@ -1,5 +1,7 @@
## Versions
**PS.** Unfortunately, due to the dependency on `pion/webrtc/v4 v4.1.3`, had to upgrade go to `1.23`. Everything described below is not relevant.
[Go 1.20](https://go.dev/doc/go1.20) is last version with support Windows 7 and macOS 10.13.
Go 1.21 support only Windows 10 and macOS 10.15.
@@ -16,8 +18,6 @@ golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.24.0 // indirect
```
**PS.** Unfortunately, due to the dependency on `pion/webrtc/v4 v4.1.3`, had to upgrade go to `1.23`.
## Build
- UPX-3.96 pack broken bin for `linux_mipsel`