mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-08 09:38:45 +00:00
Compare commits
14 Commits
Xe/missing
...
Xe/osiris
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4092180626 | ||
|
|
03758405d3 | ||
|
|
eb78ccc30c | ||
|
|
4156f84020 | ||
|
|
76dcd21582 | ||
|
|
153da4f5ac | ||
|
|
89b6af05a3 | ||
|
|
9a711f1635 | ||
|
|
dabbe63bb6 | ||
|
|
0aed7d3688 | ||
|
|
2af731033c | ||
|
|
d9c4e37978 | ||
|
|
1eafebedbc | ||
|
|
115ee97d1d |
@@ -21,7 +21,9 @@
|
|||||||
"golang.go",
|
"golang.go",
|
||||||
"unifiedjs.vscode-mdx",
|
"unifiedjs.vscode-mdx",
|
||||||
"a-h.templ",
|
"a-h.templ",
|
||||||
"redhat.vscode-yaml"
|
"redhat.vscode-yaml",
|
||||||
|
"hashicorp.hcl",
|
||||||
|
"fredwangwang.vscode-hcl-format"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.env
|
||||||
|
*.deb
|
||||||
|
*.rpm
|
||||||
|
|
||||||
|
# Additional package locks
|
||||||
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
|
# Go binaries and test artifacts
|
||||||
|
main
|
||||||
|
*.test
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_store
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# how does this get here
|
||||||
|
doc/VERSION
|
||||||
|
|
||||||
|
web/static/js/*
|
||||||
|
!web/static/js/.gitignore
|
||||||
29
.github/workflows/docker-pr.yml
vendored
29
.github/workflows/docker-pr.yml
vendored
@@ -2,7 +2,7 @@ name: Docker image builds (pull requests)
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: ["main"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
|
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
|
||||||
@@ -11,7 +11,32 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
buildx-bake:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build
|
||||||
|
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
|
||||||
|
with:
|
||||||
|
source: .
|
||||||
|
push: true
|
||||||
|
sbom: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
set: |
|
||||||
|
osiris.tags=ttl.sh/techaro/pr-${{ github.event.number }}/osiris:24h
|
||||||
|
|
||||||
|
containerbuild:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
33
.github/workflows/docker.yml
vendored
33
.github/workflows/docker.yml
vendored
@@ -17,7 +17,38 @@ permissions:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
buildx-bake:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
|
|
||||||
|
- name: Log into registry
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build
|
||||||
|
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
|
||||||
|
with:
|
||||||
|
source: .
|
||||||
|
push: true
|
||||||
|
sbom: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
set: ""
|
||||||
|
|
||||||
|
containerbuild:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
4
.github/workflows/docs-deploy.yml
vendored
4
.github/workflows/docs-deploy.yml
vendored
@@ -53,14 +53,14 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
|
|
||||||
- name: Apply k8s manifests to limsa lominsa
|
- name: Apply k8s manifests to limsa lominsa
|
||||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||||
with:
|
with:
|
||||||
args: apply -k docs/manifest
|
args: apply -k docs/manifest
|
||||||
|
|
||||||
- name: Apply k8s manifests to limsa lominsa
|
- name: Apply k8s manifests to limsa lominsa
|
||||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/zizmor.yml
vendored
2
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
run: uvx zizmor --format sarif . > results.sarif
|
run: uvx zizmor --format sarif . > results.sarif
|
||||||
|
|||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -5,6 +5,8 @@
|
|||||||
"golang.go",
|
"golang.go",
|
||||||
"unifiedjs.vscode-mdx",
|
"unifiedjs.vscode-mdx",
|
||||||
"a-h.templ",
|
"a-h.templ",
|
||||||
"redhat.vscode-yaml"
|
"redhat.vscode-yaml",
|
||||||
|
"hashicorp.hcl",
|
||||||
|
"fredwangwang.vscode-hcl-format"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
39
cmd/osiris/internal/config/bind.go
Normal file
39
cmd/osiris/internal/config/bind.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidHostpost = errors.New("bind: invalid host:port")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Bind struct {
|
||||||
|
HTTP string `hcl:"http"`
|
||||||
|
HTTPS string `hcl:"https"`
|
||||||
|
Metrics string `hcl:"metrics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bind) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if _, _, err := net.SplitHostPort(b.HTTP); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.HTTP, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := net.SplitHostPort(b.HTTPS); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.HTTPS, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := net.SplitHostPort(b.Metrics); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidHostpost, b.Metrics, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
cmd/osiris/internal/config/bind_test.go
Normal file
55
cmd/osiris/internal/config/bind_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBindValid(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
precondition func(t *testing.T)
|
||||||
|
bind Bind
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
precondition: nil,
|
||||||
|
bind: Bind{
|
||||||
|
HTTP: ":8081",
|
||||||
|
HTTPS: ":8082",
|
||||||
|
Metrics: ":8083",
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ports",
|
||||||
|
precondition: func(t *testing.T) {
|
||||||
|
ln, err := net.Listen("tcp", ":8081")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { ln.Close() })
|
||||||
|
},
|
||||||
|
bind: Bind{
|
||||||
|
HTTP: "",
|
||||||
|
HTTPS: "",
|
||||||
|
Metrics: "",
|
||||||
|
},
|
||||||
|
err: ErrInvalidHostpost,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.precondition != nil {
|
||||||
|
tt.precondition(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tt.bind.Valid(); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("want: %v", tt.err)
|
||||||
|
t.Logf("got: %v", err)
|
||||||
|
t.Error("got wrong error from validation function")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
31
cmd/osiris/internal/config/config.go
Normal file
31
cmd/osiris/internal/config/config.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Toplevel struct {
|
||||||
|
Bind Bind `hcl:"bind,block"`
|
||||||
|
Domains []Domain `hcl:"domain,block"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Toplevel) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if err := t.Bind.Valid(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("invalid bind block:\n%w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range t.Domains {
|
||||||
|
if err := d.Valid(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("when parsing domain %s: %w", d.Name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return fmt.Errorf("invalid configuration file:\n%w", errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
66
cmd/osiris/internal/config/domain.go
Normal file
66
cmd/osiris/internal/config/domain.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidDomainName = errors.New("domain: name is invalid")
|
||||||
|
ErrInvalidDomainTLSConfig = errors.New("domain: TLS config is invalid")
|
||||||
|
ErrInvalidURL = errors.New("invalid URL")
|
||||||
|
ErrInvalidURLScheme = errors.New("URL has invalid scheme")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Domain struct {
|
||||||
|
Name string `hcl:"name,label"`
|
||||||
|
TLS TLS `hcl:"tls,block"`
|
||||||
|
Target string `hcl:"target"`
|
||||||
|
InsecureSkipVerify bool `hcl:"insecure_skip_verify,optional"`
|
||||||
|
HealthTarget string `hcl:"health_target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Domain) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if _, err := idna.Lookup.ToASCII(d.Name); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidDomainName, d.Name, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.TLS.Valid(); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidDomainTLSConfig, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := isURLValid(d.Target); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("target has %w %q: %w", ErrInvalidURL, d.Target, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := isURLValid(d.HealthTarget); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("health_target has %w %q: %w", ErrInvalidURL, d.HealthTarget, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isURLValid(input string) error {
|
||||||
|
u, err := url.Parse(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http", "https", "h2c", "unix":
|
||||||
|
// do nothing
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w %s has scheme %s (want http, https, h2c, unix)", ErrInvalidURLScheme, input, u.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
89
cmd/osiris/internal/config/domain_test.go
Normal file
89
cmd/osiris/internal/config/domain_test.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDomainValid(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
input Domain
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple happy path",
|
||||||
|
input: Domain{
|
||||||
|
Name: "anubis.techaro.lol",
|
||||||
|
TLS: TLS{
|
||||||
|
Cert: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
},
|
||||||
|
Target: "http://localhost:3000",
|
||||||
|
HealthTarget: "http://localhost:9091/healthz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid domain name",
|
||||||
|
input: Domain{
|
||||||
|
Name: "\uFFFD.techaro.lol",
|
||||||
|
TLS: TLS{
|
||||||
|
Cert: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
},
|
||||||
|
Target: "http://localhost:3000",
|
||||||
|
HealthTarget: "http://localhost:9091/healthz",
|
||||||
|
},
|
||||||
|
err: ErrInvalidDomainName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid tls config",
|
||||||
|
input: Domain{
|
||||||
|
Name: "anubis.techaro.lol",
|
||||||
|
TLS: TLS{
|
||||||
|
Cert: "./testdata/tls/invalid.crt",
|
||||||
|
Key: "./testdata/tls/invalid.key",
|
||||||
|
},
|
||||||
|
Target: "http://localhost:3000",
|
||||||
|
HealthTarget: "http://localhost:9091/healthz",
|
||||||
|
},
|
||||||
|
err: ErrInvalidDomainTLSConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid URL",
|
||||||
|
input: Domain{
|
||||||
|
Name: "anubis.techaro.lol",
|
||||||
|
TLS: TLS{
|
||||||
|
Cert: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
},
|
||||||
|
Target: "file://[::1:3000",
|
||||||
|
HealthTarget: "file://[::1:9091/healthz",
|
||||||
|
},
|
||||||
|
err: ErrInvalidURL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong URL scheme",
|
||||||
|
input: Domain{
|
||||||
|
Name: "anubis.techaro.lol",
|
||||||
|
TLS: TLS{
|
||||||
|
Cert: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
},
|
||||||
|
Target: "file://localhost:3000",
|
||||||
|
HealthTarget: "file://localhost:9091/healthz",
|
||||||
|
},
|
||||||
|
err: ErrInvalidURLScheme,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("want: %v", tt.err)
|
||||||
|
t.Logf("got: %v", err)
|
||||||
|
t.Error("got wrong error from validation function")
|
||||||
|
} else {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
1
cmd/osiris/internal/config/testdata/tls/invalid.crt
vendored
Normal file
1
cmd/osiris/internal/config/testdata/tls/invalid.crt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
|
||||||
1
cmd/osiris/internal/config/testdata/tls/invalid.key
vendored
Normal file
1
cmd/osiris/internal/config/testdata/tls/invalid.key
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
|
||||||
11
cmd/osiris/internal/config/testdata/tls/selfsigned.crt
vendored
Normal file
11
cmd/osiris/internal/config/testdata/tls/selfsigned.crt
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBnzCCAVGgAwIBAgIUAw8funCpiB3ZAAPoWdSCWnzbsFIwBQYDK2VwMEUxCzAJ
|
||||||
|
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||||
|
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MTkwMjM1WhcNMjUwODE3MTkwMjM1
|
||||||
|
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||||
|
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAcXDHXV3vgpvjtTaz
|
||||||
|
s0Oj/73rMr06bhyGGhleYS1MNoWjUzBRMB0GA1UdDgQWBBQwmfKPthucFHB6Wfgz
|
||||||
|
2Nj5nkMQOjAfBgNVHSMEGDAWgBQwmfKPthucFHB6Wfgz2Nj5nkMQOjAPBgNVHRMB
|
||||||
|
Af8EBTADAQH/MAUGAytlcANBALBYbULlGwB7Ro0UTgUoQDNxEvayn3qzVFHIt7lC
|
||||||
|
/2/NzNBkk4yPT+a4mbRuydxLkv+JIvmQbarZxpksYnWlCAM=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
3
cmd/osiris/internal/config/testdata/tls/selfsigned.key
vendored
Normal file
3
cmd/osiris/internal/config/testdata/tls/selfsigned.key
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIOHKoX22Mha6SnnpLm34fSSfTUDbRiDCi6N1nOgTOlds
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
40
cmd/osiris/internal/config/tls.go
Normal file
40
cmd/osiris/internal/config/tls.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCantReadTLS = errors.New("tls: can't read TLS")
|
||||||
|
ErrInvalidTLSKeypair = errors.New("tls: can't parse TLS keypair")
|
||||||
|
)
|
||||||
|
|
||||||
|
type TLS struct {
|
||||||
|
Cert string `hcl:"cert"`
|
||||||
|
Key string `hcl:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TLS) Valid() error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if _, err := os.Stat(t.Cert); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w certificate %s: %w", ErrCantReadTLS, t.Cert, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(t.Key); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w key %s: %w", ErrCantReadTLS, t.Key, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tls.LoadX509KeyPair(t.Cert, t.Key); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("%w (%s, %s): %w", ErrInvalidTLSKeypair, t.Cert, t.Key, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
48
cmd/osiris/internal/config/tls_test.go
Normal file
48
cmd/osiris/internal/config/tls_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTLSValid(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
input TLS
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple selfsigned",
|
||||||
|
input: TLS{
|
||||||
|
Cert: "./testdata/tls/selfsigned.crt",
|
||||||
|
Key: "./testdata/tls/selfsigned.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "files don't exist",
|
||||||
|
input: TLS{
|
||||||
|
Cert: "./testdata/tls/nonexistent.crt",
|
||||||
|
Key: "./testdata/tls/nonexistent.key",
|
||||||
|
},
|
||||||
|
err: ErrCantReadTLS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid keypair",
|
||||||
|
input: TLS{
|
||||||
|
Cert: "./testdata/tls/invalid.crt",
|
||||||
|
Key: "./testdata/tls/invalid.key",
|
||||||
|
},
|
||||||
|
err: ErrInvalidTLSKeypair,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("want: %v", tt.err)
|
||||||
|
t.Logf("got: %v", err)
|
||||||
|
t.Error("got wrong error from validation function")
|
||||||
|
} else {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
85
cmd/osiris/internal/entrypoint/entrypoint.go
Normal file
85
cmd/osiris/internal/entrypoint/entrypoint.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
ConfigFname string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main(ctx context.Context, opts Options) error {
|
||||||
|
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||||
|
|
||||||
|
var cfg config.Toplevel
|
||||||
|
if err := hclsimple.DecodeFile(opts.ConfigFname, nil, &cfg); err != nil {
|
||||||
|
return fmt.Errorf("can't read configuration file %s:\n\n%w", opts.ConfigFname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Valid(); err != nil {
|
||||||
|
return fmt.Errorf("configuration file %s is invalid:\n\n%w", opts.ConfigFname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rtr, err := NewRouter(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rtr.opts = opts
|
||||||
|
go rtr.backgroundReloadConfig(ctx)
|
||||||
|
|
||||||
|
g, gCtx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
// HTTP
|
||||||
|
g.Go(func() error {
|
||||||
|
ln, err := net.Listen("tcp", cfg.Bind.HTTP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(HTTP) can't bind to tcp %s: %w", cfg.Bind.HTTP, err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
<-ctx.Done()
|
||||||
|
ln.Close()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
slog.Info("listening", "for", "http", "bind", cfg.Bind.HTTP)
|
||||||
|
|
||||||
|
return rtr.HandleHTTP(gCtx, ln)
|
||||||
|
})
|
||||||
|
|
||||||
|
// HTTPS
|
||||||
|
g.Go(func() error {
|
||||||
|
ln, err := net.Listen("tcp", cfg.Bind.HTTPS)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(https) can't bind to tcp %s: %w", cfg.Bind.HTTPS, err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
<-ctx.Done()
|
||||||
|
ln.Close()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
slog.Info("listening", "for", "https", "bind", cfg.Bind.HTTPS)
|
||||||
|
|
||||||
|
return rtr.HandleHTTPS(gCtx, ln)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
g.Go(func() error {
|
||||||
|
return rtr.ListenAndServeMetrics(gCtx, cfg.Bind.Metrics)
|
||||||
|
})
|
||||||
|
|
||||||
|
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
|
return g.Wait()
|
||||||
|
}
|
||||||
93
cmd/osiris/internal/entrypoint/entrypoint_test.go
Normal file
93
cmd/osiris/internal/entrypoint/entrypoint_test.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMainGoodConfig(t *testing.T) {
|
||||||
|
files, err := os.ReadDir("./testdata/good")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range files {
|
||||||
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(t.Context())
|
||||||
|
cfg := loadConfig(t, filepath.Join("testdata", "good", st.Name()))
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
if err := Main(ctx, Options{
|
||||||
|
ConfigFname: filepath.Join("testdata", "good", st.Name()),
|
||||||
|
}); err != nil {
|
||||||
|
var netOpErr *net.OpError
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, context.Canceled):
|
||||||
|
// Context was canceled, this is expected
|
||||||
|
return
|
||||||
|
case errors.As(err, &netOpErr):
|
||||||
|
// Network operation error occurred
|
||||||
|
t.Logf("Network operation error: %v", netOpErr)
|
||||||
|
return
|
||||||
|
case errors.Is(err, http.ErrServerClosed):
|
||||||
|
// Server was closed, this is expected
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Other unexpected error
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
wait := 5 * time.Millisecond
|
||||||
|
|
||||||
|
for i := range make([]struct{}, 10) {
|
||||||
|
if i != 0 {
|
||||||
|
time.Sleep(wait)
|
||||||
|
wait = wait * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("try %d (wait=%s)", i+1, wait)
|
||||||
|
|
||||||
|
resp, err := http.Get("http://localhost" + cfg.Bind.Metrics + "/readyz")
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("router initialization did not work")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMainBadConfig(t *testing.T) {
|
||||||
|
files, err := os.ReadDir("./testdata/bad")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, st := range files {
|
||||||
|
t.Run(st.Name(), func(t *testing.T) {
|
||||||
|
if err := Main(t.Context(), Options{
|
||||||
|
ConfigFname: filepath.Join("testdata", "bad", st.Name()),
|
||||||
|
}); err == nil {
|
||||||
|
t.Error("wanted an error but got none")
|
||||||
|
} else {
|
||||||
|
t.Log(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
35
cmd/osiris/internal/entrypoint/h2c.go
Normal file
35
cmd/osiris/internal/entrypoint/h2c.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newH2CReverseProxy(target *url.URL) *httputil.ReverseProxy {
|
||||||
|
target.Scheme = "http"
|
||||||
|
|
||||||
|
director := func(req *http.Request) {
|
||||||
|
req.URL.Scheme = target.Scheme
|
||||||
|
req.URL.Host = target.Host
|
||||||
|
req.Host = target.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use h2c transport
|
||||||
|
transport := &http2.Transport{
|
||||||
|
AllowHTTP: true,
|
||||||
|
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
// Just do plain TCP (h2c)
|
||||||
|
return net.Dial(network, addr)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &httputil.ReverseProxy{
|
||||||
|
Director: director,
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
51
cmd/osiris/internal/entrypoint/h2c_test.go
Normal file
51
cmd/osiris/internal/entrypoint/h2c_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newH2cServer(t *testing.T, h http.Handler) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
h2s := &http2.Server{}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(h2c.NewHandler(h, h2s))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
srv.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestH2CReverseProxy(t *testing.T) {
|
||||||
|
h := &ackHandler{}
|
||||||
|
|
||||||
|
srv := newH2cServer(t, h)
|
||||||
|
|
||||||
|
u, err := url.Parse(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rp := httptest.NewServer(newH2CReverseProxy(u))
|
||||||
|
defer rp.Close()
|
||||||
|
|
||||||
|
resp, err := rp.Client().Get(rp.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("wrong status code from reverse proxy: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.ack {
|
||||||
|
t.Error("h2c handler was not executed")
|
||||||
|
}
|
||||||
|
}
|
||||||
72
cmd/osiris/internal/entrypoint/metrics.go
Normal file
72
cmd/osiris/internal/entrypoint/metrics.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func healthz(w http.ResponseWriter, r *http.Request) {
|
||||||
|
services, err := internal.HealthSrv.List(r.Context(), nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("can't get list of services", "err", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []string
|
||||||
|
for k := range services.Statuses {
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var msg bytes.Buffer
|
||||||
|
|
||||||
|
var healthy bool = true
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
st := services.Statuses[k].GetStatus()
|
||||||
|
fmt.Fprintf(&msg, "%s: %s\n", k, st)
|
||||||
|
switch st {
|
||||||
|
case healthv1.HealthCheckResponse_SERVING:
|
||||||
|
// do nothing
|
||||||
|
default:
|
||||||
|
healthy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !healthy {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(msg.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func readyz(w http.ResponseWriter, r *http.Request) {
|
||||||
|
st, ok := internal.GetHealth("osiris")
|
||||||
|
if !ok {
|
||||||
|
slog.Error("health service osiris does not exist, file a bug")
|
||||||
|
http.Error(w, "health service osiris does not exist", http.StatusExpectationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch st {
|
||||||
|
case healthv1.HealthCheckResponse_NOT_SERVING:
|
||||||
|
http.Error(w, "NOT OK", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
case healthv1.HealthCheckResponse_SERVING:
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
http.Error(w, "UNKNOWN", http.StatusFailedDependency)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
66
cmd/osiris/internal/entrypoint/metrics_test.go
Normal file
66
cmd/osiris/internal/entrypoint/metrics_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
healthv1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealthz(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(healthz))
|
||||||
|
|
||||||
|
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||||
|
|
||||||
|
resp, err := srv.Client().Get(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
t.Errorf("wanted not ready but got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
|
resp, err = srv.Client().Get(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("wanted ready but got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadyz(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(readyz))
|
||||||
|
|
||||||
|
internal.SetHealth("osiris", healthv1.HealthCheckResponse_NOT_SERVING)
|
||||||
|
|
||||||
|
resp, err := srv.Client().Get(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
t.Errorf("wanted not ready but got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
|
resp, err = srv.Client().Get(srv.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("wanted ready but got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
320
cmd/osiris/internal/entrypoint/router.go
Normal file
320
cmd/osiris/internal/entrypoint/router.go
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/TecharoHQ/anubis/internal/fingerprint"
|
||||||
|
"github.com/felixge/httpsnoop"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||||
|
"github.com/lum8rjack/go-ja4h"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTargetInvalid = errors.New("[unexpected] target invalid")
|
||||||
|
ErrNoHandler = errors.New("[unexpected] no handler for domain")
|
||||||
|
ErrInvalidTLSKeypair = errors.New("[unexpected] invalid TLS keypair")
|
||||||
|
ErrNoCert = errors.New("this server does not have a certificate for that domain")
|
||||||
|
|
||||||
|
requestsPerDomain = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: "techaro",
|
||||||
|
Subsystem: "osiris",
|
||||||
|
Name: "request_count",
|
||||||
|
}, []string{"domain", "method", "response_code"})
|
||||||
|
|
||||||
|
responseTime = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
|
Namespace: "techaro",
|
||||||
|
Subsystem: "osiris",
|
||||||
|
Name: "response_time",
|
||||||
|
}, []string{"domain"})
|
||||||
|
|
||||||
|
unresolvedRequests = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Namespace: "techaro",
|
||||||
|
Subsystem: "osiris",
|
||||||
|
Name: "unresolved_requests",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
lock sync.RWMutex
|
||||||
|
routes map[string]http.Handler
|
||||||
|
tlsCerts map[string]*tls.Certificate
|
||||||
|
opts Options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) setConfig(c config.Toplevel) error {
|
||||||
|
var errs []error
|
||||||
|
newMap := map[string]http.Handler{}
|
||||||
|
newCerts := map[string]*tls.Certificate{}
|
||||||
|
|
||||||
|
for _, d := range c.Domains {
|
||||||
|
var domainErrs []error
|
||||||
|
|
||||||
|
u, err := url.Parse(d.Target)
|
||||||
|
if err != nil {
|
||||||
|
domainErrs = append(domainErrs, fmt.Errorf("%w %q: %v", ErrTargetInvalid, d.Target, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var h http.Handler
|
||||||
|
|
||||||
|
if u != nil {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http", "https":
|
||||||
|
rp := httputil.NewSingleHostReverseProxy(u)
|
||||||
|
|
||||||
|
if d.InsecureSkipVerify {
|
||||||
|
rp.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h = rp
|
||||||
|
case "h2c":
|
||||||
|
h = newH2CReverseProxy(u)
|
||||||
|
case "unix":
|
||||||
|
h = &httputil.ReverseProxy{
|
||||||
|
Director: func(r *http.Request) {
|
||||||
|
r.URL.Scheme = "http"
|
||||||
|
r.URL.Host = d.Name
|
||||||
|
r.Host = d.Name
|
||||||
|
},
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return net.Dial("unix", strings.TrimPrefix(d.Target, "unix://"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h == nil {
|
||||||
|
domainErrs = append(domainErrs, ErrNoHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMap[d.Name] = h
|
||||||
|
|
||||||
|
cert, err := tls.LoadX509KeyPair(d.TLS.Cert, d.TLS.Key)
|
||||||
|
if err != nil {
|
||||||
|
domainErrs = append(domainErrs, fmt.Errorf("%w: %w", ErrInvalidTLSKeypair, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
newCerts[d.Name] = &cert
|
||||||
|
|
||||||
|
if len(domainErrs) != 0 {
|
||||||
|
errs = append(errs, fmt.Errorf("invalid domain %s: %w", d.Name, errors.Join(domainErrs...)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
return fmt.Errorf("can't compile config to routing map: %w", errors.Join(errs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
rtr.lock.Lock()
|
||||||
|
rtr.routes = newMap
|
||||||
|
rtr.tlsCerts = newCerts
|
||||||
|
rtr.lock.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
rtr.lock.RLock()
|
||||||
|
cert, ok := rtr.tlsCerts[hello.ServerName]
|
||||||
|
rtr.lock.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNoCert
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) loadConfig() error {
|
||||||
|
slog.Info("reloading config", "fname", rtr.opts.ConfigFname)
|
||||||
|
var cfg config.Toplevel
|
||||||
|
if err := hclsimple.DecodeFile(rtr.opts.ConfigFname, nil, &cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rtr.setConfig(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("done!")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) backgroundReloadConfig(ctx context.Context) {
|
||||||
|
t := time.NewTicker(time.Hour)
|
||||||
|
defer t.Stop()
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, syscall.SIGHUP)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
if err := rtr.loadConfig(); err != nil {
|
||||||
|
slog.Error("can't reload config", "fname", rtr.opts.ConfigFname, "err", err)
|
||||||
|
}
|
||||||
|
case <-ch:
|
||||||
|
if err := rtr.loadConfig(); err != nil {
|
||||||
|
slog.Error("can't reload config", "fname", rtr.opts.ConfigFname, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(c config.Toplevel) (*Router, error) {
|
||||||
|
result := &Router{
|
||||||
|
routes: map[string]http.Handler{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := result.setConfig(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) HandleHTTP(ctx context.Context, ln net.Listener) error {
|
||||||
|
srv := http.Server{
|
||||||
|
Handler: rtr,
|
||||||
|
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
<-ctx.Done()
|
||||||
|
srv.Close()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
return srv.Serve(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) HandleHTTPS(ctx context.Context, ln net.Listener) error {
|
||||||
|
tc := &tls.Config{
|
||||||
|
GetCertificate: rtr.GetCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: rtr,
|
||||||
|
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||||
|
TLSConfig: tc,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
<-ctx.Done()
|
||||||
|
srv.Close()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
fingerprint.ApplyTLSFingerprinter(srv)
|
||||||
|
|
||||||
|
return srv.ServeTLS(ln, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) ListenAndServeMetrics(ctx context.Context, addr string) error {
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("(metrics) can't bind to tcp %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
<-ctx.Done()
|
||||||
|
ln.Close()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.Handle("/metrics", promhttp.Handler())
|
||||||
|
mux.HandleFunc("/readyz", readyz)
|
||||||
|
mux.HandleFunc("/healthz", healthz)
|
||||||
|
|
||||||
|
slog.Info("listening", "for", "metrics", "bind", addr)
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ErrorLog: internal.GetFilteredHTTPLogger(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
<-ctx.Done()
|
||||||
|
srv.Close()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
return srv.Serve(ln)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var host = r.Host
|
||||||
|
|
||||||
|
if strings.Contains(host, ":") {
|
||||||
|
host, _, _ = net.SplitHostPort(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
var h http.Handler
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
ja4hFP := ja4h.JA4H(r)
|
||||||
|
|
||||||
|
slog.Info("got request", "method", r.Method, "host", host, "path", r.URL.Path)
|
||||||
|
|
||||||
|
rtr.lock.RLock()
|
||||||
|
h, ok = rtr.routes[host]
|
||||||
|
rtr.lock.RUnlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
unresolvedRequests.Inc()
|
||||||
|
http.NotFound(w, r) // TODO(Xe): brand this
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("X-Http-Ja4h-Fingerprint", ja4hFP)
|
||||||
|
|
||||||
|
if fp := fingerprint.GetTLSFingerprint(r); fp != nil {
|
||||||
|
if ja3n := fp.JA3N(); ja3n != nil {
|
||||||
|
r.Header.Set("X-Tls-Ja3n-Fingerprint", ja3n.String())
|
||||||
|
}
|
||||||
|
if ja4 := fp.JA4(); ja4 != nil {
|
||||||
|
r.Header.Set("X-Tls-Ja4-Fingerprint", ja4.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcpFP := fingerprint.GetTCPFingerprint(r); tcpFP != nil {
|
||||||
|
r.Header.Set("X-Tcp-Ja4t-Fingerprint", tcpFP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
m := httpsnoop.CaptureMetrics(h, w, r)
|
||||||
|
|
||||||
|
requestsPerDomain.WithLabelValues(host, r.Method, fmt.Sprint(m.Code)).Inc()
|
||||||
|
responseTime.WithLabelValues(host).Observe(float64(m.Duration.Milliseconds()))
|
||||||
|
|
||||||
|
slog.Info("request completed", "host", host, "method", r.Method, "response_code", m.Code, "duration_ms", m.Duration.Milliseconds())
|
||||||
|
}
|
||||||
319
cmd/osiris/internal/entrypoint/router_test.go
Normal file
319
cmd/osiris/internal/entrypoint/router_test.go
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
package entrypoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
|
||||||
|
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadConfig(t *testing.T, fname string) config.Toplevel {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var cfg config.Toplevel
|
||||||
|
if err := hclsimple.DecodeFile(fname, nil, &cfg); err != nil {
|
||||||
|
t.Fatalf("can't read configuration file %s: %v", fname, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Valid(); err != nil {
|
||||||
|
t.Errorf("configuration file %s is invalid: %v", "./testdata/selfsigned.hcl", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouter(t *testing.T, cfg config.Toplevel) *Router {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
rtr, err := NewRouter(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRouter(t *testing.T) {
|
||||||
|
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
|
||||||
|
rtr := newRouter(t, cfg)
|
||||||
|
|
||||||
|
srv := httptest.NewServer(rtr)
|
||||||
|
defer srv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRouterFails(t *testing.T) {
|
||||||
|
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
|
||||||
|
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "test1.internal",
|
||||||
|
TLS: config.TLS{
|
||||||
|
Cert: "./testdata/tls/invalid.crt",
|
||||||
|
Key: "./testdata/tls/invalid.key",
|
||||||
|
},
|
||||||
|
Target: cfg.Domains[0].Target,
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
|
||||||
|
rtr, err := NewRouter(cfg)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("wanted an error but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(rtr)
|
||||||
|
defer srv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterSetConfig(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
configFname string
|
||||||
|
mutation func(cfg config.Toplevel) config.Toplevel
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic",
|
||||||
|
configFname: "./testdata/good/selfsigned.hcl",
|
||||||
|
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||||
|
return cfg
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all schemes",
|
||||||
|
configFname: "./testdata/good/selfsigned.hcl",
|
||||||
|
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "http.internal",
|
||||||
|
TLS: cfg.Domains[0].TLS,
|
||||||
|
Target: "http://[::1]:3000",
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "https.internal",
|
||||||
|
TLS: cfg.Domains[0].TLS,
|
||||||
|
Target: "https://[::1]:3000",
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "h2c.internal",
|
||||||
|
TLS: cfg.Domains[0].TLS,
|
||||||
|
Target: "h2c://[::1]:3000",
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "unix.internal",
|
||||||
|
TLS: cfg.Domains[0].TLS,
|
||||||
|
Target: "unix://foo.sock",
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid TLS",
|
||||||
|
configFname: "./testdata/good/selfsigned.hcl",
|
||||||
|
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "test1.internal",
|
||||||
|
TLS: config.TLS{
|
||||||
|
Cert: "./testdata/tls/invalid.crt",
|
||||||
|
Key: "./testdata/tls/invalid.key",
|
||||||
|
},
|
||||||
|
Target: cfg.Domains[0].Target,
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
},
|
||||||
|
err: ErrInvalidTLSKeypair,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "target is not a valid URL",
|
||||||
|
configFname: "./testdata/good/selfsigned.hcl",
|
||||||
|
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "test1.internal",
|
||||||
|
TLS: cfg.Domains[0].TLS,
|
||||||
|
Target: "http://[::1:443",
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
},
|
||||||
|
err: ErrTargetInvalid,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid target scheme",
|
||||||
|
configFname: "./testdata/good/selfsigned.hcl",
|
||||||
|
mutation: func(cfg config.Toplevel) config.Toplevel {
|
||||||
|
cfg.Domains = append(cfg.Domains, config.Domain{
|
||||||
|
Name: "test1.internal",
|
||||||
|
TLS: cfg.Domains[0].TLS,
|
||||||
|
Target: "foo://",
|
||||||
|
HealthTarget: cfg.Domains[0].HealthTarget,
|
||||||
|
})
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
},
|
||||||
|
err: ErrNoHandler,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg := loadConfig(t, tt.configFname)
|
||||||
|
rtr := newRouter(t, cfg)
|
||||||
|
|
||||||
|
cfg = tt.mutation(cfg)
|
||||||
|
|
||||||
|
if err := rtr.setConfig(cfg); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("want: %v", tt.err)
|
||||||
|
t.Logf("got: %v", err)
|
||||||
|
t.Error("got wrong error from rtr.setConfig function")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ackHandler struct {
|
||||||
|
ack bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *ackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ah.ack = true
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ah *ackHandler) Reset() {
|
||||||
|
ah.ack = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnixServer(t *testing.T, h http.Handler) string {
|
||||||
|
sockName := filepath.Join(t.TempDir(), "s")
|
||||||
|
ln, err := net.Listen("unix", sockName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("can't listen on %s: %v", sockName, err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ln.Close()
|
||||||
|
os.Remove(sockName)
|
||||||
|
})
|
||||||
|
|
||||||
|
go func(ctx context.Context) {
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: h,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
srv.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
srv.Serve(ln)
|
||||||
|
}(t.Context())
|
||||||
|
|
||||||
|
return "unix://" + sockName
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGetCertificate(t *testing.T) {
|
||||||
|
cfg := loadConfig(t, "./testdata/good/selfsigned.hcl")
|
||||||
|
rtr := newRouter(t, cfg)
|
||||||
|
|
||||||
|
for _, tt := range []struct {
|
||||||
|
domainName string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
domainName: "osiris.local.cetacean.club",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domainName: "whacky-fun.local",
|
||||||
|
err: ErrNoCert,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.domainName, func(t *testing.T) {
|
||||||
|
if _, err := rtr.GetCertificate(&tls.ClientHelloInfo{ServerName: tt.domainName}); !errors.Is(err, tt.err) {
|
||||||
|
t.Logf("want: %v", tt.err)
|
||||||
|
t.Logf("got: %v", err)
|
||||||
|
t.Error("got wrong error from rtr.GetCertificate")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterServeAllProtocols(t *testing.T) {
|
||||||
|
cfg := loadConfig(t, "./testdata/good/all_protocols.hcl")
|
||||||
|
|
||||||
|
httpAckHandler := &ackHandler{}
|
||||||
|
httpsAckHandler := &ackHandler{}
|
||||||
|
h2cAckHandler := &ackHandler{}
|
||||||
|
unixAckHandler := &ackHandler{}
|
||||||
|
|
||||||
|
httpSrv := httptest.NewServer(httpAckHandler)
|
||||||
|
httpsSrv := httptest.NewTLSServer(httpsAckHandler)
|
||||||
|
h2cSrv := newH2cServer(t, h2cAckHandler)
|
||||||
|
unixPath := newUnixServer(t, unixAckHandler)
|
||||||
|
|
||||||
|
cfg.Domains[0].Target = httpSrv.URL
|
||||||
|
cfg.Domains[1].Target = httpsSrv.URL
|
||||||
|
cfg.Domains[2].Target = strings.ReplaceAll(h2cSrv.URL, "http:", "h2c:")
|
||||||
|
cfg.Domains[3].Target = unixPath
|
||||||
|
|
||||||
|
// enc := json.NewEncoder(os.Stderr)
|
||||||
|
// enc.SetIndent("", " ")
|
||||||
|
// enc.Encode(cfg)
|
||||||
|
|
||||||
|
rtr := newRouter(t, cfg)
|
||||||
|
|
||||||
|
cli := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("plain http", func(t *testing.T) {
|
||||||
|
ln, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ln.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
go rtr.HandleHTTP(t.Context(), ln)
|
||||||
|
|
||||||
|
serverURL := "http://" + ln.Addr().String()
|
||||||
|
t.Log(serverURL)
|
||||||
|
|
||||||
|
for _, d := range cfg.Domains {
|
||||||
|
t.Run(d.Name, func(t *testing.T) {
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, serverURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Host = d.Name
|
||||||
|
|
||||||
|
resp, err := cli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("wrong status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
0
cmd/osiris/internal/entrypoint/testdata/bad/empty.hcl
vendored
Normal file
0
cmd/osiris/internal/entrypoint/testdata/bad/empty.hcl
vendored
Normal file
15
cmd/osiris/internal/entrypoint/testdata/bad/invalid.hcl
vendored
Normal file
15
cmd/osiris/internal/entrypoint/testdata/bad/invalid.hcl
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
bind {
|
||||||
|
http = ":65530"
|
||||||
|
https = ":65531"
|
||||||
|
metrics = ":65532"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "osiris.local.cetacean.club" {
|
||||||
|
tls {
|
||||||
|
cert = "./testdata/invalid.crt"
|
||||||
|
key = "./testdata/invalid.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "http://localhost:3000"
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
46
cmd/osiris/internal/entrypoint/testdata/good/all_protocols.hcl
vendored
Normal file
46
cmd/osiris/internal/entrypoint/testdata/good/all_protocols.hcl
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
bind {
|
||||||
|
http = ":65520"
|
||||||
|
https = ":65521"
|
||||||
|
metrics = ":65522"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "http.internal" {
|
||||||
|
tls {
|
||||||
|
cert = "./testdata/selfsigned.crt"
|
||||||
|
key = "./testdata/selfsigned.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "http://localhost:65510" # XXX(Xe) this is overwritten
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "https.internal" {
|
||||||
|
tls {
|
||||||
|
cert = "./testdata/selfsigned.crt"
|
||||||
|
key = "./testdata/selfsigned.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "https://localhost:65511" # XXX(Xe) this is overwritten
|
||||||
|
insecure_skip_verify = true
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "h2c.internal" {
|
||||||
|
tls {
|
||||||
|
cert = "./testdata/selfsigned.crt"
|
||||||
|
key = "./testdata/selfsigned.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "h2c://localhost:65511" # XXX(Xe) this is overwritten
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "unix.internal" {
|
||||||
|
tls {
|
||||||
|
cert = "./testdata/selfsigned.crt"
|
||||||
|
key = "./testdata/selfsigned.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "http://localhost:65511" # XXX(Xe) this is overwritten
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
15
cmd/osiris/internal/entrypoint/testdata/good/selfsigned.hcl
vendored
Normal file
15
cmd/osiris/internal/entrypoint/testdata/good/selfsigned.hcl
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
bind {
|
||||||
|
http = ":65530"
|
||||||
|
https = ":65531"
|
||||||
|
metrics = ":65532"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "osiris.local.cetacean.club" {
|
||||||
|
tls {
|
||||||
|
cert = "./testdata/selfsigned.crt"
|
||||||
|
key = "./testdata/selfsigned.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "http://localhost:3000"
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
11
cmd/osiris/internal/entrypoint/testdata/selfsigned.crt
vendored
Normal file
11
cmd/osiris/internal/entrypoint/testdata/selfsigned.crt
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBnzCCAVGgAwIBAgIUOLTjSYOjFk00IemtFTC4oEZs988wBQYDK2VwMEUxCzAJ
|
||||||
|
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||||
|
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MjEyNDIzWhcNMjUwODE3MjEyNDIz
|
||||||
|
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||||
|
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAPHphABS15+4VV6R1
|
||||||
|
vYzBQYIycQmOmlbA8QcfwzuB2VajUzBRMB0GA1UdDgQWBBT2s+MQ4AR6cbK4V0+d
|
||||||
|
XZnok1orhDAfBgNVHSMEGDAWgBT2s+MQ4AR6cbK4V0+dXZnok1orhDAPBgNVHRMB
|
||||||
|
Af8EBTADAQH/MAUGAytlcANBAOdoJbRMnHmkEETzVtXP+jkAI9yQNRXujnglApGP
|
||||||
|
8I5pvIYVgYCgoQrnb4haVWFldHM1T9H698n19e/egfFb+w4=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
3
cmd/osiris/internal/entrypoint/testdata/selfsigned.key
vendored
Normal file
3
cmd/osiris/internal/entrypoint/testdata/selfsigned.key
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIBop42tiZ0yzhaKo9NAc0PlAyBsE8NAE0i9Z7s2lgZuR
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
43
cmd/osiris/main.go
Normal file
43
cmd/osiris/main.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/cmd/osiris/internal/entrypoint"
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
"github.com/facebookgo/flagenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configFname = flag.String("config", "./osiris.hcl", "Configuration file (HCL), see docs")
|
||||||
|
slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)")
|
||||||
|
versionFlag = flag.Bool("version", false, "if true, show version information then quit")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flagenv.Parse()
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *versionFlag {
|
||||||
|
fmt.Println("Osiris", anubis.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
internal.InitSlog(*slogLevel)
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := entrypoint.Main(ctx, entrypoint.Options{
|
||||||
|
ConfigFname: *configFname,
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
cmd/osiris/osiris.hcl
Normal file
15
cmd/osiris/osiris.hcl
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
bind {
|
||||||
|
http = ":3004"
|
||||||
|
https = ":3005"
|
||||||
|
metrics = ":9091"
|
||||||
|
}
|
||||||
|
|
||||||
|
domain "osiris.local.cetacean.club" {
|
||||||
|
tls {
|
||||||
|
cert = "./internal/config/testdata/tls/selfsigned.crt"
|
||||||
|
key = "./internal/config/testdata/tls/selfsigned.key"
|
||||||
|
}
|
||||||
|
|
||||||
|
target = "http://localhost:3000"
|
||||||
|
health_target = "http://localhost:9091/healthz"
|
||||||
|
}
|
||||||
32
docker-bake.hcl
Normal file
32
docker-bake.hcl
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
variable "ALPINE_VERSION" { default = "3.22" }
|
||||||
|
variable "GITHUB_SHA" { default = "devel" }
|
||||||
|
variable "VERSION" { default = "devel-docker" }
|
||||||
|
|
||||||
|
group "default" {
|
||||||
|
targets = [
|
||||||
|
"osiris",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "osiris" {
|
||||||
|
args = {
|
||||||
|
ALPINE_VERSION = "3.22"
|
||||||
|
VERSION = "${VERSION}"
|
||||||
|
}
|
||||||
|
context = "."
|
||||||
|
dockerfile = "./docker/osiris.Dockerfile"
|
||||||
|
platforms = [
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm64",
|
||||||
|
"linux/arm/v7",
|
||||||
|
"linux/ppc64le",
|
||||||
|
"linux/riscv64",
|
||||||
|
]
|
||||||
|
pull = true
|
||||||
|
sbom = true
|
||||||
|
provenance = true
|
||||||
|
tags = [
|
||||||
|
"ghcr.io/techarohq/anubis/osiris:${VERSION}",
|
||||||
|
"ghcr.io/techarohq/anubis/osiris:main"
|
||||||
|
]
|
||||||
|
}
|
||||||
30
docker/osiris.Dockerfile
Normal file
30
docker/osiris.Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
ARG ALPINE_VERSION=edge
|
||||||
|
FROM --platform=${BUILDPLATFORM} alpine:${ALPINE_VERSION} AS build
|
||||||
|
|
||||||
|
RUN apk -U add go nodejs git build-base git npm bash zstd brotli gzip
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/root/go go mod download
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG VERSION=devel-docker
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/root/go GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 GOARM=7 go build -gcflags "all=-N -l" -o /app/bin/osiris -ldflags "-s -w -extldflags -static -X github.com/TecharoHQ/anubis.Version=${VERSION}" ./cmd/osiris
|
||||||
|
|
||||||
|
FROM alpine:${ALPINE_VERSION} AS run
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk -U add ca-certificates mailcap
|
||||||
|
|
||||||
|
COPY --from=build /app/bin/osiris /app/bin/osiris
|
||||||
|
|
||||||
|
CMD ["/app/bin/osiris"]
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/TecharoHQ/anubis"
|
||||||
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
<!-- This changes the project to: -->
|
<!-- This changes the project to: -->
|
||||||
|
|
||||||
- Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)).
|
- Expired records are now properly removed from bbolt databases ([#848](https://github.com/TecharoHQ/anubis/pull/848)).
|
||||||
|
|
||||||
- Fix hanging on service restart ([#853](https://github.com/TecharoHQ/anubis/issues/853))
|
- Fix hanging on service restart ([#853](https://github.com/TecharoHQ/anubis/issues/853))
|
||||||
@@ -22,6 +23,8 @@ Anubis now supports these new languages:
|
|||||||
|
|
||||||
- [Czech](https://github.com/TecharoHQ/anubis/pull/849)
|
- [Czech](https://github.com/TecharoHQ/anubis/pull/849)
|
||||||
|
|
||||||
|
Anubis now supports the [`missingHeader`](./admin/configuration/expressions.mdx#missingHeader) to assert the absence of headers in requests.
|
||||||
|
|
||||||
## v1.21.0: Minfilia Warde
|
## v1.21.0: Minfilia Warde
|
||||||
|
|
||||||
> Please, be at ease. You are among friends here.
|
> Please, be at ease. You are among friends here.
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ For example, consider this rule:
|
|||||||
|
|
||||||
For this rule, if a request comes in from `8.8.8.8` or `1.1.1.1`, Anubis will deny the request and return an error page.
|
For this rule, if a request comes in from `8.8.8.8` or `1.1.1.1`, Anubis will deny the request and return an error page.
|
||||||
|
|
||||||
#### `all` blocks
|
### `all` blocks
|
||||||
|
|
||||||
An `all` block that contains a list of expressions. If all expressions in the list return `true`, then the action specified in the rule will be taken. If any of the expressions in the list returns `false`, Anubis will move on to the next rule.
|
An `all` block that contains a list of expressions. If all expressions in the list return `true`, then the action specified in the rule will be taken. If any of the expressions in the list returns `false`, Anubis will move on to the next rule.
|
||||||
|
|
||||||
@@ -186,8 +186,32 @@ Also keep in mind that this does not account for other kinds of latency like I/O
|
|||||||
|
|
||||||
Anubis expressions can be augmented with the following functions:
|
Anubis expressions can be augmented with the following functions:
|
||||||
|
|
||||||
|
### `missingHeader`
|
||||||
|
|
||||||
|
Available in `bot` expressions.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function missingHeader(headers: Record<string, string>, key: string) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
`missingHeader` returns `true` if the request does not contain a header. This is useful when you are trying to assert behavior such as:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Adds weight to old versions of Chrome
|
||||||
|
- name: old-chrome
|
||||||
|
action: WEIGH
|
||||||
|
weight:
|
||||||
|
adjust: 10
|
||||||
|
expression:
|
||||||
|
all:
|
||||||
|
- userAgent.matches("Chrome/[1-9][0-9]?\\.0\\.0\\.0")
|
||||||
|
- missingHeader(headers, "Sec-Ch-Ua")
|
||||||
|
```
|
||||||
|
|
||||||
### `randInt`
|
### `randInt`
|
||||||
|
|
||||||
|
Available in all expressions.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
function randInt(n: int): int;
|
function randInt(n: int): int;
|
||||||
```
|
```
|
||||||
|
|||||||
29
go.mod
29
go.mod
@@ -4,12 +4,12 @@ go 1.24.2
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/TecharoHQ/thoth-proto v0.4.0
|
github.com/TecharoHQ/thoth-proto v0.4.0
|
||||||
github.com/a-h/templ v0.3.906
|
github.com/a-h/templ v0.3.920
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
github.com/facebookgo/flagenv v0.0.0-20160425205200-fcd59fca7456
|
||||||
github.com/gaissmai/bart v0.20.5
|
github.com/gaissmai/bart v0.22.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||||
github.com/google/cel-go v0.25.0
|
github.com/google/cel-go v0.26.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2
|
||||||
@@ -21,20 +21,20 @@ require (
|
|||||||
github.com/redis/go-redis/v9 v9.11.0
|
github.com/redis/go-redis/v9 v9.11.0
|
||||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||||
github.com/shirou/gopsutil/v4 v4.25.6
|
github.com/shirou/gopsutil/v4 v4.25.6
|
||||||
github.com/testcontainers/testcontainers-go v0.37.0
|
github.com/testcontainers/testcontainers-go v0.38.0
|
||||||
go.etcd.io/bbolt v1.4.2
|
go.etcd.io/bbolt v1.4.2
|
||||||
golang.org/x/net v0.42.0
|
golang.org/x/net v0.42.0
|
||||||
golang.org/x/text v0.27.0
|
golang.org/x/text v0.27.0
|
||||||
google.golang.org/grpc v1.73.0
|
google.golang.org/grpc v1.73.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/apimachinery v0.33.2
|
k8s.io/apimachinery v0.33.3
|
||||||
sigs.k8s.io/yaml v1.5.0
|
sigs.k8s.io/yaml v1.5.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 // indirect
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 // indirect
|
||||||
cel.dev/expr v0.23.1 // indirect
|
cel.dev/expr v0.24.0 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||||
@@ -47,8 +47,10 @@ require (
|
|||||||
github.com/Songmu/gitconfig v0.2.0 // indirect
|
github.com/Songmu/gitconfig v0.2.0 // indirect
|
||||||
github.com/TecharoHQ/yeet v0.6.0 // indirect
|
github.com/TecharoHQ/yeet v0.6.0 // indirect
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||||
|
github.com/agext/levenshtein v1.2.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||||
@@ -56,6 +58,8 @@ require (
|
|||||||
github.com/cli/browser v1.3.0 // indirect
|
github.com/cli/browser v1.3.0 // indirect
|
||||||
github.com/cli/go-gh v0.1.0 // indirect
|
github.com/cli/go-gh v0.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
@@ -66,7 +70,7 @@ require (
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/docker/docker v28.0.1+incompatible // indirect
|
github.com/docker/docker v28.2.2+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
|
github.com/dop251/goja v0.0.0-20250309171923-bcd7cc6bf64c // indirect
|
||||||
@@ -91,6 +95,7 @@ require (
|
|||||||
github.com/goccy/go-yaml v1.12.0 // indirect
|
github.com/goccy/go-yaml v1.12.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/go-github/v70 v70.0.0 // indirect
|
github.com/google/go-github/v70 v70.0.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||||
@@ -99,6 +104,7 @@ require (
|
|||||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||||
github.com/goreleaser/nfpm/v2 v2.42.1 // indirect
|
github.com/goreleaser/nfpm/v2 v2.42.1 // indirect
|
||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
|
github.com/hashicorp/hcl/v2 v2.24.0 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
@@ -109,11 +115,13 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/moby/go-archive v0.1.0 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
github.com/moby/sys/sequential v0.5.0 // indirect
|
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||||
github.com/moby/sys/user v0.1.0 // indirect
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.0 // indirect
|
github.com/moby/term v0.5.0 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
@@ -147,6 +155,7 @@ require (
|
|||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
github.com/zclconf/go-cty v1.16.3 // indirect
|
||||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||||
|
|||||||
66
go.sum
66
go.sum
@@ -2,12 +2,12 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeX
|
|||||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
|
||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
|
||||||
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
|
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||||
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
@@ -40,14 +40,18 @@ github.com/TecharoHQ/yeet v0.6.0 h1:RCBAjr7wIlllsgy0tpvWpLX7jsZgu2tiuBY3RrprcR0=
|
|||||||
github.com/TecharoHQ/yeet v0.6.0/go.mod h1:bj2V4Fg8qKQXoiuPZa3HuawrE8g+LsOQv/9q2WyGSsA=
|
github.com/TecharoHQ/yeet v0.6.0/go.mod h1:bj2V4Fg8qKQXoiuPZa3HuawrE8g+LsOQv/9q2WyGSsA=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||||
github.com/a-h/templ v0.3.906 h1:ZUThc8Q9n04UATaCwaG60pB1AqbulLmYEAMnWV63svg=
|
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||||
github.com/a-h/templ v0.3.906/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||||
|
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
|
||||||
|
github.com/a-h/templ v0.3.920/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||||
|
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -77,6 +81,10 @@ github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5
|
|||||||
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
|
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||||
@@ -101,8 +109,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
|
||||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
@@ -133,8 +141,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gaissmai/bart v0.20.5 h1:ehoWZWQ7j//qt0K0Zs4i9hpoPpbgqsMQiR8W2QPJh+c=
|
github.com/gaissmai/bart v0.22.0 h1:+yR2mCpZx8H8GlqA+Icqi7/Iwx2/OUbO4bVbsORK0ns=
|
||||||
github.com/gaissmai/bart v0.20.5/go.mod h1:cEed+ge8dalcbpi8wtS9x9m2hn/fNJH5suhdGQOHnYk=
|
github.com/gaissmai/bart v0.22.0/go.mod h1:RpLtt3lWq1BoRz3AAyDAJ7jhLWBkYhVCfi+ximB2t68=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
@@ -175,14 +183,14 @@ github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
|
|||||||
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
|
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||||
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
|
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
|
||||||
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
|
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
@@ -221,6 +229,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1ns
|
|||||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||||
|
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
|
||||||
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
|
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
@@ -274,16 +284,22 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
|
|||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||||
|
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||||
|
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
|
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||||
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
|
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||||
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
|
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
|
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||||
|
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||||
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
@@ -373,8 +389,8 @@ github.com/suzuki-shunsuke/pinact v1.6.0 h1:2QvSzREOquwLwKXhF9Hj0AInE/Rl63SZz9dK
|
|||||||
github.com/suzuki-shunsuke/pinact v1.6.0/go.mod h1:FDUMck0mmL0mcnNZ23Vjh/aOR5cIdZhF1IIpGksT4dQ=
|
github.com/suzuki-shunsuke/pinact v1.6.0/go.mod h1:FDUMck0mmL0mcnNZ23Vjh/aOR5cIdZhF1IIpGksT4dQ=
|
||||||
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUHP+OyvrzSCvd8m9iUcaK8=
|
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4 h1:YGHgrVjGTYHY98II6zijXUHP+OyvrzSCvd8m9iUcaK8=
|
||||||
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY=
|
github.com/suzuki-shunsuke/urfave-cli-help-all v0.0.4/go.mod h1:sSi6xaUaHfaqu32ECLeyE7NTMv+ZM5dW0JikhllaalY=
|
||||||
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
|
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
|
||||||
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
|
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
|
||||||
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
@@ -395,6 +411,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=
|
||||||
|
github.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
|
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
|
||||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
|
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
|
||||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||||
@@ -549,12 +567,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||||
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
|
||||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||||
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
|
mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw=
|
||||||
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
|
mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg=
|
||||||
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
|
pault.ag/go/debian v0.18.0 h1:nr0iiyOU5QlG1VPnhZLNhnCcHx58kukvBJp+dvaM6CQ=
|
||||||
|
|||||||
97
internal/fingerprint/ja3n.go
Normal file
97
internal/fingerprint/ja3n.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLSFingerprintJA3N represents a JA3N fingerprint
|
||||||
|
type TLSFingerprintJA3N [md5.Size]byte
|
||||||
|
|
||||||
|
func (f TLSFingerprintJA3N) String() string {
|
||||||
|
return hex.EncodeToString(f[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildJA3N(hello *tls.ClientHelloInfo, sortExtensions bool) TLSFingerprintJA3N {
|
||||||
|
buf := make([]byte, 0, 256)
|
||||||
|
|
||||||
|
{
|
||||||
|
var sslVersion uint16
|
||||||
|
var hasGrease bool
|
||||||
|
for _, v := range hello.SupportedVersions {
|
||||||
|
if v&greaseMask != greaseValue {
|
||||||
|
if v > sslVersion {
|
||||||
|
sslVersion = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasGrease = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maximum TLS 1.2 as specified on JA3, as TLS 1.3 is put in SupportedVersions
|
||||||
|
if slices.Contains(hello.Extensions, extensionSupportedVersions) && hasGrease && sslVersion > tls.VersionTLS12 {
|
||||||
|
sslVersion = tls.VersionTLS12
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = strconv.AppendUint(buf, uint64(sslVersion), 10)
|
||||||
|
buf = append(buf, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
for _, cipher := range hello.CipherSuites {
|
||||||
|
//if !slices.Contains(greaseValues[:], cipher) {
|
||||||
|
if cipher&greaseMask != greaseValue {
|
||||||
|
buf = strconv.AppendUint(buf, uint64(cipher), 10)
|
||||||
|
buf = append(buf, '-')
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = buf[:len(buf)-n]
|
||||||
|
buf = append(buf, ',')
|
||||||
|
n = 0
|
||||||
|
|
||||||
|
extensions := hello.Extensions
|
||||||
|
if sortExtensions {
|
||||||
|
extensions = slices.Clone(extensions)
|
||||||
|
slices.Sort(extensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, extension := range extensions {
|
||||||
|
if extension&greaseMask != greaseValue {
|
||||||
|
buf = strconv.AppendUint(buf, uint64(extension), 10)
|
||||||
|
buf = append(buf, '-')
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = buf[:len(buf)-n]
|
||||||
|
buf = append(buf, ',')
|
||||||
|
n = 0
|
||||||
|
|
||||||
|
for _, curve := range hello.SupportedCurves {
|
||||||
|
if curve&greaseMask != greaseValue {
|
||||||
|
buf = strconv.AppendUint(buf, uint64(curve), 10)
|
||||||
|
buf = append(buf, '-')
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = buf[:len(buf)-n]
|
||||||
|
buf = append(buf, ',')
|
||||||
|
n = 0
|
||||||
|
|
||||||
|
for _, point := range hello.SupportedPoints {
|
||||||
|
buf = strconv.AppendUint(buf, uint64(point), 10)
|
||||||
|
buf = append(buf, '-')
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = buf[:len(buf)-n]
|
||||||
|
|
||||||
|
sum := md5.Sum(buf)
|
||||||
|
return TLSFingerprintJA3N(sum[:])
|
||||||
|
}
|
||||||
176
internal/fingerprint/ja4.go
Normal file
176
internal/fingerprint/ja4.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLSFingerprintJA4 represents a JA4 fingerprint
|
||||||
|
type TLSFingerprintJA4 struct {
|
||||||
|
A [10]byte
|
||||||
|
B [6]byte
|
||||||
|
C [6]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *TLSFingerprintJA4) String() string {
|
||||||
|
if f == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join([]string{
|
||||||
|
string(f.A[:]),
|
||||||
|
hex.EncodeToString(f.B[:]),
|
||||||
|
hex.EncodeToString(f.C[:]),
|
||||||
|
}, "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildJA4(hello *tls.ClientHelloInfo) (ja4 TLSFingerprintJA4) {
|
||||||
|
buf := make([]byte, 0, 36)
|
||||||
|
|
||||||
|
hasQuic := false
|
||||||
|
|
||||||
|
for _, ext := range hello.Extensions {
|
||||||
|
if ext == extensionQUICTransportParameters {
|
||||||
|
hasQuic = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch hasQuic {
|
||||||
|
case true:
|
||||||
|
buf = append(buf, 'q')
|
||||||
|
case false:
|
||||||
|
buf = append(buf, 't')
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var sslVersion uint16
|
||||||
|
for _, v := range hello.SupportedVersions {
|
||||||
|
if v&greaseMask != greaseValue {
|
||||||
|
if v > sslVersion {
|
||||||
|
sslVersion = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sslVersion {
|
||||||
|
case tls.VersionTLS10:
|
||||||
|
buf = append(buf, '1', '0')
|
||||||
|
case tls.VersionTLS11:
|
||||||
|
buf = append(buf, '1', '1')
|
||||||
|
case tls.VersionTLS12:
|
||||||
|
buf = append(buf, '1', '2')
|
||||||
|
case tls.VersionTLS13:
|
||||||
|
buf = append(buf, '1', '3')
|
||||||
|
default:
|
||||||
|
sslVersion -= 0x0201
|
||||||
|
buf = strconv.AppendUint(buf, uint64(sslVersion>>8), 10)
|
||||||
|
buf = strconv.AppendUint(buf, uint64(sslVersion&0xff), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(hello.Extensions, extensionServerName) && hello.ServerName != "" {
|
||||||
|
buf = append(buf, 'd')
|
||||||
|
} else {
|
||||||
|
buf = append(buf, 'i')
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphers := make([]uint16, 0, len(hello.CipherSuites))
|
||||||
|
for _, cipher := range hello.CipherSuites {
|
||||||
|
if cipher&greaseMask != greaseValue {
|
||||||
|
ciphers = append(ciphers, cipher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionCount := 0
|
||||||
|
extensions := make([]uint16, 0, len(hello.Extensions))
|
||||||
|
for _, extension := range hello.Extensions {
|
||||||
|
if extension&greaseMask != greaseValue {
|
||||||
|
extensionCount++
|
||||||
|
if extension != extensionALPN && extension != extensionServerName {
|
||||||
|
extensions = append(extensions, extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schemes := make([]tls.SignatureScheme, 0, len(hello.SignatureSchemes))
|
||||||
|
|
||||||
|
for _, scheme := range hello.SignatureSchemes {
|
||||||
|
if scheme&greaseMask != greaseValue {
|
||||||
|
schemes = append(schemes, scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: maybe little endian
|
||||||
|
slices.Sort(ciphers)
|
||||||
|
slices.Sort(extensions)
|
||||||
|
//slices.Sort(schemes)
|
||||||
|
|
||||||
|
if len(ciphers) < 10 {
|
||||||
|
buf = append(buf, '0')
|
||||||
|
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
|
||||||
|
} else if len(ciphers) > 99 {
|
||||||
|
buf = append(buf, '9', '9')
|
||||||
|
} else {
|
||||||
|
buf = strconv.AppendUint(buf, uint64(len(ciphers)), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
if extensionCount < 10 {
|
||||||
|
buf = append(buf, '0')
|
||||||
|
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
|
||||||
|
} else if extensionCount > 99 {
|
||||||
|
buf = append(buf, '9', '9')
|
||||||
|
} else {
|
||||||
|
buf = strconv.AppendUint(buf, uint64(extensionCount), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hello.SupportedProtos) > 0 && len(hello.SupportedProtos[0]) > 1 {
|
||||||
|
buf = append(buf, hello.SupportedProtos[0][0], hello.SupportedProtos[0][len(hello.SupportedProtos[0])-1])
|
||||||
|
} else {
|
||||||
|
buf = append(buf, '0', '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(ja4.A[:], buf)
|
||||||
|
|
||||||
|
ja4.B = ja4SHA256(uint16SliceToHex(ciphers))
|
||||||
|
|
||||||
|
extBuf := uint16SliceToHex(extensions)
|
||||||
|
|
||||||
|
if len(schemes) > 0 {
|
||||||
|
extBuf = append(extBuf, '_')
|
||||||
|
extBuf = append(extBuf, uint16SliceToHex(schemes)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ja4.C = ja4SHA256(extBuf)
|
||||||
|
|
||||||
|
return ja4
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint16SliceToHex[T ~uint16](in []T) (out []byte) {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
out = slices.Grow(out, hex.EncodedLen(len(in)*2)+len(in))
|
||||||
|
|
||||||
|
for _, n := range in {
|
||||||
|
out = append(out, fmt.Sprintf("%04x", uint16(n))...)
|
||||||
|
out = append(out, ',')
|
||||||
|
}
|
||||||
|
out = out[:len(out)-1]
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func ja4SHA256(buf []byte) [6]byte {
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return [6]byte{0, 0, 0, 0, 0, 0}
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(buf)
|
||||||
|
|
||||||
|
return [6]byte(sum[:6])
|
||||||
|
}
|
||||||
46
internal/fingerprint/tcp.go
Normal file
46
internal/fingerprint/tcp.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JA4T represents a TCP fingerprint
|
||||||
|
type JA4T struct {
|
||||||
|
Window uint32
|
||||||
|
Options []uint8
|
||||||
|
MSS uint16
|
||||||
|
WindowScale uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j JA4T) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
// Start with the window size
|
||||||
|
fmt.Fprintf(&sb, "%d", j.Window)
|
||||||
|
|
||||||
|
// Append each option
|
||||||
|
for i, opt := range j.Options {
|
||||||
|
if i == 0 {
|
||||||
|
fmt.Fprint(&sb, "_")
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(&sb, "-")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "%d", opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append MSS and WindowScale
|
||||||
|
fmt.Fprintf(&sb, "_%d_%d", j.MSS, j.WindowScale)
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTCPFingerprint extracts TCP fingerprint from HTTP request context
|
||||||
|
func GetTCPFingerprint(r *http.Request) *JA4T {
|
||||||
|
ptr := r.Context().Value(tcpFingerprintKey{})
|
||||||
|
if fpPtr, ok := ptr.(*JA4T); ok && ptr != nil && fpPtr != nil {
|
||||||
|
return fpPtr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
106
internal/fingerprint/tcp_freebsd.go
Normal file
106
internal/fingerprint/tcp_freebsd.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
//go:build freebsd
|
||||||
|
|
||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tcpInfo struct {
|
||||||
|
State uint8
|
||||||
|
Options uint8
|
||||||
|
SndScale uint8
|
||||||
|
RcvScale uint8
|
||||||
|
__pad [4]byte
|
||||||
|
Rto uint32
|
||||||
|
Ato uint32
|
||||||
|
SndMss uint32
|
||||||
|
RcvMss uint32
|
||||||
|
Unacked uint32
|
||||||
|
Sacked uint32
|
||||||
|
Lost uint32
|
||||||
|
Retrans uint32
|
||||||
|
Fackets uint32
|
||||||
|
Last_data_sent uint32
|
||||||
|
Last_ack_sent uint32
|
||||||
|
Last_data_recv uint32
|
||||||
|
Last_ack_recv uint32
|
||||||
|
Pmtu uint32
|
||||||
|
Rcv_ssthresh uint32
|
||||||
|
RTT uint32
|
||||||
|
RTTvar uint32
|
||||||
|
Snd_ssthresh uint32
|
||||||
|
Snd_cwnd uint32
|
||||||
|
Advmss uint32
|
||||||
|
Reordering uint32
|
||||||
|
Rcv_rtt uint32
|
||||||
|
Rcv_space uint32
|
||||||
|
Total_retrans uint32
|
||||||
|
Snd_wnd uint32
|
||||||
|
// Truncated for brevity — add more fields if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignTCPFingerprint extracts TCP fingerprint information from a connection
|
||||||
|
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
|
||||||
|
tcpConn, ok := conn.(*net.TCPConn)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("not a TCPConn")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConn, err := tcpConn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SyscallConn failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info tcpInfo
|
||||||
|
var sysErr error
|
||||||
|
|
||||||
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
|
size := uint32(unsafe.Sizeof(info))
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_GETSOCKOPT,
|
||||||
|
fd,
|
||||||
|
uintptr(syscall.IPPROTO_TCP),
|
||||||
|
uintptr(syscall.TCP_INFO),
|
||||||
|
uintptr(unsafe.Pointer(&info)),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
sysErr = errno
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SyscallConn.Control: %w", err)
|
||||||
|
}
|
||||||
|
if sysErr != nil {
|
||||||
|
return nil, fmt.Errorf("getsockopt TCP_INFO: %w", sysErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fp := &JA4T{
|
||||||
|
Window: info.Snd_wnd,
|
||||||
|
MSS: uint16(info.SndMss),
|
||||||
|
WindowScale: info.SndScale,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TCPI_OPT_TIMESTAMPS = 1 << 0
|
||||||
|
TCPI_OPT_SACK = 1 << 1
|
||||||
|
TCPI_OPT_WSCALE = 1 << 2
|
||||||
|
)
|
||||||
|
|
||||||
|
if info.Options&TCPI_OPT_SACK != 0 {
|
||||||
|
fp.Options = append(fp.Options, 4, 1)
|
||||||
|
}
|
||||||
|
if info.Options&TCPI_OPT_TIMESTAMPS != 0 {
|
||||||
|
fp.Options = append(fp.Options, 8, 1)
|
||||||
|
}
|
||||||
|
if info.Options&TCPI_OPT_WSCALE != 0 {
|
||||||
|
fp.Options = append(fp.Options, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
132
internal/fingerprint/tcp_linux.go
Normal file
132
internal/fingerprint/tcp_linux.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tcpInfo struct {
|
||||||
|
State uint8
|
||||||
|
Ca_state uint8
|
||||||
|
Retransmits uint8
|
||||||
|
Probes uint8
|
||||||
|
Backoff uint8
|
||||||
|
Options uint8
|
||||||
|
Wnd_scale uint8
|
||||||
|
Delivery_rate_app_limited uint8
|
||||||
|
|
||||||
|
Rto uint32
|
||||||
|
Ato uint32
|
||||||
|
SndMss uint32
|
||||||
|
RcvMss uint32
|
||||||
|
|
||||||
|
Unacked uint32
|
||||||
|
Sacked uint32
|
||||||
|
Lost uint32
|
||||||
|
Retrans uint32
|
||||||
|
Fackets uint32
|
||||||
|
|
||||||
|
Last_data_sent uint32
|
||||||
|
Last_ack_sent uint32
|
||||||
|
Last_data_recv uint32
|
||||||
|
Last_ack_recv uint32
|
||||||
|
PMTU uint32
|
||||||
|
Rcv_ssthresh uint32
|
||||||
|
RTT uint32
|
||||||
|
RTTvar uint32
|
||||||
|
Snd_ssthresh uint32
|
||||||
|
Snd_cwnd uint32
|
||||||
|
Advmss uint32
|
||||||
|
Reordering uint32
|
||||||
|
Rcv_rtt uint32
|
||||||
|
Rcv_space uint32
|
||||||
|
Total_retrans uint32
|
||||||
|
Pacing_rate uint64
|
||||||
|
Max_pacing_rate uint64
|
||||||
|
Bytes_acked uint64
|
||||||
|
Bytes_received uint64
|
||||||
|
Segs_out uint32
|
||||||
|
Segs_in uint32
|
||||||
|
Notsent_bytes uint32
|
||||||
|
Min_rtt uint32
|
||||||
|
Data_segs_in uint32
|
||||||
|
Data_segs_out uint32
|
||||||
|
Delivery_rate uint64
|
||||||
|
Busy_time uint64
|
||||||
|
Rwnd_limited uint64
|
||||||
|
Sndbuf_limited uint64
|
||||||
|
Delivered uint32
|
||||||
|
Delivered_ce uint32
|
||||||
|
Bytes_sent uint64
|
||||||
|
Bytes_retrans uint64
|
||||||
|
DSACK_dups uint32
|
||||||
|
Reord_seen uint32
|
||||||
|
Rcv_ooopack uint32
|
||||||
|
Snd_wnd uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssignTCPFingerprint extracts TCP fingerprint information from a connection
|
||||||
|
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
|
||||||
|
tcpConn, ok := conn.(*net.TCPConn)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("not a TCPConn")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConn, err := tcpConn.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SyscallConn failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info tcpInfo
|
||||||
|
var sysErr error
|
||||||
|
|
||||||
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
|
size := uint32(unsafe.Sizeof(info))
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_GETSOCKOPT,
|
||||||
|
fd,
|
||||||
|
uintptr(syscall.IPPROTO_TCP),
|
||||||
|
uintptr(syscall.TCP_INFO),
|
||||||
|
uintptr(unsafe.Pointer(&info)),
|
||||||
|
uintptr(unsafe.Pointer(&size)),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
sysErr = errno
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("SyscallConn.Control: %w", err)
|
||||||
|
}
|
||||||
|
if sysErr != nil {
|
||||||
|
return nil, fmt.Errorf("getsockopt TCP_INFO: %w", sysErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fp := &JA4T{
|
||||||
|
Window: info.Snd_wnd,
|
||||||
|
MSS: uint16(info.SndMss),
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TCPI_OPT_TIMESTAMPS = 1 << 0
|
||||||
|
TCPI_OPT_SACK = 1 << 1
|
||||||
|
TCPI_OPT_WSCALE = 1 << 2
|
||||||
|
)
|
||||||
|
|
||||||
|
if info.Options&TCPI_OPT_SACK != 0 {
|
||||||
|
fp.Options = append(fp.Options, 4, 1)
|
||||||
|
}
|
||||||
|
if info.Options&TCPI_OPT_TIMESTAMPS != 0 {
|
||||||
|
fp.Options = append(fp.Options, 8, 1)
|
||||||
|
}
|
||||||
|
if info.Options&TCPI_OPT_WSCALE != 0 {
|
||||||
|
fp.Options = append(fp.Options, 3)
|
||||||
|
fp.WindowScale = info.Wnd_scale
|
||||||
|
}
|
||||||
|
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
11
internal/fingerprint/tcp_unsupported.go
Normal file
11
internal/fingerprint/tcp_unsupported.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !linux && !freebsd
|
||||||
|
|
||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
// AssignTCPFingerprint is not supported on this platform
|
||||||
|
func AssignTCPFingerprint(conn net.Conn) (*JA4T, error) {
|
||||||
|
// Not supported on macOS and other platforms
|
||||||
|
return &JA4T{}, nil
|
||||||
|
}
|
||||||
110
internal/fingerprint/tls.go
Normal file
110
internal/fingerprint/tls.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package fingerprint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyTLSFingerprinter configures a TLS server to capture TLS fingerprints
|
||||||
|
func ApplyTLSFingerprinter(server *http.Server) {
|
||||||
|
if server.TLSConfig == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.TLSConfig = server.TLSConfig.Clone()
|
||||||
|
|
||||||
|
getConfigForClient := server.TLSConfig.GetConfigForClient
|
||||||
|
|
||||||
|
if getConfigForClient == nil {
|
||||||
|
getConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.TLSConfig.GetConfigForClient = func(clientHello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
ja3n, ja4 := buildTLSFingerprint(clientHello)
|
||||||
|
ptr := clientHello.Context().Value(tlsFingerprintKey{})
|
||||||
|
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
|
||||||
|
fpPtr.ja3n.Store(&ja3n)
|
||||||
|
fpPtr.ja4.Store(&ja4)
|
||||||
|
}
|
||||||
|
return getConfigForClient(clientHello)
|
||||||
|
}
|
||||||
|
server.ConnContext = func(ctx context.Context, c net.Conn) context.Context {
|
||||||
|
ctx = context.WithValue(ctx, tlsFingerprintKey{}, &TLSFingerprint{})
|
||||||
|
|
||||||
|
if tc, ok := c.(*tls.Conn); ok {
|
||||||
|
tcpFP, err := AssignTCPFingerprint(tc.NetConn())
|
||||||
|
if err == nil {
|
||||||
|
ctx = context.WithValue(ctx, tcpFingerprintKey{}, tcpFP)
|
||||||
|
} else {
|
||||||
|
slog.Debug("ja4t error", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type tcpFingerprintKey struct{}
|
||||||
|
type tlsFingerprintKey struct{}
|
||||||
|
|
||||||
|
// TLSFingerprint represents TLS fingerprint data
|
||||||
|
type TLSFingerprint struct {
|
||||||
|
ja3n atomic.Pointer[TLSFingerprintJA3N]
|
||||||
|
ja4 atomic.Pointer[TLSFingerprintJA4]
|
||||||
|
}
|
||||||
|
|
||||||
|
// JA3N returns the JA3N fingerprint
|
||||||
|
func (f *TLSFingerprint) JA3N() *TLSFingerprintJA3N {
|
||||||
|
return f.ja3n.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JA4 returns the JA4 fingerprint
|
||||||
|
func (f *TLSFingerprint) JA4() *TLSFingerprintJA4 {
|
||||||
|
return f.ja4.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
const greaseMask = 0x0F0F
|
||||||
|
const greaseValue = 0x0a0a
|
||||||
|
|
||||||
|
// TLS extension numbers
|
||||||
|
const (
|
||||||
|
extensionServerName uint16 = 0
|
||||||
|
extensionStatusRequest uint16 = 5
|
||||||
|
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
|
||||||
|
extensionSupportedPoints uint16 = 11
|
||||||
|
extensionSignatureAlgorithms uint16 = 13
|
||||||
|
extensionALPN uint16 = 16
|
||||||
|
extensionSCT uint16 = 18
|
||||||
|
extensionExtendedMasterSecret uint16 = 23
|
||||||
|
extensionSessionTicket uint16 = 35
|
||||||
|
extensionPreSharedKey uint16 = 41
|
||||||
|
extensionEarlyData uint16 = 42
|
||||||
|
extensionSupportedVersions uint16 = 43
|
||||||
|
extensionCookie uint16 = 44
|
||||||
|
extensionPSKModes uint16 = 45
|
||||||
|
extensionCertificateAuthorities uint16 = 47
|
||||||
|
extensionSignatureAlgorithmsCert uint16 = 50
|
||||||
|
extensionKeyShare uint16 = 51
|
||||||
|
extensionQUICTransportParameters uint16 = 57
|
||||||
|
extensionRenegotiationInfo uint16 = 0xff01
|
||||||
|
extensionECHOuterExtensions uint16 = 0xfd00
|
||||||
|
extensionEncryptedClientHello uint16 = 0xfe0d
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildTLSFingerprint(hello *tls.ClientHelloInfo) (TLSFingerprintJA3N, TLSFingerprintJA4) {
|
||||||
|
return TLSFingerprintJA3N(buildJA3N(hello, true)), buildJA4(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTLSFingerprint extracts TLS fingerprint from HTTP request context
|
||||||
|
func GetTLSFingerprint(r *http.Request) *TLSFingerprint {
|
||||||
|
ptr := r.Context().Value(tlsFingerprintKey{})
|
||||||
|
if fpPtr, ok := ptr.(*TLSFingerprint); ok && ptr != nil && fpPtr != nil {
|
||||||
|
return fpPtr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
"github.com/google/cel-go/common/types/ref"
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
"github.com/google/cel-go/common/types/traits"
|
||||||
"github.com/google/cel-go/ext"
|
"github.com/google/cel-go/ext"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +27,33 @@ func BotEnvironment() (*cel.Env, error) {
|
|||||||
cel.Variable("load_1m", cel.DoubleType),
|
cel.Variable("load_1m", cel.DoubleType),
|
||||||
cel.Variable("load_5m", cel.DoubleType),
|
cel.Variable("load_5m", cel.DoubleType),
|
||||||
cel.Variable("load_15m", cel.DoubleType),
|
cel.Variable("load_15m", cel.DoubleType),
|
||||||
|
|
||||||
|
// Bot-specific functions:
|
||||||
|
cel.Function("missingHeader",
|
||||||
|
cel.Overload("missingHeader_map_string_string_string",
|
||||||
|
[]*cel.Type{cel.MapType(cel.StringType, cel.StringType), cel.StringType},
|
||||||
|
cel.BoolType,
|
||||||
|
cel.BinaryBinding(func(headers, key ref.Val) ref.Val {
|
||||||
|
// Convert headers to a trait that supports Find
|
||||||
|
headersMap, ok := headers.(traits.Indexer)
|
||||||
|
if !ok {
|
||||||
|
return types.ValOrErr(headers, "headers is not a map, but is %T", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyStr, ok := key.(types.String)
|
||||||
|
if !ok {
|
||||||
|
return types.ValOrErr(key, "key is not a string, but is %T", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
val := headersMap.Get(keyStr)
|
||||||
|
// Check if the key is missing by testing for an error
|
||||||
|
if types.IsError(val) {
|
||||||
|
return types.Bool(true) // header is missing
|
||||||
|
}
|
||||||
|
return types.Bool(false) // header is present
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
269
lib/policy/expressions/environment_test.go
Normal file
269
lib/policy/expressions/environment_test.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package expressions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBotEnvironment(t *testing.T) {
|
||||||
|
env, err := BotEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create bot environment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
headers map[string]string
|
||||||
|
expected types.Bool
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing-header",
|
||||||
|
expression: `missingHeader(headers, "Missing-Header")`,
|
||||||
|
headers: map[string]string{
|
||||||
|
"User-Agent": "test-agent",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
expected: types.Bool(true),
|
||||||
|
description: "should return true when header is missing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "existing-header",
|
||||||
|
expression: `missingHeader(headers, "User-Agent")`,
|
||||||
|
headers: map[string]string{
|
||||||
|
"User-Agent": "test-agent",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
expected: types.Bool(false),
|
||||||
|
description: "should return false when header exists",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case-sensitive",
|
||||||
|
expression: `missingHeader(headers, "user-agent")`,
|
||||||
|
headers: map[string]string{
|
||||||
|
"User-Agent": "test-agent",
|
||||||
|
},
|
||||||
|
expected: types.Bool(true),
|
||||||
|
description: "should be case-sensitive (user-agent != User-Agent)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty-headers",
|
||||||
|
expression: `missingHeader(headers, "Any-Header")`,
|
||||||
|
headers: map[string]string{},
|
||||||
|
expected: types.Bool(true),
|
||||||
|
description: "should return true for any header when map is empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "real-world-sec-ch-ua",
|
||||||
|
expression: `missingHeader(headers, "Sec-Ch-Ua")`,
|
||||||
|
headers: map[string]string{
|
||||||
|
"User-Agent": "curl/7.68.0",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Host": "example.com",
|
||||||
|
},
|
||||||
|
expected: types.Bool(true),
|
||||||
|
description: "should detect missing browser-specific headers from bots",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "browser-with-sec-ch-ua",
|
||||||
|
expression: `missingHeader(headers, "Sec-Ch-Ua")`,
|
||||||
|
headers: map[string]string{
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
"Sec-Ch-Ua": `"Chrome"; v="91", "Not A Brand"; v="99"`,
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
},
|
||||||
|
expected: types.Bool(false),
|
||||||
|
description: "should return false when browser sends Sec-Ch-Ua header",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
prog, err := Compile(env, tt.expression)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _, err := prog.Eval(map[string]interface{}{
|
||||||
|
"headers": tt.headers,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("function-compilation", func(t *testing.T) {
|
||||||
|
src := `missingHeader(headers, "Test-Header")`
|
||||||
|
_, err := Compile(env, src)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to compile missingHeader expression: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThresholdEnvironment(t *testing.T) {
|
||||||
|
env, err := ThresholdEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create threshold environment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
variables map[string]interface{}
|
||||||
|
expected types.Bool
|
||||||
|
description string
|
||||||
|
shouldCompile bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "weight-variable-available",
|
||||||
|
expression: `weight > 100`,
|
||||||
|
variables: map[string]interface{}{"weight": 150},
|
||||||
|
expected: types.Bool(true),
|
||||||
|
description: "should support weight variable in expressions",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "weight-variable-false-case",
|
||||||
|
expression: `weight > 100`,
|
||||||
|
variables: map[string]interface{}{"weight": 50},
|
||||||
|
expected: types.Bool(false),
|
||||||
|
description: "should correctly evaluate weight comparisons",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missingHeader-not-available",
|
||||||
|
expression: `missingHeader(headers, "Test")`,
|
||||||
|
variables: map[string]interface{}{},
|
||||||
|
expected: types.Bool(false), // not used
|
||||||
|
description: "should not have missingHeader function available",
|
||||||
|
shouldCompile: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
prog, err := Compile(env, tt.expression)
|
||||||
|
|
||||||
|
if !tt.shouldCompile {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("%s: expected compilation to fail but it succeeded", tt.description)
|
||||||
|
}
|
||||||
|
return // Test passed - compilation failed as expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _, err := prog.Eval(tt.variables)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewEnvironment(t *testing.T) {
|
||||||
|
env, err := New()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create new environment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression string
|
||||||
|
variables map[string]interface{}
|
||||||
|
expectBool *bool // nil if we just want to test compilation or non-bool result
|
||||||
|
description string
|
||||||
|
shouldCompile bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "randInt-function-compilation",
|
||||||
|
expression: `randInt(10)`,
|
||||||
|
variables: map[string]interface{}{},
|
||||||
|
expectBool: nil, // Don't check result, just compilation
|
||||||
|
description: "should compile randInt function",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "randInt-range-validation",
|
||||||
|
expression: `randInt(10) >= 0 && randInt(10) < 10`,
|
||||||
|
variables: map[string]interface{}{},
|
||||||
|
expectBool: boolPtr(true),
|
||||||
|
description: "should return values in correct range",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "strings-extension-size",
|
||||||
|
expression: `"hello".size() == 5`,
|
||||||
|
variables: map[string]interface{}{},
|
||||||
|
expectBool: boolPtr(true),
|
||||||
|
description: "should support string extension functions",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "strings-extension-contains",
|
||||||
|
expression: `"hello world".contains("world")`,
|
||||||
|
variables: map[string]interface{}{},
|
||||||
|
expectBool: boolPtr(true),
|
||||||
|
description: "should support string contains function",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "strings-extension-startsWith",
|
||||||
|
expression: `"hello world".startsWith("hello")`,
|
||||||
|
variables: map[string]interface{}{},
|
||||||
|
expectBool: boolPtr(true),
|
||||||
|
description: "should support string startsWith function",
|
||||||
|
shouldCompile: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
prog, err := Compile(env, tt.expression)
|
||||||
|
|
||||||
|
if !tt.shouldCompile {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("%s: expected compilation to fail but it succeeded", tt.description)
|
||||||
|
}
|
||||||
|
return // Test passed - compilation failed as expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we only want to test compilation, skip evaluation
|
||||||
|
if tt.expectBool == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _, err := prog.Eval(tt.variables)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != types.Bool(*tt.expectBool) {
|
||||||
|
t.Errorf("%s: expected %v, got %v", tt.description, *tt.expectBool, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create bool pointers
|
||||||
|
func boolPtr(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
407
package-lock.json
generated
407
package-lock.json
generated
@@ -9,9 +9,9 @@
|
|||||||
"version": "1.21.0",
|
"version": "1.21.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.1.0",
|
||||||
"cssnano-preset-advanced": "^7.0.7",
|
"cssnano-preset-advanced": "^7.0.8",
|
||||||
"esbuild": "^0.25.6",
|
"esbuild": "^0.25.8",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||||
"integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
|
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
|
||||||
"integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
|
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -54,9 +54,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
|
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -71,9 +71,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
|
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -88,9 +88,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
|
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -105,9 +105,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
|
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -122,9 +122,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
|
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -139,9 +139,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
|
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -156,9 +156,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
|
||||||
"integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
|
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -173,9 +173,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
|
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -190,9 +190,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
|
||||||
"integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
|
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -207,9 +207,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
|
||||||
"integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
|
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -224,9 +224,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
|
||||||
"integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
|
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -241,9 +241,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
|
||||||
"integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
|
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -258,9 +258,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
|
||||||
"integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
|
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -275,9 +275,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
|
||||||
"integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
|
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -292,9 +292,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
|
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -309,9 +309,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
|
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -326,9 +326,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
|
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -343,9 +343,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
|
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -360,9 +360,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
|
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -377,9 +377,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
|
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -394,9 +394,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
|
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -411,9 +411,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
|
||||||
"integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
|
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -428,9 +428,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
|
||||||
"integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
|
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -445,9 +445,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
|
||||||
"integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
|
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -461,16 +461,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trysound/sax": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@@ -601,9 +591,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.25.0",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
|
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -621,8 +611,8 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001718",
|
"caniuse-lite": "^1.0.30001726",
|
||||||
"electron-to-chromium": "^1.5.160",
|
"electron-to-chromium": "^1.5.173",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.3"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
@@ -647,9 +637,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001724",
|
"version": "1.0.30001727",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
|
||||||
"integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==",
|
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -735,13 +725,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "7.2.0",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
@@ -765,9 +755,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "5.1.0",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -782,13 +772,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-tree": {
|
"node_modules/css-tree": {
|
||||||
"version": "2.3.1",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mdn-data": "2.0.30",
|
"mdn-data": "2.12.2",
|
||||||
"source-map-js": "^1.0.1"
|
"source-map-js": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -796,9 +786,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/css-what": {
|
"node_modules/css-what": {
|
||||||
"version": "6.1.0",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -822,13 +812,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cssnano": {
|
"node_modules/cssnano": {
|
||||||
"version": "7.0.7",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.0.tgz",
|
||||||
"integrity": "sha512-evKu7yiDIF7oS+EIpwFlMF730ijRyLFaM2o5cTxRGJR9OKHKkc+qP443ZEVR9kZG0syaAJJCPJyfv5pbrxlSng==",
|
"integrity": "sha512-Pu3rlKkd0ZtlCUzBrKL1Z4YmhKppjC1H9jo7u1o4qaKqyhvixFgu5qLyNIAOjSTg9DjVPtUqdROq2EfpVMEe+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano-preset-default": "^7.0.7",
|
"cssnano-preset-default": "^7.0.8",
|
||||||
"lilconfig": "^3.1.3"
|
"lilconfig": "^3.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -843,15 +833,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cssnano-preset-advanced": {
|
"node_modules/cssnano-preset-advanced": {
|
||||||
"version": "7.0.7",
|
"version": "7.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-7.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-7.0.8.tgz",
|
||||||
"integrity": "sha512-uBLTct5OBy3r+WL8zB+RAUsok2E5eWdi3xvrotI0aS/E3KvEKKyaXIlAXLHUI0MSopHXJEuUX4jMTr9VkUtWkA==",
|
"integrity": "sha512-KYw7gH8xmIzTwHefuM/m3lkMz4jn5EbjxZO2RHVsOGvrXCxSjbc0/f/gELWW9ZIgbQdJMCkijEo76gBYGY4S3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"cssnano-preset-default": "^7.0.7",
|
"cssnano-preset-default": "^7.0.8",
|
||||||
"postcss-discard-unused": "^7.0.4",
|
"postcss-discard-unused": "^7.0.4",
|
||||||
"postcss-merge-idents": "^7.0.1",
|
"postcss-merge-idents": "^7.0.1",
|
||||||
"postcss-reduce-idents": "^7.0.1",
|
"postcss-reduce-idents": "^7.0.1",
|
||||||
@@ -865,27 +855,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cssnano-preset-default": {
|
"node_modules/cssnano-preset-default": {
|
||||||
"version": "7.0.7",
|
"version": "7.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.8.tgz",
|
||||||
"integrity": "sha512-jW6CG/7PNB6MufOrlovs1TvBTEVmhY45yz+bd0h6nw3h6d+1e+/TX+0fflZ+LzvZombbT5f+KC063w9VoHeHow==",
|
"integrity": "sha512-d+3R2qwrUV3g4LEMOjnndognKirBZISylDZAF/TPeCWVjEwlXS2e4eN4ICkoobRe7pD3H6lltinKVyS1AJhdjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"css-declaration-sorter": "^7.2.0",
|
"css-declaration-sorter": "^7.2.0",
|
||||||
"cssnano-utils": "^5.0.1",
|
"cssnano-utils": "^5.0.1",
|
||||||
"postcss-calc": "^10.1.1",
|
"postcss-calc": "^10.1.1",
|
||||||
"postcss-colormin": "^7.0.3",
|
"postcss-colormin": "^7.0.4",
|
||||||
"postcss-convert-values": "^7.0.5",
|
"postcss-convert-values": "^7.0.6",
|
||||||
"postcss-discard-comments": "^7.0.4",
|
"postcss-discard-comments": "^7.0.4",
|
||||||
"postcss-discard-duplicates": "^7.0.2",
|
"postcss-discard-duplicates": "^7.0.2",
|
||||||
"postcss-discard-empty": "^7.0.1",
|
"postcss-discard-empty": "^7.0.1",
|
||||||
"postcss-discard-overridden": "^7.0.1",
|
"postcss-discard-overridden": "^7.0.1",
|
||||||
"postcss-merge-longhand": "^7.0.5",
|
"postcss-merge-longhand": "^7.0.5",
|
||||||
"postcss-merge-rules": "^7.0.5",
|
"postcss-merge-rules": "^7.0.6",
|
||||||
"postcss-minify-font-values": "^7.0.1",
|
"postcss-minify-font-values": "^7.0.1",
|
||||||
"postcss-minify-gradients": "^7.0.1",
|
"postcss-minify-gradients": "^7.0.1",
|
||||||
"postcss-minify-params": "^7.0.3",
|
"postcss-minify-params": "^7.0.4",
|
||||||
"postcss-minify-selectors": "^7.0.5",
|
"postcss-minify-selectors": "^7.0.5",
|
||||||
"postcss-normalize-charset": "^7.0.1",
|
"postcss-normalize-charset": "^7.0.1",
|
||||||
"postcss-normalize-display-values": "^7.0.1",
|
"postcss-normalize-display-values": "^7.0.1",
|
||||||
@@ -893,13 +883,13 @@
|
|||||||
"postcss-normalize-repeat-style": "^7.0.1",
|
"postcss-normalize-repeat-style": "^7.0.1",
|
||||||
"postcss-normalize-string": "^7.0.1",
|
"postcss-normalize-string": "^7.0.1",
|
||||||
"postcss-normalize-timing-functions": "^7.0.1",
|
"postcss-normalize-timing-functions": "^7.0.1",
|
||||||
"postcss-normalize-unicode": "^7.0.3",
|
"postcss-normalize-unicode": "^7.0.4",
|
||||||
"postcss-normalize-url": "^7.0.1",
|
"postcss-normalize-url": "^7.0.1",
|
||||||
"postcss-normalize-whitespace": "^7.0.1",
|
"postcss-normalize-whitespace": "^7.0.1",
|
||||||
"postcss-ordered-values": "^7.0.2",
|
"postcss-ordered-values": "^7.0.2",
|
||||||
"postcss-reduce-initial": "^7.0.3",
|
"postcss-reduce-initial": "^7.0.4",
|
||||||
"postcss-reduce-transforms": "^7.0.1",
|
"postcss-reduce-transforms": "^7.0.1",
|
||||||
"postcss-svgo": "^7.0.2",
|
"postcss-svgo": "^7.1.0",
|
||||||
"postcss-unique-selectors": "^7.0.4"
|
"postcss-unique-selectors": "^7.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1035,9 +1025,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.171",
|
"version": "1.5.187",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
|
||||||
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==",
|
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -1062,9 +1052,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
|
||||||
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -1075,32 +1065,32 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.25.6",
|
"@esbuild/aix-ppc64": "0.25.8",
|
||||||
"@esbuild/android-arm": "0.25.6",
|
"@esbuild/android-arm": "0.25.8",
|
||||||
"@esbuild/android-arm64": "0.25.6",
|
"@esbuild/android-arm64": "0.25.8",
|
||||||
"@esbuild/android-x64": "0.25.6",
|
"@esbuild/android-x64": "0.25.8",
|
||||||
"@esbuild/darwin-arm64": "0.25.6",
|
"@esbuild/darwin-arm64": "0.25.8",
|
||||||
"@esbuild/darwin-x64": "0.25.6",
|
"@esbuild/darwin-x64": "0.25.8",
|
||||||
"@esbuild/freebsd-arm64": "0.25.6",
|
"@esbuild/freebsd-arm64": "0.25.8",
|
||||||
"@esbuild/freebsd-x64": "0.25.6",
|
"@esbuild/freebsd-x64": "0.25.8",
|
||||||
"@esbuild/linux-arm": "0.25.6",
|
"@esbuild/linux-arm": "0.25.8",
|
||||||
"@esbuild/linux-arm64": "0.25.6",
|
"@esbuild/linux-arm64": "0.25.8",
|
||||||
"@esbuild/linux-ia32": "0.25.6",
|
"@esbuild/linux-ia32": "0.25.8",
|
||||||
"@esbuild/linux-loong64": "0.25.6",
|
"@esbuild/linux-loong64": "0.25.8",
|
||||||
"@esbuild/linux-mips64el": "0.25.6",
|
"@esbuild/linux-mips64el": "0.25.8",
|
||||||
"@esbuild/linux-ppc64": "0.25.6",
|
"@esbuild/linux-ppc64": "0.25.8",
|
||||||
"@esbuild/linux-riscv64": "0.25.6",
|
"@esbuild/linux-riscv64": "0.25.8",
|
||||||
"@esbuild/linux-s390x": "0.25.6",
|
"@esbuild/linux-s390x": "0.25.8",
|
||||||
"@esbuild/linux-x64": "0.25.6",
|
"@esbuild/linux-x64": "0.25.8",
|
||||||
"@esbuild/netbsd-arm64": "0.25.6",
|
"@esbuild/netbsd-arm64": "0.25.8",
|
||||||
"@esbuild/netbsd-x64": "0.25.6",
|
"@esbuild/netbsd-x64": "0.25.8",
|
||||||
"@esbuild/openbsd-arm64": "0.25.6",
|
"@esbuild/openbsd-arm64": "0.25.8",
|
||||||
"@esbuild/openbsd-x64": "0.25.6",
|
"@esbuild/openbsd-x64": "0.25.8",
|
||||||
"@esbuild/openharmony-arm64": "0.25.6",
|
"@esbuild/openharmony-arm64": "0.25.8",
|
||||||
"@esbuild/sunos-x64": "0.25.6",
|
"@esbuild/sunos-x64": "0.25.8",
|
||||||
"@esbuild/win32-arm64": "0.25.6",
|
"@esbuild/win32-arm64": "0.25.8",
|
||||||
"@esbuild/win32-ia32": "0.25.6",
|
"@esbuild/win32-ia32": "0.25.8",
|
||||||
"@esbuild/win32-x64": "0.25.6"
|
"@esbuild/win32-x64": "0.25.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
@@ -1380,9 +1370,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdn-data": {
|
"node_modules/mdn-data": {
|
||||||
"version": "2.0.30",
|
"version": "2.12.2",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
@@ -1618,13 +1608,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-colormin": {
|
"node_modules/postcss-colormin": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz",
|
||||||
"integrity": "sha512-xZxQcSyIVZbSsl1vjoqZAcMYYdnJsIyG8OvqShuuqf12S88qQboxxEy0ohNCOLwVPXTU+hFHvJPACRL2B5ohTA==",
|
"integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"caniuse-api": "^3.0.0",
|
"caniuse-api": "^3.0.0",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
@@ -1637,13 +1627,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-convert-values": {
|
"node_modules/postcss-convert-values": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.6.tgz",
|
||||||
"integrity": "sha512-0VFhH8nElpIs3uXKnVtotDJJNX0OGYSZmdt4XfSfvOMrFw1jKfpwpZxfC4iN73CTM/MWakDEmsHQXkISYj4BXw==",
|
"integrity": "sha512-MD/eb39Mr60hvgrqpXsgbiqluawYg/8K4nKsqRsuDX9f+xN1j6awZCUv/5tLH8ak3vYp/EMXwdcnXvfZYiejCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1837,13 +1827,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-merge-rules": {
|
"node_modules/postcss-merge-rules": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz",
|
||||||
"integrity": "sha512-ZonhuSwEaWA3+xYbOdJoEReKIBs5eDiBVLAGpYZpNFPzXZcEE5VKR7/qBEQvTZpiwjqhhqEQ+ax5O3VShBj9Wg==",
|
"integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"caniuse-api": "^3.0.0",
|
"caniuse-api": "^3.0.0",
|
||||||
"cssnano-utils": "^5.0.1",
|
"cssnano-utils": "^5.0.1",
|
||||||
"postcss-selector-parser": "^7.1.0"
|
"postcss-selector-parser": "^7.1.0"
|
||||||
@@ -1890,13 +1880,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-minify-params": {
|
"node_modules/postcss-minify-params": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz",
|
||||||
"integrity": "sha512-vUKV2+f5mtjewYieanLX0xemxIp1t0W0H/D11u+kQV/MWdygOO7xPMkbK+r9P6Lhms8MgzKARF/g5OPXhb8tgg==",
|
"integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"cssnano-utils": "^5.0.1",
|
"cssnano-utils": "^5.0.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
@@ -2018,13 +2008,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-normalize-unicode": {
|
"node_modules/postcss-normalize-unicode": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz",
|
||||||
"integrity": "sha512-EcoA29LvG3F+EpOh03iqu+tJY3uYYKzArqKJHxDhUYLa2u58aqGq16K6/AOsXD9yqLN8O6y9mmePKN5cx6krOw==",
|
"integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"postcss-value-parser": "^4.2.0"
|
"postcss-value-parser": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2100,13 +2090,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-reduce-initial": {
|
"node_modules/postcss-reduce-initial": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz",
|
||||||
"integrity": "sha512-RFvkZaqiWtGMlVjlUHpaxGqEL27lgt+Q2Ixjf83CRAzqdo+TsDyGPtJUbPx2MuYIJ+sCQc2TrOvRnhcXQfgIVA==",
|
"integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"browserslist": "^4.24.5",
|
"browserslist": "^4.25.1",
|
||||||
"caniuse-api": "^3.0.0"
|
"caniuse-api": "^3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2174,14 +2164,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-svgo": {
|
"node_modules/postcss-svgo": {
|
||||||
"version": "7.0.2",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz",
|
||||||
"integrity": "sha512-5Dzy66JlnRM6pkdOTF8+cGsB1fnERTE8Nc+Eed++fOWo1hdsBptCsbG8UuJkgtZt75bRtMJIrPeZmtfANixdFA==",
|
"integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"svgo": "^3.3.2"
|
"svgo": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.12.0 || ^20.9.0 || >= 18"
|
"node": "^18.12.0 || ^20.9.0 || >= 18"
|
||||||
@@ -2336,6 +2326,13 @@
|
|||||||
"url": "0.10.x"
|
"url": "0.10.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -2428,25 +2425,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svgo": {
|
"node_modules/svgo": {
|
||||||
"version": "3.3.2",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
|
||||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
"integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@trysound/sax": "0.2.0",
|
"commander": "^11.1.0",
|
||||||
"commander": "^7.2.0",
|
|
||||||
"css-select": "^5.1.0",
|
"css-select": "^5.1.0",
|
||||||
"css-tree": "^2.3.1",
|
"css-tree": "^3.0.1",
|
||||||
"css-what": "^6.1.0",
|
"css-what": "^6.1.0",
|
||||||
"csso": "^5.0.5",
|
"csso": "^5.0.5",
|
||||||
"picocolors": "^1.0.0"
|
"picocolors": "^1.1.1",
|
||||||
|
"sax": "^1.4.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"svgo": "bin/svgo"
|
"svgo": "bin/svgo.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^7.1.0",
|
||||||
"cssnano-preset-advanced": "^7.0.7",
|
"cssnano-preset-advanced": "^7.0.8",
|
||||||
"esbuild": "^0.25.6",
|
"esbuild": "^0.25.8",
|
||||||
"playwright": "^1.52.0",
|
"playwright": "^1.52.0",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user