mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 08:57:48 +02:00
Compare commits
2 Commits
f0f1f7985c
...
4bf70700f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bf70700f2 | ||
|
|
18f7a4bbc0 |
@@ -931,9 +931,6 @@ func (sm *SmartManager) parseSmartForSata(output []byte, deviceType string) (boo
|
||||
if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok {
|
||||
rawValue = parsed
|
||||
}
|
||||
if smartData.SmartStatus == "PASSED" && rawValue > 0 && (attr.ID == 5 || attr.ID == 197 || attr.ID == 198) {
|
||||
smartData.SmartStatus = "WARNING"
|
||||
}
|
||||
smartAttr := &smart.SmartAttribute{
|
||||
ID: attr.ID,
|
||||
Name: attr.Name,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/henrygd/beszel/internal/entities/smart"
|
||||
@@ -90,27 +89,6 @@ func TestParseSmartForSata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSmartForSataWarnsForCriticalAttributes(t *testing.T) {
|
||||
for _, attrID := range []int{5, 197, 198} {
|
||||
t.Run("attribute "+strconv.Itoa(attrID), func(t *testing.T) {
|
||||
jsonPayload := []byte(fmt.Sprintf(`{
|
||||
"smartctl": {"exit_status": 0},
|
||||
"device": {"name": "/dev/sda", "type": "sat"},
|
||||
"model_name": "Example",
|
||||
"serial_number": "WARNING%d",
|
||||
"smart_status": {"passed": true},
|
||||
"temperature": {"current": 30},
|
||||
"ata_smart_attributes": {"table": [{"id": %d, "raw": {"value": 1, "string": "1"}}]}
|
||||
}`, attrID, attrID))
|
||||
|
||||
sm := &SmartManager{SmartDataMap: make(map[string]*smart.SmartData)}
|
||||
hasData, _ := sm.parseSmartForSata(jsonPayload, "")
|
||||
require.True(t, hasData)
|
||||
assert.Equal(t, "WARNING", sm.SmartDataMap[fmt.Sprintf("WARNING%d", attrID)].SmartStatus)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSmartForSataPreservesFailedAndUnknownStatus(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
|
||||
@@ -2,7 +2,11 @@ package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -78,12 +82,81 @@ func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
||||
}
|
||||
// authenticate with trusted header
|
||||
if trustedHeader, _ := utils.GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
||||
// only honor the header from these peers, if set
|
||||
trustedProxies, restricted := parseTrustedProxies()
|
||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||
if restricted && !isTrustedProxy(trustedProxies, e.Request.RemoteAddr) {
|
||||
return e.Next()
|
||||
}
|
||||
return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parseTrustedProxies reads TRUSTED_PROXY_IPS (comma-separated IPs or CIDRs).
|
||||
// restricted is false when the variable is unset or empty, meaning the trusted
|
||||
// header is accepted from any peer. Invalid entries are skipped with a warning,
|
||||
// so a typo narrows the allowlist rather than widening it.
|
||||
func parseTrustedProxies() (prefixes []netip.Prefix, restricted bool) {
|
||||
value, _ := utils.GetEnv("TRUSTED_PROXY_IPS")
|
||||
if value == "" {
|
||||
return nil, false
|
||||
}
|
||||
for entry := range strings.SplitSeq(value, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
if prefix, err := parseProxyPrefix(entry); err == nil {
|
||||
prefixes = append(prefixes, prefix)
|
||||
} else {
|
||||
slog.Warn("Ignoring invalid TRUSTED_PROXY_IPS entry", "entry", entry)
|
||||
}
|
||||
}
|
||||
return prefixes, true
|
||||
}
|
||||
|
||||
// parseProxyPrefix parses an IP or CIDR into a masked prefix. IPv4-mapped IPv6
|
||||
// entries are converted to IPv4 so they match IPv4 peers.
|
||||
func parseProxyPrefix(entry string) (netip.Prefix, error) {
|
||||
prefix, err := netip.ParsePrefix(entry)
|
||||
if err != nil {
|
||||
addr, err := netip.ParseAddr(entry)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
return netip.PrefixFrom(addr, addr.BitLen()), nil
|
||||
}
|
||||
if prefix.Addr().Is4In6() {
|
||||
if prefix.Bits() < 96 {
|
||||
return netip.Prefix{}, fmt.Errorf("%s covers more than the IPv4-mapped range", entry)
|
||||
}
|
||||
prefix = netip.PrefixFrom(prefix.Addr().Unmap(), prefix.Bits()-96)
|
||||
}
|
||||
return prefix.Masked(), nil
|
||||
}
|
||||
|
||||
// isTrustedProxy reports whether the peer address of a request (host:port) is
|
||||
// within one of the prefixes.
|
||||
func isTrustedProxy(prefixes []netip.Prefix, remoteAddr string) bool {
|
||||
host, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
host = remoteAddr
|
||||
}
|
||||
addr, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
addr = addr.Unmap().WithZone("")
|
||||
for _, prefix := range prefixes {
|
||||
if prefix.Contains(addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// registerApiRoutes registers custom API routes
|
||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||
// auth protected routes
|
||||
|
||||
@@ -1107,6 +1107,79 @@ func TestTrustedHeaderMiddleware(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedHeaderProxyAllowlist(t *testing.T) {
|
||||
var hubs []*beszelTests.TestHub
|
||||
|
||||
defer func() {
|
||||
for _, hub := range hubs {
|
||||
hub.Cleanup()
|
||||
}
|
||||
}()
|
||||
|
||||
testAppFactory := func(t testing.TB) *pbTests.TestApp {
|
||||
hub, _ := beszelTests.NewTestHub(t.TempDir())
|
||||
hubs = append(hubs, hub)
|
||||
hub.StartHub()
|
||||
return hub.TestApp
|
||||
}
|
||||
|
||||
// httptest requests arrive from 192.0.2.1:1234
|
||||
testCases := []struct {
|
||||
name string
|
||||
proxies string
|
||||
expectedStatus int
|
||||
expectedContent []string
|
||||
}{
|
||||
{
|
||||
name: "peer inside an allowed range",
|
||||
proxies: "10.0.0.0/8, 192.0.2.0/24",
|
||||
expectedStatus: 200,
|
||||
expectedContent: []string{"\"key\":", "\"v\":"},
|
||||
},
|
||||
{
|
||||
name: "peer is the listed address",
|
||||
proxies: "192.0.2.1",
|
||||
expectedStatus: 200,
|
||||
expectedContent: []string{"\"key\":", "\"v\":"},
|
||||
},
|
||||
{
|
||||
name: "peer outside the allowlist",
|
||||
proxies: "10.0.0.0/8",
|
||||
expectedStatus: 401,
|
||||
expectedContent: []string{"requires valid"},
|
||||
},
|
||||
{
|
||||
name: "allowlist with no valid entry",
|
||||
proxies: "proxy.internal",
|
||||
expectedStatus: 401,
|
||||
expectedContent: []string{"requires valid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted")
|
||||
t.Setenv("TRUSTED_PROXY_IPS", tc.proxies)
|
||||
|
||||
scenario := beszelTests.ApiScenario{
|
||||
Name: "GET /getkey - with trusted header",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/beszel/getkey",
|
||||
Headers: map[string]string{
|
||||
"X-Beszel-Trusted": "user@test.com",
|
||||
},
|
||||
ExpectedStatus: tc.expectedStatus,
|
||||
ExpectedContent: tc.expectedContent,
|
||||
TestAppFactory: testAppFactory,
|
||||
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||
beszelTests.CreateUser(app, "user@test.com", "password123")
|
||||
},
|
||||
}
|
||||
scenario.Test(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateEndpoint(t *testing.T) {
|
||||
t.Setenv("CHECK_UPDATES", "true")
|
||||
|
||||
|
||||
127
internal/hub/trusted_proxy_test.go
Normal file
127
internal/hub/trusted_proxy_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
//go:build testing
|
||||
|
||||
package hub
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseTrustedProxies(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
value string
|
||||
prefixes []string
|
||||
restricted bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
value: "",
|
||||
restricted: false,
|
||||
},
|
||||
{
|
||||
name: "blank",
|
||||
value: " , ",
|
||||
prefixes: nil,
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
name: "single addresses become host prefixes",
|
||||
value: "10.0.0.5, 2001:db8::1",
|
||||
prefixes: []string{"10.0.0.5/32", "2001:db8::1/128"},
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
name: "cidrs are masked",
|
||||
value: "172.16.5.9/12,fd00::1/64",
|
||||
prefixes: []string{"172.16.0.0/12", "fd00::/64"},
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
name: "ipv4-mapped entries become ipv4",
|
||||
value: "::ffff:10.0.0.5, ::ffff:10.0.0.0/104",
|
||||
prefixes: []string{"10.0.0.5/32", "10.0.0.0/8"},
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
name: "invalid entries are skipped, valid ones kept",
|
||||
value: "proxy.internal, 10.0.0.0/8, 300.1.1.1, ::ffff:0.0.0.0/64",
|
||||
prefixes: []string{"10.0.0.0/8"},
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
name: "only invalid entries trust nobody",
|
||||
value: "proxy.internal",
|
||||
prefixes: nil,
|
||||
restricted: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("TRUSTED_PROXY_IPS", tc.value)
|
||||
prefixes, restricted := parseTrustedProxies()
|
||||
assert.Equal(t, tc.restricted, restricted)
|
||||
var got []string
|
||||
for _, p := range prefixes {
|
||||
got = append(got, p.String())
|
||||
}
|
||||
assert.Equal(t, tc.prefixes, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("unset", func(t *testing.T) {
|
||||
t.Setenv("TRUSTED_PROXY_IPS", "")
|
||||
os.Unsetenv("TRUSTED_PROXY_IPS")
|
||||
prefixes, restricted := parseTrustedProxies()
|
||||
assert.False(t, restricted)
|
||||
assert.Nil(t, prefixes)
|
||||
})
|
||||
|
||||
t.Run("prefixed env var takes precedence", func(t *testing.T) {
|
||||
t.Setenv("TRUSTED_PROXY_IPS", "10.0.0.0/8")
|
||||
t.Setenv("BESZEL_HUB_TRUSTED_PROXY_IPS", "192.168.0.0/16")
|
||||
prefixes, restricted := parseTrustedProxies()
|
||||
assert.True(t, restricted)
|
||||
require.Len(t, prefixes, 1)
|
||||
assert.Equal(t, "192.168.0.0/16", prefixes[0].String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsTrustedProxy(t *testing.T) {
|
||||
prefixes := []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/8"),
|
||||
netip.MustParsePrefix("2001:db8::/32"),
|
||||
netip.MustParsePrefix("fe80::/10"),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
trusted bool
|
||||
}{
|
||||
{"ipv4 in prefix", "10.20.30.40:51234", true},
|
||||
{"ipv4 outside prefix", "11.0.0.1:51234", false},
|
||||
{"ipv6 in prefix", "[2001:db8:1::2]:443", true},
|
||||
{"ipv6 outside prefix", "[2001:db9::1]:443", false},
|
||||
{"ipv4-mapped ipv6 matches ipv4 prefix", "[::ffff:10.1.2.3]:80", true},
|
||||
{"zone is ignored", "[fe80::1%eth0]:80", true},
|
||||
{"no port", "10.1.2.3", true},
|
||||
{"empty", "", false},
|
||||
{"garbage", "not-an-address:80", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.trusted, isTrustedProxy(prefixes, tc.remoteAddr))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("empty allowlist trusts nobody", func(t *testing.T) {
|
||||
assert.False(t, isTrustedProxy(nil, "10.0.0.1:1"))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user