Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions internal/api/admin_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/require"
)

func TestAdminAuthAlwaysRequired(t *testing.T) {
for _, endpoint := range []string{"config", "restart", "exit"} {
for _, method := range []string{"GET", "POST", "PATCH", "OPTIONS"} {
for _, remote := range []string{"127.0.0.1:1234", "[::1]:1234", "192.0.2.1:1234", "@"} {
for _, credentials := range []struct {
user, pass string
want int
}{
{"", "", 401}, {"admin", "wrong", 401}, {"admin", "secret", 204},
} {
handler := requireAdmin("admin", "secret", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(204) })
req := httptest.NewRequest(method, "/go2rtc/api/"+endpoint, nil)
req.RemoteAddr = remote
if credentials.user != "" {
req.SetBasicAuth(credentials.user, credentials.pass)
}
res := httptest.NewRecorder()
handler(res, req)
require.Equal(t, credentials.want, res.Code, "%s %s %s", endpoint, method, remote)
}
}
}
}
}

func TestAdminDisabledWithoutCredentials(t *testing.T) {
for _, credentials := range [][2]string{{"", ""}, {"admin", ""}, {"", "secret"}} {
handler := requireAdmin(credentials[0], credentials[1], func(http.ResponseWriter, *http.Request) { t.Fatal("admin handler reached") })
req := httptest.NewRequest("PATCH", "/api/config", nil)
req.SetBasicAuth(credentials[0], credentials[1])
res := httptest.NewRecorder()
handler(res, req)
require.Equal(t, 403, res.Code)
}
}
26 changes: 22 additions & 4 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Init() {
}

// default config
cfg.Mod.Listen = ":1984"
cfg.Mod.Listen = "127.0.0.1:1984"

// load config from YAML
app.LoadConfig(&cfg)
Expand All @@ -58,9 +58,9 @@ func Init() {
initStatic(cfg.Mod.StaticDir)

HandleFunc("api", apiHandler)
HandleFunc("api/config", configHandler)
HandleFunc("api/exit", exitHandler)
HandleFunc("api/restart", restartHandler)
HandleFunc("api/config", requireAdmin(cfg.Mod.Username, cfg.Mod.Password, configHandler))
HandleFunc("api/exit", requireAdmin(cfg.Mod.Username, cfg.Mod.Password, exitHandler))
HandleFunc("api/restart", requireAdmin(cfg.Mod.Username, cfg.Mod.Password, restartHandler))
HandleFunc("api/log", logHandler)

Handler = http.DefaultServeMux // 4th
Expand Down Expand Up @@ -357,3 +357,21 @@ func Error(w http.ResponseWriter, err error) {

http.Error(w, err.Error(), http.StatusInsufficientStorage)
}

// Administrative endpoints never inherit the loopback/OPTIONS auth exemption.
// With no credentials configured, they are disabled rather than public.
func requireAdmin(username, password string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if username == "" || password == "" {
http.Error(w, "Administrative API disabled: configure credentials", http.StatusForbidden)
return
}
user, pass, ok := r.BasicAuth()
if !ok || user != username || pass != password {
w.Header().Set("WWW-Authenticate", `Basic realm="go2rtc"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
34 changes: 23 additions & 11 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,33 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
query := streams.ParseQuery(rawQuery)

var path string
return newProducer(rawURL, shell.QuoteSplit(rawURL[5:]), query)
}

// RTSP flow should have `{output}` inside URL
// pipe flow may have `#{params}` inside URL
if i := strings.Index(rawURL, "{output}"); i > 0 {
// NewProducer runs an argument vector built by a trusted module. It is not a
// stream scheme and cannot be reached through an exec: redirect.
func NewProducer(source string, args []string) (core.Producer, error) {
return newProducer(source, args, nil)
}

func newProducer(source string, args []string, query url.Values) (prod core.Producer, err error) {
if len(args) == 0 {
return nil, errors.New("exec: empty command")
}
args = slices.Clone(args)
var path string
for i, arg := range args {
if arg != "{output}" {
continue
}
if rtsp.Port == "" {
return nil, errors.New("exec: rtsp module disabled")
}

sum := md5.Sum([]byte(rawURL))
sum := md5.Sum([]byte(source))
path = "/" + hex.EncodeToString(sum[:])
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
}

cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
cmd := shell.NewCommandArgs(args)
cmd.Stderr = &logWriter{
buf: make([]byte, 512),
debug: log.Debug().Enabled(),
Expand Down Expand Up @@ -116,9 +128,9 @@ func execHandle(rawURL string) (prod core.Producer, err error) {
}

if path == "" {
prod, err = handlePipe(rawURL, cmd)
prod, err = handlePipe(source, cmd)
} else {
prod, err = handleRTSP(rawURL, cmd, path, timeout)
prod, err = handleRTSP(source, cmd, path, timeout)
}

if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/ffmpeg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ ffmpeg:
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
- You can use `rotate` param with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
- You can use `framerate` with HTTP JPEG/MJPEG inputs that don't provide timestamps (ex. `#framerate=5#video=h264`)
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
- This will greatly increase the CPU of the server, even with hardware acceleration
- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)
Expand Down
Loading