feat: add read-only mode to API and UI, disable write actions

This commit is contained in:
Sergey Krashevich
2026-02-01 04:13:43 +03:00
parent 6085c8aabe
commit cc453dd9ed
24 changed files with 438 additions and 8 deletions
+27
View File
@@ -32,6 +32,7 @@ func Init() {
TLSCert string `yaml:"tls_cert"`
TLSKey string `yaml:"tls_key"`
UnixListen string `yaml:"unix_listen"`
ReadOnly bool `yaml:"read_only"`
AllowPaths []string `yaml:"allow_paths"`
} `yaml:"api"`
@@ -50,6 +51,9 @@ func Init() {
allowPaths = cfg.Mod.AllowPaths
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
ReadOnly = cfg.Mod.ReadOnly
app.ConfigReadOnly = ReadOnly
app.Info["read_only"] = ReadOnly
initStatic(cfg.Mod.StaticDir)
@@ -149,6 +153,15 @@ const (
)
var Handler http.Handler
var ReadOnly bool
func IsReadOnly() bool {
return ReadOnly
}
func ReadOnlyError(w http.ResponseWriter) {
http.Error(w, "read-only", http.StatusForbidden)
}
// HandleFunc handle pattern with relative path:
// - "api/streams" => "{basepath}/api/streams"
@@ -249,6 +262,11 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
return
}
if IsReadOnly() {
ReadOnlyError(w)
return
}
s := r.URL.Query().Get("code")
code, err := strconv.Atoi(s)
@@ -267,6 +285,11 @@ func restartHandler(w http.ResponseWriter, r *http.Request) {
return
}
if IsReadOnly() {
ReadOnlyError(w)
return
}
path, err := os.Executable()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -285,6 +308,10 @@ func logHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/jsonlines")
_, _ = app.MemoryLog.WriteTo(w)
case "DELETE":
if IsReadOnly() {
ReadOnlyError(w)
return
}
app.MemoryLog.Reset()
Response(w, "OK", "text/plain")
default:
+4
View File
@@ -26,6 +26,10 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
Response(w, data, "application/yaml")
case "POST", "PATCH":
if IsReadOnly() {
ReadOnlyError(w)
return
}
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
+36
View File
@@ -0,0 +1,36 @@
package api
import (
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/stretchr/testify/require"
)
func TestConfigHandlerReadOnly(t *testing.T) {
prevPath := app.ConfigPath
prevReadOnly := ReadOnly
t.Cleanup(func() {
app.ConfigPath = prevPath
ReadOnly = prevReadOnly
})
app.ConfigPath = filepath.Join(t.TempDir(), "config.yaml")
ReadOnly = true
for _, method := range []string{"POST", "PATCH"} {
t.Run(method, func(t *testing.T) {
req := httptest.NewRequest(method, "/api/config", strings.NewReader("log:\n level: info\n"))
w := httptest.NewRecorder()
configHandler(w, req)
require.Equal(t, http.StatusForbidden, w.Code)
require.Contains(t, w.Body.String(), "read-only")
})
}
}