mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-27 18:42:41 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9b89c3917 | |||
| f8605bcd3c | |||
| 1d700a0370 | |||
| 681c2cc2ed | |||
| 8f8ae76d56 | |||
| f21706eb12 | |||
| d5ccf9c670 |
@@ -38,3 +38,4 @@ Samsung
|
||||
wenet
|
||||
qwertiko
|
||||
setuplistener
|
||||
mba
|
||||
|
||||
@@ -47,6 +47,7 @@ cachediptoasn
|
||||
Caddyfile
|
||||
caninetools
|
||||
Cardyb
|
||||
CAs
|
||||
celchecker
|
||||
celphase
|
||||
cerr
|
||||
@@ -203,8 +204,10 @@ kagi
|
||||
kagibot
|
||||
Keyfunc
|
||||
keypair
|
||||
keypairreloader
|
||||
KHTML
|
||||
kinda
|
||||
kpr
|
||||
KUBECONFIG
|
||||
lcj
|
||||
ldflags
|
||||
@@ -229,6 +232,7 @@ metarefresh
|
||||
metrix
|
||||
mimi
|
||||
Minfilia
|
||||
minica
|
||||
mistralai
|
||||
mnt
|
||||
Mojeek
|
||||
@@ -313,6 +317,7 @@ searchbot
|
||||
searx
|
||||
sebest
|
||||
secretplans
|
||||
selfsigned
|
||||
Semrush
|
||||
Seo
|
||||
setsebool
|
||||
|
||||
@@ -174,6 +174,27 @@ status_codes:
|
||||
# metrics:
|
||||
# bind: ":9090"
|
||||
# network: "tcp"
|
||||
#
|
||||
# # To protect your metrics server with basic auth, set credentials below:
|
||||
# #
|
||||
# # https://anubis.techaro.lol/docs/admin/policies#http-basic-authentication
|
||||
# basicAuth:
|
||||
# username: ""
|
||||
# password: ""
|
||||
#
|
||||
# # To serve metrics over TLS, set the path to the right TLS certificate and key
|
||||
# # here. When the files change on disk, they will automatically be reloaded.
|
||||
# #
|
||||
# # https://anubis.techaro.lol/docs/admin/policies#tls
|
||||
# tls:
|
||||
# certificate: /path/to/tls.crt
|
||||
# key: /path/to/tls.key
|
||||
#
|
||||
# # If you want to secure your metrics endpoint using mutual TLS (mTLS), set
|
||||
# # the path to a certificate authority public certificate here.
|
||||
# #
|
||||
# # https://anubis.techaro.lol/docs/admin/policies#mtls
|
||||
# ca: /path/to/ca.crt
|
||||
|
||||
# Anubis can store temporary data in one of a few backends. See the storage
|
||||
# backends section of the docs for more information:
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# - Claude-SearchBot: No published IP allowlist
|
||||
- name: "ai-crawlers-search"
|
||||
user_agent_regex: >-
|
||||
OAI-SearchBot|Claude-SearchBot|PerplexityBot
|
||||
OAI-SearchBot|Claude-SearchBot|PerplexityBot|meta-webindexer
|
||||
action: DENY
|
||||
|
||||
@@ -20,7 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fixed mixed tab/space indentation in Caddy documentation code block
|
||||
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
|
||||
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
|
||||
- Fixed case-sensitivity mismatch in geoipchecker.go
|
||||
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
|
||||
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
|
||||
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
||||
- Fix a bug in the dataset poisoning maze that could allow denial of service [#1580](https://github.com/TecharoHQ/anubis/issues/1580).
|
||||
|
||||
## v1.25.0: Necron
|
||||
|
||||
|
||||
@@ -138,6 +138,57 @@ metrics:
|
||||
socketMode: "0700" # must be a string
|
||||
```
|
||||
|
||||
### TLS
|
||||
|
||||
If you want to serve the metrics server over TLS, use the `tls` block:
|
||||
|
||||
```yaml
|
||||
metrics:
|
||||
bind: ":9090"
|
||||
network: "tcp"
|
||||
|
||||
tls:
|
||||
certificate: /path/to/tls.crt
|
||||
key: /path/to/tls.key
|
||||
```
|
||||
|
||||
The certificate and key will automatically be reloaded when the respective files change.
|
||||
|
||||
### mTLS
|
||||
|
||||
If you want to validate requests to ensure that they use a client certificate signed by a certificate authority (mutual TLS or mTLS), set the `ca` value in the `tls` block:
|
||||
|
||||
```yaml
|
||||
metrics:
|
||||
bind: ":9090"
|
||||
network: "tcp"
|
||||
|
||||
tls:
|
||||
certificate: /path/to/tls.crt
|
||||
key: /path/to/tls.key
|
||||
ca: /path/to/ca.crt
|
||||
```
|
||||
|
||||
As it is not expected for certificate authority certificates to change often, the CA certificate will NOT be automatically reloaded when the respective file changes.
|
||||
|
||||
### HTTP basic authentication
|
||||
|
||||
Anubis' metrics server also supports setting HTTP basic auth as a lightweight protection against unauthorized users viewing metrics data. As the basic auth credentials are hardcoded in the configuration file, administrators SHOULD use randomly generated credentials, such as type-4 UUIDs or other high entropy strings. These credentials MUST NOT be sensitive or used to protect other high value systems.
|
||||
|
||||
Configure it with the `basicAuth` block under `metrics`:
|
||||
|
||||
```yaml
|
||||
metrics:
|
||||
bind: ":9090"
|
||||
network: "tcp"
|
||||
|
||||
basicAuth:
|
||||
username: azurediamond
|
||||
password: hunter2
|
||||
```
|
||||
|
||||
If you have Python installed, you can generate a high entropy password with `python -c 'import secrets; print(secrets.token_urlsafe(32))'`.
|
||||
|
||||
## Imprint / Impressum support
|
||||
|
||||
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// BasicAuth wraps next in HTTP Basic authentication using the provided
|
||||
// credentials. If either username or password is empty, next is returned
|
||||
// unchanged and a debug log line is emitted.
|
||||
//
|
||||
// Credentials are compared in constant time to avoid leaking information
|
||||
// through timing side channels.
|
||||
func BasicAuth(realm, username, password string, next http.Handler) http.Handler {
|
||||
if username == "" || password == "" {
|
||||
slog.Debug("skipping middleware, basic auth credentials are empty")
|
||||
return next
|
||||
}
|
||||
|
||||
expectedUser := sha256.Sum256([]byte(username))
|
||||
expectedPass := sha256.Sum256([]byte(password))
|
||||
challenge := fmt.Sprintf("Basic realm=%q, charset=\"UTF-8\"", realm)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
unauthorized(w, challenge)
|
||||
return
|
||||
}
|
||||
|
||||
gotUser := sha256.Sum256([]byte(user))
|
||||
gotPass := sha256.Sum256([]byte(pass))
|
||||
|
||||
userMatch := subtle.ConstantTimeCompare(gotUser[:], expectedUser[:])
|
||||
passMatch := subtle.ConstantTimeCompare(gotPass[:], expectedPass[:])
|
||||
|
||||
if userMatch&passMatch != 1 {
|
||||
unauthorized(w, challenge)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func unauthorized(w http.ResponseWriter, challenge string) {
|
||||
w.Header().Set("WWW-Authenticate", challenge)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func okHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBasicAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
realm = "test-realm"
|
||||
username = "admin"
|
||||
password = "hunter2"
|
||||
)
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
setAuth bool
|
||||
user string
|
||||
pass string
|
||||
wantStatus int
|
||||
wantBody string
|
||||
wantChall bool
|
||||
}{
|
||||
{
|
||||
name: "valid credentials",
|
||||
setAuth: true,
|
||||
user: username,
|
||||
pass: password,
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: "ok",
|
||||
},
|
||||
{
|
||||
name: "missing credentials",
|
||||
setAuth: false,
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantChall: true,
|
||||
},
|
||||
{
|
||||
name: "wrong username",
|
||||
setAuth: true,
|
||||
user: "nobody",
|
||||
pass: password,
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantChall: true,
|
||||
},
|
||||
{
|
||||
name: "wrong password",
|
||||
setAuth: true,
|
||||
user: username,
|
||||
pass: "wrong",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantChall: true,
|
||||
},
|
||||
{
|
||||
name: "empty supplied credentials",
|
||||
setAuth: true,
|
||||
user: "",
|
||||
pass: "",
|
||||
wantStatus: http.StatusUnauthorized,
|
||||
wantChall: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := BasicAuth(realm, username, password, okHandler())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if tt.setAuth {
|
||||
req.SetBasicAuth(tt.user, tt.pass)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != tt.wantStatus {
|
||||
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
|
||||
}
|
||||
|
||||
if tt.wantBody != "" && rec.Body.String() != tt.wantBody {
|
||||
t.Errorf("body = %q, want %q", rec.Body.String(), tt.wantBody)
|
||||
}
|
||||
|
||||
chall := rec.Header().Get("WWW-Authenticate")
|
||||
if tt.wantChall {
|
||||
if chall == "" {
|
||||
t.Error("WWW-Authenticate header missing on 401")
|
||||
}
|
||||
if !strings.Contains(chall, realm) {
|
||||
t.Errorf("WWW-Authenticate = %q, want realm %q", chall, realm)
|
||||
}
|
||||
} else if chall != "" {
|
||||
t.Errorf("unexpected WWW-Authenticate header: %q", chall)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicAuthPassthrough(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
}{
|
||||
{name: "empty username", username: "", password: "hunter2"},
|
||||
{name: "empty password", username: "admin", password: ""},
|
||||
{name: "both empty", username: "", password: ""},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := BasicAuth("realm", tt.username, tt.password, okHandler())
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d (passthrough expected)", rec.Code, http.StatusOK)
|
||||
}
|
||||
if rec.Body.String() != "ok" {
|
||||
t.Errorf("body = %q, want %q", rec.Body.String(), "ok")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -76,13 +76,6 @@ type Impl struct {
|
||||
affirmation, body, title spintax.Spintax
|
||||
}
|
||||
|
||||
func (i *Impl) incrementUA(ctx context.Context, userAgent string) int {
|
||||
result, _ := i.uaWeight.Get(ctx, internal.SHA256sum(userAgent))
|
||||
result++
|
||||
i.uaWeight.Set(ctx, internal.SHA256sum(userAgent), result, time.Hour)
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
|
||||
result, _ := i.networkWeight.Get(ctx, internal.SHA256sum(network))
|
||||
result++
|
||||
@@ -90,20 +83,19 @@ func (i *Impl) incrementNetwork(ctx context.Context, network string) int {
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Impl) CheckUA() checker.Impl {
|
||||
return checker.Func(func(r *http.Request) (bool, error) {
|
||||
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
|
||||
if result >= 25 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Impl) CheckNetwork() checker.Impl {
|
||||
return checker.Func(func(r *http.Request) (bool, error) {
|
||||
result, _ := i.uaWeight.Get(r.Context(), internal.SHA256sum(r.UserAgent()))
|
||||
realIP, _ := internal.RealIP(r)
|
||||
if !realIP.IsValid() {
|
||||
realIP = netip.MustParseAddr(r.Header.Get("X-Real-Ip"))
|
||||
}
|
||||
|
||||
network, ok := internal.ClampIP(realIP)
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
result, _ := i.networkWeight.Get(r.Context(), internal.SHA256sum(network.String()))
|
||||
if result >= 25 {
|
||||
return true, nil
|
||||
}
|
||||
@@ -164,7 +156,6 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
networkCount := i.incrementNetwork(r.Context(), network.String())
|
||||
uaCount := i.incrementUA(r.Context(), r.UserAgent())
|
||||
|
||||
stage := r.PathValue("stage")
|
||||
|
||||
@@ -172,8 +163,8 @@ func (i *Impl) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
lg.Debug("found new entrance point", "id", id, "stage", stage, "userAgent", r.UserAgent(), "clampedIP", network)
|
||||
} else {
|
||||
switch {
|
||||
case networkCount%256 == 0, uaCount%256 == 0:
|
||||
lg.Warn("found possible crawler", "id", id, "network", network)
|
||||
case networkCount%256 == 0:
|
||||
lg.Warn("found possible crawler", "id", id, "network", network, "userAgent", r.UserAgent())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,14 +190,6 @@ func New(opts Options) (*Server, error) {
|
||||
},
|
||||
Name: "honeypot/network",
|
||||
},
|
||||
policy.Bot{
|
||||
Rules: mazeGen.CheckUA(),
|
||||
Action: config.RuleWeigh,
|
||||
Weight: &config.Weight{
|
||||
Adjust: 30,
|
||||
},
|
||||
Name: "honeypot/user-agent",
|
||||
},
|
||||
)
|
||||
} else {
|
||||
result.logger.Error("can't init honeypot subsystem", "err", err)
|
||||
|
||||
+121
-9
@@ -1,24 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
|
||||
ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
|
||||
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
|
||||
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
|
||||
ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode")
|
||||
ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network")
|
||||
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
|
||||
ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration")
|
||||
ErrInvalidMetricsBasicAuthConfig = errors.New("config: invalid metrics basic auth configuration")
|
||||
ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
|
||||
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
|
||||
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
|
||||
ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode")
|
||||
ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network")
|
||||
ErrNoMetricsTLSCertificate = errors.New("config.Metrics.TLS: must define certificate file")
|
||||
ErrNoMetricsTLSKey = errors.New("config.Metrics.TLS: must define key file")
|
||||
ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid")
|
||||
ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate")
|
||||
ErrCantReadFile = errors.New("config: can't read required file")
|
||||
ErrNoMetricsBasicAuthUsername = errors.New("config.Metrics.BasicAuth: must define username")
|
||||
ErrNoMetricsBasicAuthPassword = errors.New("config.Metrics.BasicAuth: must define password")
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
Bind string `json:"bind" yaml:"bind"`
|
||||
Network string `json:"network" yaml:"network"`
|
||||
SocketMode string `json:"socketMode" yaml:"socketMode"`
|
||||
Bind string `json:"bind" yaml:"bind"`
|
||||
Network string `json:"network" yaml:"network"`
|
||||
SocketMode string `json:"socketMode" yaml:"socketMode"`
|
||||
TLS *MetricsTLS `json:"tls" yaml:"tls"`
|
||||
BasicAuth *MetricsBasicAuth `json:"basicAuth" yaml:"basicAuth"`
|
||||
}
|
||||
|
||||
func (m *Metrics) Valid() error {
|
||||
@@ -46,9 +60,107 @@ func (m *Metrics) Valid() error {
|
||||
errs = append(errs, ErrInvalidMetricsNetwork)
|
||||
}
|
||||
|
||||
if m.TLS != nil {
|
||||
if err := m.TLS.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if m.BasicAuth != nil {
|
||||
if err := m.BasicAuth.Valid(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MetricsTLS struct {
|
||||
Certificate string `json:"certificate" yaml:"certificate"`
|
||||
Key string `json:"key" yaml:"key"`
|
||||
CA string `json:"ca" yaml:"ca"`
|
||||
}
|
||||
|
||||
func (mt *MetricsTLS) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if mt.Certificate == "" {
|
||||
errs = append(errs, ErrNoMetricsTLSCertificate)
|
||||
}
|
||||
|
||||
if err := canReadFile(mt.Certificate); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.Certificate, err))
|
||||
}
|
||||
|
||||
if mt.Key == "" {
|
||||
errs = append(errs, ErrNoMetricsTLSKey)
|
||||
}
|
||||
|
||||
if err := canReadFile(mt.Key); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.Key, err))
|
||||
}
|
||||
|
||||
if _, err := tls.LoadX509KeyPair(mt.Certificate, mt.Key); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidMetricsTLSKeypair, err))
|
||||
}
|
||||
|
||||
if mt.CA != "" {
|
||||
caCert, err := os.ReadFile(mt.CA)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.CA, err))
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM(caCert) {
|
||||
errs = append(errs, fmt.Errorf("%w %s", ErrInvalidMetricsCACertificate, mt.CA))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(ErrInvalidMetricsTLSConfig, errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func canReadFile(fname string) error {
|
||||
fin, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fin.Close()
|
||||
|
||||
data := make([]byte, 64)
|
||||
if _, err := fin.Read(data); err != nil {
|
||||
return fmt.Errorf("can't read %s: %w", fname, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MetricsBasicAuth struct {
|
||||
Username string `json:"username" yaml:"username"`
|
||||
Password string `json:"password" yaml:"password"`
|
||||
}
|
||||
|
||||
func (mba *MetricsBasicAuth) Valid() error {
|
||||
var errs []error
|
||||
|
||||
if mba.Username == "" {
|
||||
errs = append(errs, ErrNoMetricsBasicAuthUsername)
|
||||
}
|
||||
|
||||
if mba.Password == "" {
|
||||
errs = append(errs, ErrNoMetricsBasicAuthPassword)
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errors.Join(ErrInvalidMetricsBasicAuthConfig, errors.Join(errs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,6 +75,161 @@ func TestMetricsValid(t *testing.T) {
|
||||
},
|
||||
err: ErrInvalidMetricsNetwork,
|
||||
},
|
||||
{
|
||||
name: "invalid TLS config",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{},
|
||||
},
|
||||
err: ErrInvalidMetricsTLSConfig,
|
||||
},
|
||||
{
|
||||
name: "selfsigned TLS cert",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{
|
||||
Certificate: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong path to selfsigned TLS cert",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{
|
||||
Certificate: "./testdata/tls2/selfsigned.crt",
|
||||
Key: "./testdata/tls2/selfsigned.key",
|
||||
},
|
||||
},
|
||||
err: ErrCantReadFile,
|
||||
},
|
||||
{
|
||||
name: "unparseable TLS cert",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{
|
||||
Certificate: "./testdata/tls/invalid.crt",
|
||||
Key: "./testdata/tls/invalid.key",
|
||||
},
|
||||
},
|
||||
err: ErrInvalidMetricsTLSKeypair,
|
||||
},
|
||||
{
|
||||
name: "mTLS with CA",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{
|
||||
Certificate: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
CA: "./testdata/tls/minica.pem",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mTLS with nonexistent CA",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{
|
||||
Certificate: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
CA: "./testdata/tls/nonexistent.crt",
|
||||
},
|
||||
},
|
||||
err: ErrCantReadFile,
|
||||
},
|
||||
{
|
||||
name: "mTLS with invalid CA",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
TLS: &MetricsTLS{
|
||||
Certificate: "./testdata/tls/selfsigned.crt",
|
||||
Key: "./testdata/tls/selfsigned.key",
|
||||
CA: "./testdata/tls/invalid.crt",
|
||||
},
|
||||
},
|
||||
err: ErrInvalidMetricsCACertificate,
|
||||
},
|
||||
{
|
||||
name: "basic auth credentials set",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
BasicAuth: &MetricsBasicAuth{
|
||||
Username: "admin",
|
||||
Password: "hunter2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid basic auth config",
|
||||
input: &Metrics{
|
||||
Bind: ":9090",
|
||||
Network: "tcp",
|
||||
BasicAuth: &MetricsBasicAuth{},
|
||||
},
|
||||
err: ErrInvalidMetricsBasicAuthConfig,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||
t.Logf("wanted error: %v", tt.err)
|
||||
t.Logf("got error: %v", err)
|
||||
t.Error("validation failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsBasicAuthValid(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
input *MetricsBasicAuth
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "both set",
|
||||
input: &MetricsBasicAuth{
|
||||
Username: "admin",
|
||||
Password: "hunter2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty username and password",
|
||||
input: &MetricsBasicAuth{},
|
||||
err: ErrInvalidMetricsBasicAuthConfig,
|
||||
},
|
||||
{
|
||||
name: "missing username",
|
||||
input: &MetricsBasicAuth{
|
||||
Password: "hunter2",
|
||||
},
|
||||
err: ErrNoMetricsBasicAuthUsername,
|
||||
},
|
||||
{
|
||||
name: "missing password",
|
||||
input: &MetricsBasicAuth{
|
||||
Username: "admin",
|
||||
},
|
||||
err: ErrNoMetricsBasicAuthPassword,
|
||||
},
|
||||
{
|
||||
name: "missing both surfaces wrapper error",
|
||||
input: &MetricsBasicAuth{},
|
||||
err: ErrNoMetricsBasicAuthUsername,
|
||||
},
|
||||
{
|
||||
name: "missing both surfaces password error",
|
||||
input: &MetricsBasicAuth{},
|
||||
err: ErrNoMetricsBasicAuthPassword,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB1zCCAVygAwIBAgIIYO0SAFtXlVgwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
|
||||
bWluaWNhIHJvb3QgY2EgNDE2MmMwMB4XDTI2MDQyMjIzMjUwMVoXDTI4MDUyMjIz
|
||||
MjUwMVowEjEQMA4GA1UEAxMHMS4xLjEuMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA
|
||||
BLsuA2LKGbEBuSA4LTm1KaKc7/QCkUOsipXR4+D5/3sWBZiAH7iWUgHwpx5YZf2q
|
||||
kZn6oRda+ks0JLTQ6VhteQedmb7l86bMeDMR8p4Lg2b38l/xEr7S25UfUDKudXrO
|
||||
AqNxMG8wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
|
||||
BQcDAjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFE/7VDxF2+cUs9bu0pJM3xoC
|
||||
L1TSMA8GA1UdEQQIMAaHBAEBAQEwCgYIKoZIzj0EAwMDaQAwZgIxAPLXds9MMH4K
|
||||
F5FxTf9i0PKPsLQARsABVTgwB94hMR70rqW8Pwbjl7ZGNaYlaeRHUwIxAPMQ8zoF
|
||||
nim+YS1xLqQek/LXuJto8jxcfkQQBsboVzcTa5uaNRhNd5YwrpomGl3lKA==
|
||||
-----END CERTIFICATE-----
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBN8QsHxxHGJpStu8K7
|
||||
D/FmaBBNo6c514KGFSIfqGFuREF5aOL3gN/W11yk2OIibdWhZANiAAS7LgNiyhmx
|
||||
AbkgOC05tSminO/0ApFDrIqV0ePg+f97FgWYgB+4llIB8KceWGX9qpGZ+qEXWvpL
|
||||
NCS00OlYbXkHnZm+5fOmzHgzEfKeC4Nm9/Jf8RK+0tuVH1AyrnV6zgI=
|
||||
-----END PRIVATE KEY-----
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDr9QQo7ZaTgUL6d73G
|
||||
2BG7+YRTFJHAZa0FogRglfc+jYttL1J4/xTig3RmHoqSgrehZANiAASDhijM9Xe0
|
||||
G9Vam6AJMeKC6aWDNSLwrxNVmPxemsY/yJ1urBgnxRd9GFH6YW1ki/B8rS+Xl1UX
|
||||
NnhBrukLaXvgAQQq782/5IUYGsvK5jw8+dSscYVMCQJwGfmQuaNeczQ=
|
||||
-----END PRIVATE KEY-----
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB+zCCAYKgAwIBAgIIQWLAtv4ijQ0wCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
|
||||
bWluaWNhIHJvb3QgY2EgNDE2MmMwMCAXDTI2MDQyMjIzMjUwMVoYDzIxMjYwNDIy
|
||||
MjMyNTAxWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA0MTYyYzAwdjAQBgcq
|
||||
hkjOPQIBBgUrgQQAIgNiAASDhijM9Xe0G9Vam6AJMeKC6aWDNSLwrxNVmPxemsY/
|
||||
yJ1urBgnxRd9GFH6YW1ki/B8rS+Xl1UXNnhBrukLaXvgAQQq782/5IUYGsvK5jw8
|
||||
+dSscYVMCQJwGfmQuaNeczSjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQW
|
||||
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
|
||||
DgQWBBRP+1Q8RdvnFLPW7tKSTN8aAi9U0jAfBgNVHSMEGDAWgBRP+1Q8RdvnFLPW
|
||||
7tKSTN8aAi9U0jAKBggqhkjOPQQDAwNnADBkAjBfY7vb7cuLTjg7uoe+kl07FMYT
|
||||
BGMSnWdhN3yXqMUS3A6XZxD/LntXT6V7yFOlAJYCMH7w8/ATYaTqbk2jBRyQt9/x
|
||||
ajN+kZ6ZK+fKttqE8CD62mbHg09xoNxRq+K2I3PVyQ==
|
||||
-----END CERTIFICATE-----
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnzCCAVGgAwIBAgIUK39B3Ft+kU5o81IuISs79O4u1ncwBQYDK2VwMEUxCzAJ
|
||||
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjYwNDIyMTQyNjE4WhcNMjYwNTIyMTQyNjE4
|
||||
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAfgpAUpp8MIOOdQpH
|
||||
fxaw3R7mFKQRMR6Kmxzk1Xn/2VujUzBRMB0GA1UdDgQWBBSmkBmzo0RiZ2iocMR8
|
||||
uIIpz9cZyTAfBgNVHSMEGDAWgBSmkBmzo0RiZ2iocMR8uIIpz9cZyTAPBgNVHRMB
|
||||
Af8EBTADAQH/MAUGAytlcANBAG37XXZrVUUzGyy3T9qsPIzvJQAGpGhdjJ7bt9O6
|
||||
sBhzrliTONPrudYuyUggWsHgFb0JlN2xs4/2HhKU+PY7AAQ=
|
||||
-----END CERTIFICATE-----
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIL0HxjjfVlg6zQPB9/zTLq0IBzfp8gEoifEYzQZYIj+T
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,81 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KeypairReloader struct {
|
||||
certMu sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
certPath string
|
||||
keyPath string
|
||||
modTime time.Time
|
||||
lg *slog.Logger
|
||||
}
|
||||
|
||||
func NewKeypairReloader(certPath, keyPath string, lg *slog.Logger) (*KeypairReloader, error) {
|
||||
result := &KeypairReloader{
|
||||
certPath: certPath,
|
||||
keyPath: keyPath,
|
||||
lg: lg,
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.cert = &cert
|
||||
|
||||
st, err := os.Stat(certPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.modTime = st.ModTime()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (kpr *KeypairReloader) maybeReload() error {
|
||||
kpr.lg.Debug("loading new keypair", "cert", kpr.certPath, "key", kpr.keyPath)
|
||||
newCert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st, err := os.Stat(kpr.certPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kpr.certMu.Lock()
|
||||
defer kpr.certMu.Unlock()
|
||||
kpr.cert = &newCert
|
||||
kpr.modTime = st.ModTime()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kpr *KeypairReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
st, err := os.Stat(kpr.certPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat(%q): %w", kpr.certPath, err)
|
||||
}
|
||||
|
||||
kpr.certMu.RLock()
|
||||
needsReload := st.ModTime().After(kpr.modTime)
|
||||
kpr.certMu.RUnlock()
|
||||
|
||||
if needsReload {
|
||||
if err := kpr.maybeReload(); err != nil {
|
||||
return nil, fmt.Errorf("reload cert: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
kpr.certMu.RLock()
|
||||
defer kpr.certMu.RUnlock()
|
||||
return kpr.cert, nil
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func discardLogger() *slog.Logger {
|
||||
return slog.New(slog.DiscardHandler)
|
||||
}
|
||||
|
||||
// writeKeypair generates a fresh self-signed cert + RSA key and writes them
|
||||
// as PEM files in dir. Returns the paths and the cert's DER bytes so callers
|
||||
// can identify which pair was loaded.
|
||||
func writeKeypair(t *testing.T, dir, prefix string) (certPath, keyPath string, certDER []byte) {
|
||||
t.Helper()
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("rsa.GenerateKey: %v", err)
|
||||
}
|
||||
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
||||
Subject: pkix.Name{CommonName: "keypairreloader-test"},
|
||||
NotBefore: time.Now().Add(-time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"keypairreloader-test"},
|
||||
}
|
||||
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatalf("x509.CreateCertificate: %v", err)
|
||||
}
|
||||
|
||||
certPath = filepath.Join(dir, prefix+"cert.pem")
|
||||
keyPath = filepath.Join(dir, prefix+"key.pem")
|
||||
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
|
||||
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
|
||||
t.Fatalf("write cert: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
||||
t.Fatalf("write key: %v", err)
|
||||
}
|
||||
|
||||
return certPath, keyPath, der
|
||||
}
|
||||
|
||||
func TestNewKeypairReloader(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
goodCert, goodKey, _ := writeKeypair(t, dir, "good-")
|
||||
|
||||
garbagePath := filepath.Join(dir, "garbage.pem")
|
||||
if err := os.WriteFile(garbagePath, []byte("not a pem file"), 0o600); err != nil {
|
||||
t.Fatalf("write garbage: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
certPath string
|
||||
keyPath string
|
||||
wantErr error
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "valid cert and key",
|
||||
certPath: goodCert,
|
||||
keyPath: goodKey,
|
||||
},
|
||||
{
|
||||
name: "missing cert file",
|
||||
certPath: filepath.Join(dir, "does-not-exist.pem"),
|
||||
keyPath: goodKey,
|
||||
wantErr: os.ErrNotExist,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "missing key file",
|
||||
certPath: goodCert,
|
||||
keyPath: filepath.Join(dir, "does-not-exist-key.pem"),
|
||||
wantErr: os.ErrNotExist,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "cert file is garbage",
|
||||
certPath: garbagePath,
|
||||
keyPath: goodKey,
|
||||
wantNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
kpr, err := NewKeypairReloader(tt.certPath, tt.keyPath, discardLogger())
|
||||
|
||||
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("err = %v, want errors.Is(..., %v)", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr == nil && !tt.wantNil && err != nil {
|
||||
t.Errorf("unexpected err: %v", err)
|
||||
}
|
||||
if tt.wantNil && kpr != nil {
|
||||
t.Errorf("kpr = %+v, want nil", kpr)
|
||||
}
|
||||
if !tt.wantNil && kpr == nil {
|
||||
t.Errorf("kpr is nil, want non-nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeypairReloader_GetCertificate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
run func(t *testing.T)
|
||||
}{
|
||||
{
|
||||
name: "returns loaded cert",
|
||||
run: func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath, keyPath, wantDER := writeKeypair(t, dir, "a-")
|
||||
|
||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeypairReloader: %v", err)
|
||||
}
|
||||
|
||||
got, err := kpr.GetCertificate(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCertificate: %v", err)
|
||||
}
|
||||
if len(got.Certificate) == 0 || !bytes.Equal(got.Certificate[0], wantDER) {
|
||||
t.Errorf("GetCertificate returned wrong cert bytes")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reloads when mtime advances",
|
||||
run: func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath, keyPath, _ := writeKeypair(t, dir, "a-")
|
||||
|
||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeypairReloader: %v", err)
|
||||
}
|
||||
|
||||
// Overwrite with a new pair at the same paths and bump mtime.
|
||||
newCertPath, newKeyPath, newDER := writeKeypair(t, dir, "b-")
|
||||
mustRename(t, newCertPath, certPath)
|
||||
mustRename(t, newKeyPath, keyPath)
|
||||
future := time.Now().Add(time.Hour)
|
||||
if err := os.Chtimes(certPath, future, future); err != nil {
|
||||
t.Fatalf("Chtimes: %v", err)
|
||||
}
|
||||
|
||||
got, err := kpr.GetCertificate(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCertificate: %v", err)
|
||||
}
|
||||
if len(got.Certificate) == 0 || !bytes.Equal(got.Certificate[0], newDER) {
|
||||
t.Errorf("GetCertificate did not return reloaded cert")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not reload when mtime unchanged",
|
||||
run: func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath, keyPath, originalDER := writeKeypair(t, dir, "a-")
|
||||
|
||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeypairReloader: %v", err)
|
||||
}
|
||||
|
||||
// Overwrite the cert/key files with a *different* keypair, then
|
||||
// rewind mtime so the reloader must not pick up the change.
|
||||
newCertPath, newKeyPath, newDER := writeKeypair(t, dir, "b-")
|
||||
mustRename(t, newCertPath, certPath)
|
||||
mustRename(t, newKeyPath, keyPath)
|
||||
past := time.Unix(0, 0)
|
||||
if err := os.Chtimes(certPath, past, past); err != nil {
|
||||
t.Fatalf("Chtimes: %v", err)
|
||||
}
|
||||
|
||||
got, err := kpr.GetCertificate(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetCertificate: %v", err)
|
||||
}
|
||||
if len(got.Certificate) == 0 {
|
||||
t.Fatal("empty cert chain")
|
||||
}
|
||||
if bytes.Equal(got.Certificate[0], newDER) {
|
||||
t.Errorf("GetCertificate reloaded despite unchanged mtime")
|
||||
}
|
||||
if !bytes.Equal(got.Certificate[0], originalDER) {
|
||||
t.Errorf("GetCertificate did not return original cert")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "does not panic when reload fails after mtime bump",
|
||||
run: func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certPath, keyPath, _ := writeKeypair(t, dir, "a-")
|
||||
|
||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("NewKeypairReloader: %v", err)
|
||||
}
|
||||
|
||||
// Corrupt the cert file and bump mtime. maybeReload will fail.
|
||||
if err := os.WriteFile(certPath, []byte("not a pem file"), 0o600); err != nil {
|
||||
t.Fatalf("corrupt cert: %v", err)
|
||||
}
|
||||
future := time.Now().Add(time.Hour)
|
||||
if err := os.Chtimes(certPath, future, future); err != nil {
|
||||
t.Fatalf("Chtimes: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("GetCertificate panicked on reload failure: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
got, err := kpr.GetCertificate(nil)
|
||||
if err == nil {
|
||||
t.Errorf("GetCertificate returned nil err for corrupt cert; got %+v", got)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.run(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustRename(t *testing.T, from, to string) {
|
||||
t.Helper()
|
||||
if err := os.Rename(from, to); err != nil {
|
||||
t.Fatalf("rename %q -> %q: %v", from, to, err)
|
||||
}
|
||||
}
|
||||
+52
-3
@@ -2,11 +2,14 @@ package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/TecharoHQ/anubis/internal"
|
||||
@@ -20,10 +23,16 @@ type Server struct {
|
||||
Log *slog.Logger
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context, done func()) error {
|
||||
func (s *Server) Run(ctx context.Context, done func()) {
|
||||
defer done()
|
||||
lg := s.Log.With("subsystem", "metrics")
|
||||
|
||||
if err := s.run(ctx, lg); err != nil {
|
||||
lg.Error("can't serve metrics server", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
|
||||
@@ -62,6 +71,39 @@ func (s *Server) Run(ctx context.Context, done func()) error {
|
||||
|
||||
defer ln.Close()
|
||||
|
||||
if s.Config.TLS != nil {
|
||||
kpr, err := NewKeypairReloader(s.Config.TLS.Certificate, s.Config.TLS.Key, lg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't setup keypair reloader: %w", err)
|
||||
}
|
||||
|
||||
srv.TLSConfig = &tls.Config{
|
||||
GetCertificate: kpr.GetCertificate,
|
||||
}
|
||||
|
||||
if s.Config.TLS.CA != "" {
|
||||
caCert, err := os.ReadFile(s.Config.TLS.CA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w %s: %w", config.ErrCantReadFile, s.Config.TLS.CA, err)
|
||||
}
|
||||
|
||||
certPool := x509.NewCertPool()
|
||||
if !certPool.AppendCertsFromPEM(caCert) {
|
||||
return fmt.Errorf("%w %s", config.ErrInvalidMetricsCACertificate, s.Config.TLS.CA)
|
||||
}
|
||||
|
||||
srv.TLSConfig.ClientCAs = certPool
|
||||
srv.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
}
|
||||
|
||||
if s.Config.BasicAuth != nil {
|
||||
var h http.Handler = mux
|
||||
h = internal.BasicAuth("anubis-metrics", s.Config.BasicAuth.Username, s.Config.BasicAuth.Password, mux)
|
||||
|
||||
srv.Handler = h
|
||||
}
|
||||
|
||||
lg.Debug("listening for metrics", "url", metricsURL)
|
||||
|
||||
go func() {
|
||||
@@ -73,8 +115,15 @@ func (s *Server) Run(ctx context.Context, done func()) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("can't serve metrics server: %w", err)
|
||||
switch s.Config.TLS != nil {
|
||||
case true:
|
||||
if err := srv.ServeTLS(ln, "", ""); !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("can't serve TLS metrics server: %w", err)
|
||||
}
|
||||
case false:
|
||||
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("can't serve metrics server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -18,7 +18,7 @@ func (c *Client) GeoIPCheckerFor(countries []string) checker.Impl {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintln(&sb, "GeoIPChecker")
|
||||
for _, cc := range countries {
|
||||
countryMap[cc] = struct{}{}
|
||||
countryMap[strings.ToLower(cc)] = struct{}{}
|
||||
fmt.Fprintln(&sb, cc)
|
||||
}
|
||||
|
||||
|
||||
Generated
+222
-220
@@ -15,9 +15,9 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.5.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"baseline-browser-mapping": "^2.10.15",
|
||||
"cssnano": "^7.1.4",
|
||||
"cssnano-preset-advanced": "^7.0.12",
|
||||
"baseline-browser-mapping": "^2.10.20",
|
||||
"cssnano": "^7.1.5",
|
||||
"cssnano-preset-advanced": "^7.0.13",
|
||||
"esbuild": "^0.28.0",
|
||||
"husky": "^9.1.7",
|
||||
"playwright": "^1.52.0",
|
||||
@@ -25,7 +25,7 @@
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.8.1"
|
||||
"prettier": "^3.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/sha256-js": {
|
||||
@@ -92,9 +92,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@colordx/core": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.0.3.tgz",
|
||||
"integrity": "sha512-xBQ0MYRTNNxW3mS2sJtlQTT7C3Sasqgh1/PsHva7fyDb5uqYY+gv9V0utDdX8X80mqzbGz3u/IDJdn2d/uW09g==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.4.2.tgz",
|
||||
"integrity": "sha512-oC//VDid7CrDg+iXE/8RBq1s+MP+EFh5ggJOkpM9+ZathjC736A67Yg9LMAQULQ1blj9E0m0g8PubOu1/HniaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1055,9 +1055,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.15",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz",
|
||||
"integrity": "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ==",
|
||||
"version": "2.10.20",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
|
||||
"integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1112,9 +1112,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1132,11 +1132,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
"electron-to-chromium": "^1.5.328",
|
||||
"node-releases": "^2.0.36",
|
||||
"update-browserslist-db": "^1.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -1169,9 +1169,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001779",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001779.tgz",
|
||||
"integrity": "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA==",
|
||||
"version": "1.0.30001791",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
|
||||
"integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1436,13 +1436,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.4.tgz",
|
||||
"integrity": "sha512-T9PNS7y+5Nc9Qmu9mRONqfxG1RVY7Vuvky0XN6MZ+9hqplesTEwnj9r0ROtVuSwUVfaDhVlavuzWIVLUgm4hkQ==",
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.5.tgz",
|
||||
"integrity": "sha512-4yEvjF2zcoAOWfNq6X687ORJc5SvM5xbg6EGuLSBmGoWZbsL69wpmw1tA3fZt7OwIG+G4ndjF95RSS4luvim7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.12",
|
||||
"cssnano-preset-default": "^7.0.13",
|
||||
"lilconfig": "^3.1.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1457,18 +1457,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano-preset-advanced": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-7.0.12.tgz",
|
||||
"integrity": "sha512-S+UCfEMBCaYqNmjY+yCTNh4ur+B83z5qpwkzUMER6kVpdQ5CBUxIkALZGhp2KPP3M4VefRCIBMqJ6Z0PFMwvEA==",
|
||||
"version": "7.0.13",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-7.0.13.tgz",
|
||||
"integrity": "sha512-ox6P+hptJCN2RbZ0XFVROeKL1IwgDLP6Kh+4/lKhO9kLIcAO4w9UphWlp0t51nTqWN/VQ+5gflHfxpAwg3x7OA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.27",
|
||||
"browserslist": "^4.28.1",
|
||||
"cssnano-preset-default": "^7.0.12",
|
||||
"browserslist": "^4.28.2",
|
||||
"cssnano-preset-default": "^7.0.13",
|
||||
"postcss-discard-unused": "^7.0.5",
|
||||
"postcss-merge-idents": "^7.0.1",
|
||||
"postcss-reduce-idents": "^7.0.1",
|
||||
"postcss-reduce-idents": "^7.0.2",
|
||||
"postcss-zindex": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1479,61 +1479,61 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano-preset-default": {
|
||||
"version": "7.0.12",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.12.tgz",
|
||||
"integrity": "sha512-B3Eoouzw/sl2zANI0AL9KbacummJTCww+fkHaDBMZad/xuVx8bUduPLly6hKVQAlrmvYkS1jB1CVQEKm3gn0AA==",
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.15.tgz",
|
||||
"integrity": "sha512-60kx7lJ40//HA85cIfQXSOJFby2D2V1pOMNHVCxue3KFWCjRzmiQyL9OvI+NAhwUlaojOfF9eK3nGvrJLCBUfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist": "^4.28.2",
|
||||
"css-declaration-sorter": "^7.2.0",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"cssnano-utils": "^5.0.2",
|
||||
"postcss-calc": "^10.1.1",
|
||||
"postcss-colormin": "^7.0.7",
|
||||
"postcss-convert-values": "^7.0.9",
|
||||
"postcss-discard-comments": "^7.0.6",
|
||||
"postcss-discard-duplicates": "^7.0.2",
|
||||
"postcss-discard-empty": "^7.0.1",
|
||||
"postcss-discard-overridden": "^7.0.1",
|
||||
"postcss-merge-longhand": "^7.0.5",
|
||||
"postcss-merge-rules": "^7.0.8",
|
||||
"postcss-minify-font-values": "^7.0.1",
|
||||
"postcss-minify-gradients": "^7.0.2",
|
||||
"postcss-minify-params": "^7.0.6",
|
||||
"postcss-minify-selectors": "^7.0.6",
|
||||
"postcss-normalize-charset": "^7.0.1",
|
||||
"postcss-normalize-display-values": "^7.0.1",
|
||||
"postcss-normalize-positions": "^7.0.1",
|
||||
"postcss-normalize-repeat-style": "^7.0.1",
|
||||
"postcss-normalize-string": "^7.0.1",
|
||||
"postcss-normalize-timing-functions": "^7.0.1",
|
||||
"postcss-normalize-unicode": "^7.0.6",
|
||||
"postcss-normalize-url": "^7.0.1",
|
||||
"postcss-normalize-whitespace": "^7.0.1",
|
||||
"postcss-ordered-values": "^7.0.2",
|
||||
"postcss-reduce-initial": "^7.0.6",
|
||||
"postcss-reduce-transforms": "^7.0.1",
|
||||
"postcss-svgo": "^7.1.1",
|
||||
"postcss-unique-selectors": "^7.0.5"
|
||||
"postcss-colormin": "^7.0.9",
|
||||
"postcss-convert-values": "^7.0.11",
|
||||
"postcss-discard-comments": "^7.0.7",
|
||||
"postcss-discard-duplicates": "^7.0.3",
|
||||
"postcss-discard-empty": "^7.0.2",
|
||||
"postcss-discard-overridden": "^7.0.2",
|
||||
"postcss-merge-longhand": "^7.0.6",
|
||||
"postcss-merge-rules": "^7.0.10",
|
||||
"postcss-minify-font-values": "^7.0.2",
|
||||
"postcss-minify-gradients": "^7.0.4",
|
||||
"postcss-minify-params": "^7.0.8",
|
||||
"postcss-minify-selectors": "^7.1.0",
|
||||
"postcss-normalize-charset": "^7.0.2",
|
||||
"postcss-normalize-display-values": "^7.0.2",
|
||||
"postcss-normalize-positions": "^7.0.3",
|
||||
"postcss-normalize-repeat-style": "^7.0.3",
|
||||
"postcss-normalize-string": "^7.0.2",
|
||||
"postcss-normalize-timing-functions": "^7.0.2",
|
||||
"postcss-normalize-unicode": "^7.0.8",
|
||||
"postcss-normalize-url": "^7.0.2",
|
||||
"postcss-normalize-whitespace": "^7.0.2",
|
||||
"postcss-ordered-values": "^7.0.3",
|
||||
"postcss-reduce-initial": "^7.0.8",
|
||||
"postcss-reduce-transforms": "^7.0.2",
|
||||
"postcss-svgo": "^7.1.2",
|
||||
"postcss-unique-selectors": "^7.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano-utils": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz",
|
||||
"integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.2.tgz",
|
||||
"integrity": "sha512-kt41WLK7FLKfePzPi645Y+/NtW/nNM7Su6nlNUfJyRNW3JcuU3JU7+cWJc+JexTeZ8dRBvFufefdG2XpXkIo0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/csso": {
|
||||
@@ -1662,9 +1662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.313",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz",
|
||||
"integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==",
|
||||
"version": "1.5.344",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
|
||||
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -2347,9 +2347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"version": "2.0.38",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
||||
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2478,9 +2478,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2554,14 +2554,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-colormin": {
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.7.tgz",
|
||||
"integrity": "sha512-sBQ628lSj3VQpDquQel8Pen5mmjFPsO4pH9lDLaHB1AVkMRHtkl0pRB5DCWznc9upWsxint/kV+AveSj7W1tew==",
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.9.tgz",
|
||||
"integrity": "sha512-EZpoUlmbXQUpe+g4ZaGM2kjGlHrQ7Bjzb3xHcNrC9ysI1tGoib6DAYvxg6aB7MGxsjgLF+Qx/jwZQkJ5cKDvXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colordx/core": "^5.0.0",
|
||||
"browserslist": "^4.28.1",
|
||||
"@colordx/core": "^5.2.0",
|
||||
"browserslist": "^4.28.2",
|
||||
"caniuse-api": "^3.0.0",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
@@ -2569,30 +2569,30 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-convert-values": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.9.tgz",
|
||||
"integrity": "sha512-l6uATQATZaCa0bckHV+r6dLXfWtUBKXxO3jK+AtxxJJtgMPD+VhhPCCx51I4/5w8U5uHV67g3w7PXj+V3wlMlg==",
|
||||
"version": "7.0.11",
|
||||
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.11.tgz",
|
||||
"integrity": "sha512-H+s7P0f9jJylSysAHs3/5MhAx7GthDO05uw1h56L2xyEqpiLTFLEqBNw3PUYzD5p/AKwWaigCXf6FGELpOw9lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist": "^4.28.2",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-discard-comments": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.6.tgz",
|
||||
"integrity": "sha512-Sq+Fzj1Eg5/CPf1ERb0wS1Im5cvE2gDXCE+si4HCn1sf+jpQZxDI4DXEp8t77B/ImzDceWE2ebJQFXdqZ6GRJw==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.7.tgz",
|
||||
"integrity": "sha512-FJhE3fSte7HaRNL4iwD8LTG9vWqj3puxXIdig6LfrFqc1TJRUhY4kXOkeTXZZfTXYny+k+SO7fd2fymj1wduJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2602,46 +2602,46 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-discard-duplicates": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz",
|
||||
"integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.3.tgz",
|
||||
"integrity": "sha512-9cRxXwhEM/aNZon1qZyToX4NmjbFbxOGbww+0CnbYFDbbPRGZ8jg4IbM8UlA+CzkXxM35itxyaHKNqBBg/RTDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-discard-empty": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz",
|
||||
"integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.2.tgz",
|
||||
"integrity": "sha512-NZFouOmOwtngJVgkNeI1LtkzFdYqIurxgy4wq3qNvIiXFURTZ3b/K7q3dP3QitlWQ5imHDQL0qSorItQhoxb1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-discard-overridden": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz",
|
||||
"integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.2.tgz",
|
||||
"integrity": "sha512-Ym01X4v6U3sY8X0P1J9P+RTar+7JyLTOzDrxKSeaArFsLmkVu4KcAKPBWDYRIyZ/q4jwpSPnOnekeSSqXSXKUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-discard-unused": {
|
||||
@@ -2756,45 +2756,45 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-merge-longhand": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz",
|
||||
"integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.6.tgz",
|
||||
"integrity": "sha512-lDsWeKRsssX/9vKFpingoRiuvGajtOGCJhs1kyaTJ5fzaVzs0aPPYe38UZ/ukMFEA5iuRIjQJHIkH2niYO3ubQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"stylehacks": "^7.0.5"
|
||||
"stylehacks": "^7.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-merge-rules": {
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.8.tgz",
|
||||
"integrity": "sha512-BOR1iAM8jnr7zoQSlpeBmCsWV5Uudi/+5j7k05D0O/WP3+OFMPD86c1j/20xiuRtyt45bhxw/7hnhZNhW2mNFA==",
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.10.tgz",
|
||||
"integrity": "sha512-UXYKxkg8Cy1so/evF7AE/25PNXZb3E0SrvjdbtbGf+MW+doLenKqRLQzz6YZW469ktiXK2MVLFWtel/DftCV0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist": "^4.28.2",
|
||||
"caniuse-api": "^3.0.0",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"cssnano-utils": "^5.0.2",
|
||||
"postcss-selector-parser": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-minify-font-values": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz",
|
||||
"integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.2.tgz",
|
||||
"integrity": "sha512-Z82NUmnvhPrvMUaHfkaAVBmWQq9F8Dox4Dy0LiwbaTxfmDUWLQtS+0WCgKViwdWCPPajiY9YzoQftgqKdXkM5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2804,52 +2804,54 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-minify-gradients": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.2.tgz",
|
||||
"integrity": "sha512-fVY3AB8Um7SJR5usHqTY2Ngf9qh8IRN+FFzrBP0ONJy6yYXsP7xyjK2BvSAIrpgs1cST+H91V0TXi3diHLYJtw==",
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.4.tgz",
|
||||
"integrity": "sha512-g8MNeNyN+lbwKy8DCtJ6zU6awBL0InBsSOaKmgZ1MdRLVItLQUNFNAzzzBnOp4qowOcyyB6GetTlQ0/0UNXvag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colordx/core": "^5.0.0",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"@colordx/core": "^5.2.0",
|
||||
"cssnano-utils": "^5.0.2",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-minify-params": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.6.tgz",
|
||||
"integrity": "sha512-YOn02gC68JijlaXVuKvFSCvQOhTpblkcfDre2hb/Aaa58r2BIaK4AtE/cyZf2wV7YKAG+UlP9DT+By0ry1E4VQ==",
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.8.tgz",
|
||||
"integrity": "sha512-DIUKM5DZGTmxN7KFKT+rxt0FdPDmRrdK/k3n3+6Po+N/QYn06juwagHcfOVBG0CfCHwcnI612GAUCZc3eT+ZEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.2",
|
||||
"cssnano-utils": "^5.0.2",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-minify-selectors": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.1.0.tgz",
|
||||
"integrity": "sha512-HYl/6I0aL+UvpA10t65BSa7h+tVjBgE6oRI5N/3ylX3vtwvlDL67G3FT3vYDPnTksxr0riiyJcT0tBtyRVoloA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-minify-selectors": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.6.tgz",
|
||||
"integrity": "sha512-lIbC0jy3AAwDxEgciZlBullDiMBeBCT+fz5G8RcA9MWqh/hfUkpOI3vNDUNEZHgokaoiv0juB9Y8fGcON7rU/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-api": "^3.0.0",
|
||||
"cssesc": "^3.0.0",
|
||||
"postcss-selector-parser": "^7.1.1"
|
||||
},
|
||||
@@ -2857,26 +2859,26 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-charset": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz",
|
||||
"integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.2.tgz",
|
||||
"integrity": "sha512-YoINoiR4YKlzfB95Y93b0DSxWy7FLw+1SADIaznMHb88AKizpzfF80tolmiDEbYr1UM4r4Hw+NZq37SwT5f3uw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-display-values": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz",
|
||||
"integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.2.tgz",
|
||||
"integrity": "sha512-wu/NTSjnp8sX5TnEHVPN+eScjAtRs18ELtEduG+Ek3GxjeUDUT+VAA3PJjVIXBcVIk6fiLYFj2iKH0q99S3T2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2886,13 +2888,13 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-positions": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz",
|
||||
"integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.3.tgz",
|
||||
"integrity": "sha512-1CJI++oA3yK/fQlPUcEngUfcSWS08Pkt9fK+jVgL53mmtHDBHi0YiuB0m3D9BXwZjmfvCc2GQmFqCAF/CVcPzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2902,13 +2904,13 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-repeat-style": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz",
|
||||
"integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.3.tgz",
|
||||
"integrity": "sha512-RvImJ2Ml4LZSx31qC2C8LDiz65IgBNATtwEr9r3Aue+D0cCGbj4rjNojb/uGpEm4QxnOTzFqMvaDYuKiT1Cmpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2918,13 +2920,13 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-string": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz",
|
||||
"integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.2.tgz",
|
||||
"integrity": "sha512-FqtrUh2BU2MnVeLeWBbJ2rwOjuDnA91XvoImc1BbgMWIxdxiPTaquflBHsmFBA3xh3pC3wPZO9W5MaIc7wU/Xw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2934,13 +2936,13 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-timing-functions": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz",
|
||||
"integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.2.tgz",
|
||||
"integrity": "sha512-5H5fpXBnMACEXzn7k9RP7qWZ1eWg8cuZkUuFygStY7icOj+UucwMWXeMmdkF/iITvTVa7fP85tdRCJeznpdFfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2950,30 +2952,30 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-unicode": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.6.tgz",
|
||||
"integrity": "sha512-z6bwTV84YW6ZvvNoaNLuzRW4/uWxDKYI1iIDrzk6D2YTL7hICApy+Q1LP6vBEsljX8FM7YSuV9qI79XESd4ddQ==",
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.8.tgz",
|
||||
"integrity": "sha512-imCM3cwK3hvlAG4z1AzYM24m8BPA3/Jk/S71wfbn2I6+E2b+UwFaGvlNqydihXTSl3OFPeQXztqCzg+NGeSibQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist": "^4.28.2",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-url": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz",
|
||||
"integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.2.tgz",
|
||||
"integrity": "sha512-bLnNY7t76NLRb9QQyCVmCN4qwoHxiq6vABH/CXav9wTuR6dNGHGQ72AyO/+h2quWxZk3l7BqxNL1vtDi9H6y1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2983,13 +2985,13 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-whitespace": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz",
|
||||
"integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.2.tgz",
|
||||
"integrity": "sha512-TNSVkuhkeOhl36WruQlflxOb7HweoeZowSusNpfsM1+ZvqJ24Mc+xksu05ecMQxlu+0zgI8pyznO2EWqDCQbLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2999,30 +3001,30 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-ordered-values": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz",
|
||||
"integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.3.tgz",
|
||||
"integrity": "sha512-FTt6R9RF7NAYfpOHa2XFPm89FVuo5GiIbcfwOXFy1MYF38BeiNW9ke8ybw9Pk62eEsUlRVVbxHWA3B7ERYqOOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"cssnano-utils": "^5.0.2",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-reduce-idents": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-7.0.1.tgz",
|
||||
"integrity": "sha512-CHwIHGaPitJUWY/LLz/jKNI/Zq+KWhH1kfj0SDCTrSQQmcO4fwJ/vkifLTsRhWP6/256MvCHY+RJR3sPwtgA/g==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-7.0.3.tgz",
|
||||
"integrity": "sha512-LWtWV2J6cPqZjJ1xzcoLZ1o5KDrX7dmiRR4m7rHb5NLc79RJ8cG7jyCJidhRGWIqCyMxCrtsqa7A1z9dw+n5AA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3032,30 +3034,30 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-reduce-initial": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.6.tgz",
|
||||
"integrity": "sha512-G6ZyK68AmrPdMB6wyeA37ejnnRG2S8xinJrZJnOv+IaRKf6koPAVbQsiC7MfkmXaGmF1UO+QCijb27wfpxuRNg==",
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.8.tgz",
|
||||
"integrity": "sha512-VeVRmbgpgTZuRcDQdqnsB4iYTeS2dBRV07UdwK6V3x61F1xTQ2pgIzHBIR4rQYRlXRNKBTGYYhEL1eNA7w9vaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.28.1",
|
||||
"browserslist": "^4.28.2",
|
||||
"caniuse-api": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-reduce-transforms": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz",
|
||||
"integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.2.tgz",
|
||||
"integrity": "sha512-OV5P9hMnf7kEkeXVXyS5ESqxbIls7a3TqFymUAV5JICO/9YCBEU+QQhQjZiDHaLwFdV7/CL481kVeBUk5FdY3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3065,7 +3067,7 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-reporter": {
|
||||
@@ -3110,9 +3112,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-svgo": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.1.tgz",
|
||||
"integrity": "sha512-zU9H9oEDrUFKa0JB7w+IYL7Qs9ey1mZyjhbf0KLxwJDdDRtoPvCmaEfknzqfHj44QS9VD6c5sJnBAVYTLRg/Sg==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.2.tgz",
|
||||
"integrity": "sha512-ixExc8m+/68yuSYQzV/1DgtTup/7nI2dN9eiDS5GMRUzeCH4q9UcqeZPwcSVhdf8ay9fRwXDUHwcY5/XzQSszQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3123,13 +3125,13 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-unique-selectors": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.5.tgz",
|
||||
"integrity": "sha512-3QoYmEt4qg/rUWDn6Tc8+ZVPmbp4G1hXDtCNWDx0st8SjtCbRcxRXDDM1QrEiXGG3A45zscSJFb4QH90LViyxg==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.6.tgz",
|
||||
"integrity": "sha512-cDxnYw1QuBMW5w3svZ0BlYF0IA4Amr+1JoTLXzu6vDFPNwohN2QU+sPZNx15b930LR7ce+/600h28/cYoxO9vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3139,7 +3141,7 @@
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-url": {
|
||||
@@ -3192,9 +3194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
|
||||
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3319,9 +3321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
|
||||
"integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
@@ -3390,20 +3392,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.5.tgz",
|
||||
"integrity": "sha512-5kNb7V37BNf0Q3w+1pxfa+oiNPS++/b4Jil9e/kPDgrk1zjEd6uR7SZeJiYaLYH6RRSC1XX2/37OTeU/4FvuIA==",
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.10.tgz",
|
||||
"integrity": "sha512-sRJ7klmhe/Fl5woJcbJUa2qP1Ueffsl1CQI4ePvqXLkZmcIuAt09aP9uT/FOFPqXh9Rh8M5UkgEnwTdTKn/Aag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.24.5",
|
||||
"postcss-selector-parser": "^7.1.0"
|
||||
"browserslist": "^4.28.2",
|
||||
"postcss-selector-parser": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >=22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4.32"
|
||||
"postcss": "^8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
|
||||
+4
-4
@@ -22,9 +22,9 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.5.0",
|
||||
"@commitlint/config-conventional": "^20.5.0",
|
||||
"baseline-browser-mapping": "^2.10.15",
|
||||
"cssnano": "^7.1.4",
|
||||
"cssnano-preset-advanced": "^7.0.12",
|
||||
"baseline-browser-mapping": "^2.10.20",
|
||||
"cssnano": "^7.1.5",
|
||||
"cssnano-preset-advanced": "^7.0.13",
|
||||
"esbuild": "^0.28.0",
|
||||
"husky": "^9.1.7",
|
||||
"playwright": "^1.52.0",
|
||||
@@ -32,7 +32,7 @@
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-import-url": "^7.2.0",
|
||||
"postcss-url": "^10.1.3",
|
||||
"prettier": "^3.8.1"
|
||||
"prettier": "^3.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^5.2.0",
|
||||
|
||||
@@ -54,6 +54,7 @@ User-agent: meta-externalagent
|
||||
User-agent: Meta-ExternalAgent
|
||||
User-agent: meta-externalfetcher
|
||||
User-agent: Meta-ExternalFetcher
|
||||
User-agent: meta-webindexer
|
||||
User-agent: MistralAI-User
|
||||
User-agent: MistralAI-User/1.0
|
||||
User-agent: MyCentralAIScraperBot
|
||||
|
||||
Reference in New Issue
Block a user