diff --git a/README.md b/README.md index 8cd24f63..622898d1 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ The configuration can be changed dinamically when the server is running (hot rel ### Encryption -Incoming and outgoing streams can be encrypted with TLS (obtaining the RTSPS protocol). A TLS certificate must be installed on the server; if the server is installed on a machine that is publicly accessible from the internet, a certificate can be requested from a Certificate authority by using tools like [Certbot](https://certbot.eff.org/); otherwise, a self-signed certificate can be generated with openSSL: +Incoming and outgoing streams can be encrypted with TLS (obtaining the RTSPS protocol). A self-signed TLS certificate is needed and can be generated with openSSL: ``` openssl genrsa -out server.key 2048 @@ -171,7 +171,7 @@ Streams can then be published and read with the `rtsps` scheme and the `8555` po ffmpeg -i rtsps://ip:8555/... ``` -If the client is _GStreamer_ and the server certificate is self signed, remember to disable the certificate validation: +If the client is _GStreamer_, disable the certificate validation: ``` gst-launch-1.0 rtspsrc location=rtsps://ip:8555/... tls-validation-flags=0 diff --git a/internal/conf/path.go b/internal/conf/path.go index 880f3800..3de47c1f 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -66,34 +66,41 @@ func CheckPathName(name string) error { // PathConf is a path configuration. type PathConf struct { - Regexp *regexp.Regexp `yaml:"-" json:"-"` + Regexp *regexp.Regexp `yaml:"-" json:"-"` + + // source Source string `yaml:"source"` SourceProtocol string `yaml:"sourceProtocol"` SourceProtocolParsed *gortsplib.StreamProtocol `yaml:"-" json:"-"` + SourceFingerprint string `yaml:"sourceFingerprint" json:"sourceFingerprint"` SourceOnDemand bool `yaml:"sourceOnDemand"` SourceOnDemandStartTimeout time.Duration `yaml:"sourceOnDemandStartTimeout"` SourceOnDemandCloseAfter time.Duration `yaml:"sourceOnDemandCloseAfter"` SourceRedirect string `yaml:"sourceRedirect"` DisablePublisherOverride bool `yaml:"disablePublisherOverride"` Fallback string `yaml:"fallback"` - RunOnInit string `yaml:"runOnInit"` - RunOnInitRestart bool `yaml:"runOnInitRestart"` - RunOnDemand string `yaml:"runOnDemand"` - RunOnDemandRestart bool `yaml:"runOnDemandRestart"` - RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"` - RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"` - RunOnPublish string `yaml:"runOnPublish"` - RunOnPublishRestart bool `yaml:"runOnPublishRestart"` - RunOnRead string `yaml:"runOnRead"` - RunOnReadRestart bool `yaml:"runOnReadRestart"` - PublishUser string `yaml:"publishUser"` - PublishPass string `yaml:"publishPass"` - PublishIps []string `yaml:"publishIps"` - PublishIpsParsed []interface{} `yaml:"-" json:"-"` - ReadUser string `yaml:"readUser"` - ReadPass string `yaml:"readPass"` - ReadIps []string `yaml:"readIps"` - ReadIpsParsed []interface{} `yaml:"-" json:"-"` + + // custom commands + RunOnInit string `yaml:"runOnInit"` + RunOnInitRestart bool `yaml:"runOnInitRestart"` + RunOnDemand string `yaml:"runOnDemand"` + RunOnDemandRestart bool `yaml:"runOnDemandRestart"` + RunOnDemandStartTimeout time.Duration `yaml:"runOnDemandStartTimeout"` + RunOnDemandCloseAfter time.Duration `yaml:"runOnDemandCloseAfter"` + RunOnPublish string `yaml:"runOnPublish"` + RunOnPublishRestart bool `yaml:"runOnPublishRestart"` + RunOnRead string `yaml:"runOnRead"` + RunOnReadRestart bool `yaml:"runOnReadRestart"` + + // authentication + PublishUser string `yaml:"publishUser"` + PublishPass string `yaml:"publishPass"` + PublishIps []string `yaml:"publishIps"` + PublishIpsParsed []interface{} `yaml:"-" json:"-"` + ReadUser string `yaml:"readUser"` + ReadPass string `yaml:"readPass"` + ReadIps []string `yaml:"readIps"` + ReadIpsParsed []interface{} `yaml:"-" json:"-"` } func (pconf *PathConf) fillAndCheck(name string) error { @@ -163,6 +170,10 @@ func (pconf *PathConf) fillAndCheck(name string) error { return fmt.Errorf("unsupported protocol '%s'", pconf.SourceProtocol) } + if strings.HasPrefix(pconf.Source, "rtsps://") && pconf.SourceFingerprint == "" { + return fmt.Errorf("sourceFingerprint is required with a RTSPS URL") + } + case strings.HasPrefix(pconf.Source, "rtmp://"): if pconf.Regexp != nil { return fmt.Errorf("a path with a regular expression (or path 'all') cannot have a RTMP source; use another path") diff --git a/internal/path/path.go b/internal/path/path.go index 53c33c98..b1ad2a5f 100644 --- a/internal/path/path.go +++ b/internal/path/path.go @@ -406,6 +406,7 @@ func (pa *Path) startExternalSource() { pa.source = sourcertsp.New( pa.conf.Source, pa.conf.SourceProtocolParsed, + pa.conf.SourceFingerprint, pa.readTimeout, pa.writeTimeout, pa.readBufferCount, diff --git a/internal/sourcertsp/source.go b/internal/sourcertsp/source.go index 70148c31..d27fadc4 100644 --- a/internal/sourcertsp/source.go +++ b/internal/sourcertsp/source.go @@ -1,6 +1,11 @@ package sourcertsp import ( + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "fmt" + "strings" "sync" "sync/atomic" "time" @@ -28,6 +33,7 @@ type Parent interface { type Source struct { ur string proto *gortsplib.StreamProtocol + fingerprint string readTimeout time.Duration writeTimeout time.Duration readBufferCount int @@ -41,8 +47,10 @@ type Source struct { } // New allocates a Source. -func New(ur string, +func New( + ur string, proto *gortsplib.StreamProtocol, + fingerprint string, readTimeout time.Duration, writeTimeout time.Duration, readBufferCount int, @@ -53,6 +61,7 @@ func New(ur string, s := &Source{ ur: ur, proto: proto, + fingerprint: fingerprint, readTimeout: readTimeout, writeTimeout: writeTimeout, readBufferCount: readBufferCount, @@ -121,7 +130,23 @@ func (s *Source) runInner() bool { defer close(dialDone) conf := gortsplib.ClientConf{ - StreamProtocol: s.proto, + StreamProtocol: s.proto, + TLSConfig: &tls.Config{ + InsecureSkipVerify: true, + VerifyConnection: func(cs tls.ConnectionState) error { + h := sha256.New() + h.Write(cs.PeerCertificates[0].Raw) + hstr := hex.EncodeToString(h.Sum(nil)) + fingerprintLower := strings.ToLower(s.fingerprint) + + if hstr != fingerprintLower { + return fmt.Errorf("server fingerprint do not match: expected %s, got %s", + fingerprintLower, hstr) + } + + return nil + }, + }, ReadTimeout: s.readTimeout, WriteTimeout: s.writeTimeout, ReadBufferCount: s.readBufferCount, diff --git a/main_sourcertsp_test.go b/main_sourcertsp_test.go index 274cf93d..4aa1dc6a 100644 --- a/main_sourcertsp_test.go +++ b/main_sourcertsp_test.go @@ -98,6 +98,7 @@ func TestSourceRTSP(t *testing.T) { "paths:\n" + " proxied:\n" + " source: rtsps://testuser:testpass@localhost:8555/teststream\n" + + " sourceFingerprint: 33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\n" + " sourceOnDemand: yes\n") require.Equal(t, true, ok) defer p2.close() diff --git a/rtsp-simple-server.yml b/rtsp-simple-server.yml index 2e6e5b07..6f493b70 100644 --- a/rtsp-simple-server.yml +++ b/rtsp-simple-server.yml @@ -60,6 +60,9 @@ rtpPort: 8000 # port of the UDP/RTCP listener. This is used only if "udp" is in protocols. rtcpPort: 8001 # path to the server key. This is used only if encryption is "strict" or "optional". +# this can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 serverKey: server.key # path to the server certificate. This is used only if encryption is "strict" or "optional". serverCert: server.crt @@ -92,16 +95,23 @@ paths: # source of the stream - this can be: # * record -> the stream is published by a RTSP or RTMP client # * rtsp://existing-url -> the stream is pulled from another RTSP server - # * rtsps://existing-url -> the stream is pulled from another RTSP server + # * rtsps://existing-url -> the stream is pulled from another RTSP server, with RTSPS # * rtmp://existing-url -> the stream is pulled from a RTMP server # * redirect -> the stream is provided by another path or server source: record - # if the source is an RTSP URL, this is the protocol that will be used to + # if the source is an RTSP or RTSPS URL, this is the protocol that will be used to # pull the stream. available options are "automatic", "udp", "tcp". # the tcp protocol can help to overcome the error "no UDP packets received recently". sourceProtocol: automatic + # if the source is an RTSPS URL, the fingerprint of the certificate of the source + # must be provided in order to prevent man-in-the-middle attacks. + # it can be obtained from the source by running: + # openssl s_client -connect source_ip:source_port /dev/null | sed -n '/BEGIN/,/END/p' > server.crt + # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':' + sourceFingerprint: + # if the source is an RTSP or RTMP URL, it will be pulled only when at least # one reader is connected, saving bandwidth. sourceOnDemand: no