mirror of
https://github.com/aziis98/statuspage.git
synced 2026-10-06 22:55:21 +00:00
security: FileServer static serving (path traversal fix), 5s refresh cooldown, dockerignore
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
node_modules
|
||||
bin
|
||||
dist
|
||||
data.volume
|
||||
*.local*
|
||||
assets
|
||||
@@ -42,7 +42,8 @@ statuspage
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go (recent, 1.22+ for `http.ServeMux` patterns).
|
||||
- Go 1.26+ (see the `go` directive in `go.mod`; the backend uses `http.ServeMux`
|
||||
method patterns, added in 1.22).
|
||||
- A C compiler for `mattn/go-sqlite3` (the `CGO_ENABLED=1` build).
|
||||
- `bun` (or `npm`) for the frontend build.
|
||||
|
||||
@@ -64,8 +65,12 @@ Then open `http://localhost:5000`.
|
||||
|
||||
### Docker
|
||||
|
||||
Clone the repo and set up your config inside it:
|
||||
|
||||
```sh
|
||||
cp config.local.yaml config.local.yaml # your config goes here
|
||||
git clone https://git.phc.dm.unipi.it/aziis98/statuspage
|
||||
cd statuspage
|
||||
cp example.config.yaml config.local.yaml # your config goes here
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
|
||||
+4
-19
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/pflag"
|
||||
@@ -32,22 +31,8 @@ func buildFrontend() error {
|
||||
return c.Run()
|
||||
}
|
||||
|
||||
func spaHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
rel := filepath.Clean(strings.TrimPrefix(r.URL.Path, "/"))
|
||||
if rel != "." && rel != "" {
|
||||
p := filepath.Join("dist", rel)
|
||||
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.ServeFile(w, r, filepath.Join("dist", "index.html"))
|
||||
}
|
||||
func spaHandler() http.Handler {
|
||||
return http.FileServer(http.Dir("dist"))
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -78,7 +63,7 @@ func main() {
|
||||
mux.HandleFunc("GET /api/metrics/{machine}", mm.metricsHandler)
|
||||
mux.HandleFunc("POST /api/refresh/{machine}", mm.refreshHandler)
|
||||
if !*devServer {
|
||||
mux.HandleFunc("GET /", spaHandler())
|
||||
mux.Handle("GET /", spaHandler())
|
||||
}
|
||||
log.Printf("listening on %s (mock)", *addr)
|
||||
if err := http.ListenAndServe(*addr, mux); err != nil {
|
||||
@@ -118,7 +103,7 @@ func main() {
|
||||
mux.HandleFunc("GET /api/metrics/{machine}", mon.metricsHandler)
|
||||
mux.HandleFunc("POST /api/refresh/{machine}", mon.refreshHandler)
|
||||
if !*devServer {
|
||||
mux.HandleFunc("GET /", spaHandler())
|
||||
mux.Handle("GET /", spaHandler())
|
||||
}
|
||||
|
||||
log.Printf("listening on %s", *addr)
|
||||
|
||||
@@ -31,6 +31,9 @@ const checkNA = "na"
|
||||
|
||||
const maxIPHistory = 10
|
||||
|
||||
// minimum interval between interactive refresh requests for the same machine
|
||||
const refreshCooldown = 5 * time.Second
|
||||
|
||||
type sshResultState struct {
|
||||
Ok bool `json:"ok"`
|
||||
Result string `json:"result"`
|
||||
@@ -125,6 +128,9 @@ type Monitor struct {
|
||||
|
||||
statesMu sync.RWMutex
|
||||
states map[string]*machineState
|
||||
|
||||
refreshMu sync.Mutex
|
||||
lastRefresh map[string]time.Time
|
||||
}
|
||||
|
||||
func NewMonitor(cfg *Config, history *History) *Monitor {
|
||||
@@ -136,6 +142,7 @@ func NewMonitor(cfg *Config, history *History) *Monitor {
|
||||
ctx: context.Background(),
|
||||
history: history,
|
||||
states: make(map[string]*machineState, len(cfg.Machines)),
|
||||
lastRefresh: make(map[string]time.Time, len(cfg.Machines)),
|
||||
}
|
||||
for _, mc := range cfg.Machines {
|
||||
m.states[mc.ID] = &machineState{status: statusUnknown, icmp: checkNA, tcp: checkNA}
|
||||
@@ -765,6 +772,17 @@ func (m *Monitor) refreshHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "machine not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
m.refreshMu.Lock()
|
||||
if now := time.Now(); now.Sub(m.lastRefresh[id]) < refreshCooldown {
|
||||
m.refreshMu.Unlock()
|
||||
http.Error(w, "refresh cooldown active", http.StatusTooManyRequests)
|
||||
return
|
||||
} else {
|
||||
m.lastRefresh[id] = now
|
||||
}
|
||||
m.refreshMu.Unlock()
|
||||
|
||||
log.Printf("interactive refresh scheduled for %s (%s)", mc.Name, mc.Host)
|
||||
m.Refresh(mc)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
|
||||
Reference in New Issue
Block a user