diff --git a/Makefile b/Makefile index cf124bbf0cb..1a7d878b771 100644 --- a/Makefile +++ b/Makefile @@ -8,12 +8,12 @@ help: @echo "" @echo "available actions:" @echo "" - @echo " mod-tidy run go mod tidy" - @echo " format format source files" - @echo " test run available tests" - @echo " run run app" - @echo " release build release assets" - @echo " travis-setup setup travis CI" + @echo " mod-tidy run go mod tidy" + @echo " format format source files" + @echo " test run available tests" + @echo " run ARGS=args run app" + @echo " release build release assets" + @echo " travis-setup setup travis CI" @echo "" mod-tidy: @@ -64,7 +64,7 @@ run: --network=host \ --name temp \ temp \ - /out + /out $(ARGS) define DOCKERFILE_RELEASE FROM $(BASE_IMAGE) diff --git a/README.md b/README.md index b07d2ad21c7..9ba1d13f0c7 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Features: * Publish multiple streams at once, each in a separate path, that can be read by multiple users * Each stream can have multiple video and audio tracks * Supports the RTP/RTCP streaming protocol +* Optional authentication schema for publishers * Compatible with Linux and Windows, does not require any dependency or interpreter, it's a single executable @@ -27,6 +28,8 @@ Precompiled binaries are available in the [release](https://github.com/aler9/rts ## Usage +#### Basic usage + 1. Start the server: ``` ./rtsp-simple-server @@ -47,14 +50,24 @@ Precompiled binaries are available in the [release](https://github.com/aler9/rts gst-launch-1.0 -v rtspsrc location=rtsp://localhost:8554/mystream ! rtph264depay ! decodebin ! autovideosink ``` -
+#### Publisher authentication -## Full command-line usage +1. Start the server and set a publish key: + ``` + ./rtsp-simple-server --publish-key=IU23yyfaw6324 + ``` + + 2. Only publishers which have the key will be able to publish: + ``` + ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?key=IU23yyfaw6324 + ``` + +#### Full command-line usage ``` usage: rtsp-simple-server [] -rtsp-simple-server +rtsp-simple-server v0.0.0 RTSP server. @@ -64,8 +77,10 @@ Flags: --rtsp-port=8554 port of the RTSP TCP listener --rtp-port=8000 port of the RTP UDP listener --rtcp-port=8001 port of the RTCP UDP listener + --publish-key="" optional authentication key required to publish ``` +
## Links diff --git a/client.go b/client.go index 0223472f1d1..98688214215 100644 --- a/client.go +++ b/client.go @@ -19,6 +19,7 @@ var ( errTeardown = errors.New("teardown") errPlay = errors.New("play") errRecord = errors.New("record") + errWrongKey = errors.New("wrong key") ) func interleavedChannelToTrack(channel int) (int, trackFlow) { @@ -170,7 +171,7 @@ func (c *client) run() { return } - // TEARDOWN, close connection silently + // TEARDOWN: close connection silently case errTeardown: return @@ -258,7 +259,20 @@ func (c *client) run() { } } - // error: write and exit + // wrong key: reply with 401 and exit + case errWrongKey: + c.log("ERR: %s", err) + + c.rconn.WriteResponse(&rtsp.Response{ + StatusCode: 401, + Status: "Unauthorized", + Headers: map[string]string{ + "CSeq": req.Headers["CSeq"], + }, + }) + return + + // generic error: reply with code 400 and exit default: c.log("ERR: %s", err) @@ -287,30 +301,27 @@ func (c *client) handleRequest(req *rtsp.Request) (*rtsp.Response, error) { return nil, fmt.Errorf("cseq missing") } - path, err := func() (string, error) { - ur, err := url.Parse(req.Url) - if err != nil { - return "", fmt.Errorf("unable to parse path '%s'", req.Url) - } - path := ur.Path + ur, err := url.Parse(req.Url) + if err != nil { + return nil, fmt.Errorf("unable to parse path '%s'", req.Url) + } + + path := func() string { + ret := ur.Path // remove leading slash - if len(path) > 1 { - path = path[1:] + if len(ret) > 1 { + ret = ret[1:] } // strip any subpath - if n := strings.Index(path, "/"); n >= 0 { - path = path[:n] + if n := strings.Index(ret, "/"); n >= 0 { + ret = ret[:n] } - return path, nil + return ret }() - c.p.mutex.Lock() - c.path = path - c.p.mutex.Unlock() - switch req.Method { case "OPTIONS": // do not check state, since OPTIONS can be requested @@ -397,6 +408,22 @@ func (c *client) handleRequest(req *rtsp.Request) (*rtsp.Response, error) { return nil, fmt.Errorf("invalid SDP: %s", err) } + if c.p.publishKey != "" { + q, err := url.ParseQuery(ur.RawQuery) + if err != nil { + return nil, fmt.Errorf("unable to parse query") + } + + key, ok := q["key"] + if !ok || len(key) == 0 { + return nil, fmt.Errorf("key missing") + } + + if key[0] != c.p.publishKey { + return nil, errWrongKey + } + } + err = func() error { c.p.mutex.Lock() defer c.p.mutex.Unlock() diff --git a/main.go b/main.go index e4c1ee7d9b3..e2d6ea40c4f 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "log" "net" "os" + "regexp" "sync" "gopkg.in/alecthomas/kingpin.v2" @@ -42,6 +43,7 @@ type program struct { rtspPort int rtpPort int rtcpPort int + publishKey string mutex sync.RWMutex rtspl *rtspListener rtpl *udpListener @@ -50,11 +52,20 @@ type program struct { publishers map[string]*client } -func newProgram(rtspPort int, rtpPort int, rtcpPort int) (*program, error) { +func newProgram(rtspPort int, rtpPort int, rtcpPort int, publishKey string) (*program, error) { + if publishKey != "" { + if !regexp.MustCompile("^[a-zA-Z0-9]+$").MatchString(publishKey) { + return nil, fmt.Errorf("publish key must be alphanumeric") + } + } + + log.Printf("rtsp-simple-server %s", Version) + p := &program{ rtspPort: rtspPort, rtpPort: rtpPort, rtcpPort: rtcpPort, + publishKey: publishKey, clients: make(map[*client]struct{}), publishers: make(map[string]*client), } @@ -120,6 +131,7 @@ func main() { rtspPort := kingpin.Flag("rtsp-port", "port of the RTSP TCP listener").Default("8554").Int() rtpPort := kingpin.Flag("rtp-port", "port of the RTP UDP listener").Default("8000").Int() rtcpPort := kingpin.Flag("rtcp-port", "port of the RTCP UDP listener").Default("8001").Int() + publishKey := kingpin.Flag("publish-key", "optional authentication key required to publish").Default("").String() kingpin.Parse() @@ -128,7 +140,7 @@ func main() { os.Exit(0) } - p, err := newProgram(*rtspPort, *rtpPort, *rtcpPort) + p, err := newProgram(*rtspPort, *rtpPort, *rtcpPort, *publishKey) if err != nil { log.Fatal("ERR: ", err) }