mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 17:07:47 +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 {
|
if parsed, ok := smart.ParseSmartRawValueString(attr.Raw.String); ok {
|
||||||
rawValue = parsed
|
rawValue = parsed
|
||||||
}
|
}
|
||||||
if smartData.SmartStatus == "PASSED" && rawValue > 0 && (attr.ID == 5 || attr.ID == 197 || attr.ID == 198) {
|
|
||||||
smartData.SmartStatus = "WARNING"
|
|
||||||
}
|
|
||||||
smartAttr := &smart.SmartAttribute{
|
smartAttr := &smart.SmartAttribute{
|
||||||
ID: attr.ID,
|
ID: attr.ID,
|
||||||
Name: attr.Name,
|
Name: attr.Name,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/entities/smart"
|
"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) {
|
func TestParseSmartForSataPreservesFailedAndUnknownStatus(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package hub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -78,12 +82,81 @@ func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
|||||||
}
|
}
|
||||||
// authenticate with trusted header
|
// authenticate with trusted header
|
||||||
if trustedHeader, _ := utils.GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
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 {
|
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))
|
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
|
// registerApiRoutes registers custom API routes
|
||||||
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
||||||
// auth protected routes
|
// 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) {
|
func TestUpdateEndpoint(t *testing.T) {
|
||||||
t.Setenv("CHECK_UPDATES", "true")
|
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