mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-07 09:18:18 +00:00
Compare commits
1 Commits
Xe/osiris
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3850f2988 |
@@ -21,9 +21,7 @@
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml",
|
||||
"hashicorp.hcl",
|
||||
"fredwangwang.vscode-hcl-format"
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
.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:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: [ "main" ]
|
||||
|
||||
env:
|
||||
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
|
||||
@@ -11,32 +11,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
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:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
33
.github/workflows/docker.yml
vendored
33
.github/workflows/docker.yml
vendored
@@ -17,38 +17,7 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
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:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
4
.github/workflows/docs-deploy.yml
vendored
4
.github/workflows/docs-deploy.yml
vendored
@@ -53,14 +53,14 @@ jobs:
|
||||
push: true
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
args: apply -k docs/manifest
|
||||
|
||||
- name: Apply k8s manifests to limsa lominsa
|
||||
uses: actions-hub/kubectl@b5b19eeb6a0ffde16637e398f8b96ef01eb8fdb7 # v1.33.3
|
||||
uses: actions-hub/kubectl@d50394b7d704525f93faefce1e65a6329ff67271 # v1.33.2
|
||||
env:
|
||||
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/zizmor.yml
vendored
2
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v6.4.1
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -5,8 +5,6 @@
|
||||
"golang.go",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"a-h.templ",
|
||||
"redhat.vscode-yaml",
|
||||
"hashicorp.hcl",
|
||||
"fredwangwang.vscode-hcl-format"
|
||||
"redhat.vscode-yaml"
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
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 +0,0 @@
|
||||
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
|
||||
@@ -1 +0,0 @@
|
||||
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
|
||||
@@ -1,11 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnzCCAVGgAwIBAgIUAw8funCpiB3ZAAPoWdSCWnzbsFIwBQYDK2VwMEUxCzAJ
|
||||
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MTkwMjM1WhcNMjUwODE3MTkwMjM1
|
||||
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAcXDHXV3vgpvjtTaz
|
||||
s0Oj/73rMr06bhyGGhleYS1MNoWjUzBRMB0GA1UdDgQWBBQwmfKPthucFHB6Wfgz
|
||||
2Nj5nkMQOjAfBgNVHSMEGDAWgBQwmfKPthucFHB6Wfgz2Nj5nkMQOjAPBgNVHRMB
|
||||
Af8EBTADAQH/MAUGAytlcANBALBYbULlGwB7Ro0UTgUoQDNxEvayn3qzVFHIt7lC
|
||||
/2/NzNBkk4yPT+a4mbRuydxLkv+JIvmQbarZxpksYnWlCAM=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,3 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIOHKoX22Mha6SnnpLm34fSSfTUDbRiDCi6N1nOgTOlds
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,40 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
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())
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBnzCCAVGgAwIBAgIUOLTjSYOjFk00IemtFTC4oEZs988wBQYDK2VwMEUxCzAJ
|
||||
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjUwNzE4MjEyNDIzWhcNMjUwODE3MjEyNDIz
|
||||
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
||||
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAPHphABS15+4VV6R1
|
||||
vYzBQYIycQmOmlbA8QcfwzuB2VajUzBRMB0GA1UdDgQWBBT2s+MQ4AR6cbK4V0+d
|
||||
XZnok1orhDAfBgNVHSMEGDAWgBT2s+MQ4AR6cbK4V0+dXZnok1orhDAPBgNVHRMB
|
||||
Af8EBTADAQH/MAUGAytlcANBAOdoJbRMnHmkEETzVtXP+jkAI9yQNRXujnglApGP
|
||||
8I5pvIYVgYCgoQrnb4haVWFldHM1T9H698n19e/egfFb+w4=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,3 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIBop42tiZ0yzhaKo9NAc0PlAyBsE8NAE0i9Z7s2lgZuR
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,43 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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"
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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"
|
||||
6
go.mod
6
go.mod
@@ -47,10 +47,8 @@ require (
|
||||
github.com/Songmu/gitconfig v0.2.0 // indirect
|
||||
github.com/TecharoHQ/yeet v0.6.0 // 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/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/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
@@ -95,7 +93,6 @@ require (
|
||||
github.com/goccy/go-yaml v1.12.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // 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-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
@@ -104,7 +101,6 @@ require (
|
||||
github.com/goreleaser/fileglob v1.3.0 // indirect
|
||||
github.com/goreleaser/nfpm/v2 v2.42.1 // 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
@@ -115,7 +111,6 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
@@ -155,7 +150,6 @@ require (
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // 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
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -40,8 +40,6 @@ github.com/TecharoHQ/yeet v0.6.0 h1:RCBAjr7wIlllsgy0tpvWpLX7jsZgu2tiuBY3RrprcR0=
|
||||
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/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
|
||||
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=
|
||||
@@ -50,8 +48,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
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/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/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -229,8 +225,6 @@ 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/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
@@ -284,8 +278,6 @@ 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/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-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/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
@@ -411,8 +403,6 @@ 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/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
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/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
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[:])
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
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])
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//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
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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
|
||||
}
|
||||
407
package-lock.json
generated
407
package-lock.json
generated
@@ -9,9 +9,9 @@
|
||||
"version": "1.21.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.1.0",
|
||||
"cssnano-preset-advanced": "^7.0.8",
|
||||
"esbuild": "^0.25.8",
|
||||
"cssnano": "^7.0.7",
|
||||
"cssnano-preset-advanced": "^7.0.7",
|
||||
"esbuild": "^0.25.6",
|
||||
"playwright": "^1.52.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.1",
|
||||
@@ -20,9 +20,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||
"integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
|
||||
"integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -37,9 +37,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
|
||||
"integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz",
|
||||
"integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -54,9 +54,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -71,9 +71,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -88,9 +88,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -105,9 +105,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -122,9 +122,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -139,9 +139,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -156,9 +156,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
|
||||
"integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz",
|
||||
"integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -173,9 +173,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -190,9 +190,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
|
||||
"integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz",
|
||||
"integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -207,9 +207,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
|
||||
"integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz",
|
||||
"integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -224,9 +224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
|
||||
"integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz",
|
||||
"integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -241,9 +241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
|
||||
"integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz",
|
||||
"integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -258,9 +258,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
|
||||
"integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz",
|
||||
"integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -275,9 +275,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
|
||||
"integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz",
|
||||
"integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -292,9 +292,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -309,9 +309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -326,9 +326,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -343,9 +343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -360,9 +360,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -377,9 +377,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -394,9 +394,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -411,9 +411,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
|
||||
"integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz",
|
||||
"integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -428,9 +428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
|
||||
"integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz",
|
||||
"integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -445,9 +445,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
|
||||
"integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz",
|
||||
"integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -461,6 +461,16 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@@ -591,9 +601,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.25.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
|
||||
"version": "4.25.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
||||
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -611,8 +621,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001726",
|
||||
"electron-to-chromium": "^1.5.173",
|
||||
"caniuse-lite": "^1.0.30001718",
|
||||
"electron-to-chromium": "^1.5.160",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
@@ -637,9 +647,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001727",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
|
||||
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
|
||||
"version": "1.0.30001724",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz",
|
||||
"integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -725,13 +735,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
|
||||
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
@@ -755,9 +765,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
@@ -772,13 +782,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.12.2",
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -786,9 +796,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
@@ -812,13 +822,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.0.tgz",
|
||||
"integrity": "sha512-Pu3rlKkd0ZtlCUzBrKL1Z4YmhKppjC1H9jo7u1o4qaKqyhvixFgu5qLyNIAOjSTg9DjVPtUqdROq2EfpVMEe+w==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.7.tgz",
|
||||
"integrity": "sha512-evKu7yiDIF7oS+EIpwFlMF730ijRyLFaM2o5cTxRGJR9OKHKkc+qP443ZEVR9kZG0syaAJJCPJyfv5pbrxlSng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.8",
|
||||
"cssnano-preset-default": "^7.0.7",
|
||||
"lilconfig": "^3.1.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -833,15 +843,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano-preset-advanced": {
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-7.0.8.tgz",
|
||||
"integrity": "sha512-KYw7gH8xmIzTwHefuM/m3lkMz4jn5EbjxZO2RHVsOGvrXCxSjbc0/f/gELWW9ZIgbQdJMCkijEo76gBYGY4S3Q==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-7.0.7.tgz",
|
||||
"integrity": "sha512-uBLTct5OBy3r+WL8zB+RAUsok2E5eWdi3xvrotI0aS/E3KvEKKyaXIlAXLHUI0MSopHXJEuUX4jMTr9VkUtWkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.21",
|
||||
"browserslist": "^4.25.1",
|
||||
"cssnano-preset-default": "^7.0.8",
|
||||
"browserslist": "^4.24.5",
|
||||
"cssnano-preset-default": "^7.0.7",
|
||||
"postcss-discard-unused": "^7.0.4",
|
||||
"postcss-merge-idents": "^7.0.1",
|
||||
"postcss-reduce-idents": "^7.0.1",
|
||||
@@ -855,27 +865,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cssnano-preset-default": {
|
||||
"version": "7.0.8",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.8.tgz",
|
||||
"integrity": "sha512-d+3R2qwrUV3g4LEMOjnndognKirBZISylDZAF/TPeCWVjEwlXS2e4eN4ICkoobRe7pD3H6lltinKVyS1AJhdjQ==",
|
||||
"version": "7.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.7.tgz",
|
||||
"integrity": "sha512-jW6CG/7PNB6MufOrlovs1TvBTEVmhY45yz+bd0h6nw3h6d+1e+/TX+0fflZ+LzvZombbT5f+KC063w9VoHeHow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"css-declaration-sorter": "^7.2.0",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"postcss-calc": "^10.1.1",
|
||||
"postcss-colormin": "^7.0.4",
|
||||
"postcss-convert-values": "^7.0.6",
|
||||
"postcss-colormin": "^7.0.3",
|
||||
"postcss-convert-values": "^7.0.5",
|
||||
"postcss-discard-comments": "^7.0.4",
|
||||
"postcss-discard-duplicates": "^7.0.2",
|
||||
"postcss-discard-empty": "^7.0.1",
|
||||
"postcss-discard-overridden": "^7.0.1",
|
||||
"postcss-merge-longhand": "^7.0.5",
|
||||
"postcss-merge-rules": "^7.0.6",
|
||||
"postcss-merge-rules": "^7.0.5",
|
||||
"postcss-minify-font-values": "^7.0.1",
|
||||
"postcss-minify-gradients": "^7.0.1",
|
||||
"postcss-minify-params": "^7.0.4",
|
||||
"postcss-minify-params": "^7.0.3",
|
||||
"postcss-minify-selectors": "^7.0.5",
|
||||
"postcss-normalize-charset": "^7.0.1",
|
||||
"postcss-normalize-display-values": "^7.0.1",
|
||||
@@ -883,13 +893,13 @@
|
||||
"postcss-normalize-repeat-style": "^7.0.1",
|
||||
"postcss-normalize-string": "^7.0.1",
|
||||
"postcss-normalize-timing-functions": "^7.0.1",
|
||||
"postcss-normalize-unicode": "^7.0.4",
|
||||
"postcss-normalize-unicode": "^7.0.3",
|
||||
"postcss-normalize-url": "^7.0.1",
|
||||
"postcss-normalize-whitespace": "^7.0.1",
|
||||
"postcss-ordered-values": "^7.0.2",
|
||||
"postcss-reduce-initial": "^7.0.4",
|
||||
"postcss-reduce-initial": "^7.0.3",
|
||||
"postcss-reduce-transforms": "^7.0.1",
|
||||
"postcss-svgo": "^7.1.0",
|
||||
"postcss-svgo": "^7.0.2",
|
||||
"postcss-unique-selectors": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1025,9 +1035,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.187",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
|
||||
"integrity": "sha512-cl5Jc9I0KGUoOoSbxvTywTa40uspGJt/BDBoDLoxJRSBpWh4FFXBsjNRHfQrONsV/OoEjDfHUmZQa2d6Ze4YgA==",
|
||||
"version": "1.5.171",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz",
|
||||
"integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1052,9 +1062,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
|
||||
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
|
||||
"version": "0.25.6",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||
"integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -1065,32 +1075,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.8",
|
||||
"@esbuild/android-arm": "0.25.8",
|
||||
"@esbuild/android-arm64": "0.25.8",
|
||||
"@esbuild/android-x64": "0.25.8",
|
||||
"@esbuild/darwin-arm64": "0.25.8",
|
||||
"@esbuild/darwin-x64": "0.25.8",
|
||||
"@esbuild/freebsd-arm64": "0.25.8",
|
||||
"@esbuild/freebsd-x64": "0.25.8",
|
||||
"@esbuild/linux-arm": "0.25.8",
|
||||
"@esbuild/linux-arm64": "0.25.8",
|
||||
"@esbuild/linux-ia32": "0.25.8",
|
||||
"@esbuild/linux-loong64": "0.25.8",
|
||||
"@esbuild/linux-mips64el": "0.25.8",
|
||||
"@esbuild/linux-ppc64": "0.25.8",
|
||||
"@esbuild/linux-riscv64": "0.25.8",
|
||||
"@esbuild/linux-s390x": "0.25.8",
|
||||
"@esbuild/linux-x64": "0.25.8",
|
||||
"@esbuild/netbsd-arm64": "0.25.8",
|
||||
"@esbuild/netbsd-x64": "0.25.8",
|
||||
"@esbuild/openbsd-arm64": "0.25.8",
|
||||
"@esbuild/openbsd-x64": "0.25.8",
|
||||
"@esbuild/openharmony-arm64": "0.25.8",
|
||||
"@esbuild/sunos-x64": "0.25.8",
|
||||
"@esbuild/win32-arm64": "0.25.8",
|
||||
"@esbuild/win32-ia32": "0.25.8",
|
||||
"@esbuild/win32-x64": "0.25.8"
|
||||
"@esbuild/aix-ppc64": "0.25.6",
|
||||
"@esbuild/android-arm": "0.25.6",
|
||||
"@esbuild/android-arm64": "0.25.6",
|
||||
"@esbuild/android-x64": "0.25.6",
|
||||
"@esbuild/darwin-arm64": "0.25.6",
|
||||
"@esbuild/darwin-x64": "0.25.6",
|
||||
"@esbuild/freebsd-arm64": "0.25.6",
|
||||
"@esbuild/freebsd-x64": "0.25.6",
|
||||
"@esbuild/linux-arm": "0.25.6",
|
||||
"@esbuild/linux-arm64": "0.25.6",
|
||||
"@esbuild/linux-ia32": "0.25.6",
|
||||
"@esbuild/linux-loong64": "0.25.6",
|
||||
"@esbuild/linux-mips64el": "0.25.6",
|
||||
"@esbuild/linux-ppc64": "0.25.6",
|
||||
"@esbuild/linux-riscv64": "0.25.6",
|
||||
"@esbuild/linux-s390x": "0.25.6",
|
||||
"@esbuild/linux-x64": "0.25.6",
|
||||
"@esbuild/netbsd-arm64": "0.25.6",
|
||||
"@esbuild/netbsd-x64": "0.25.6",
|
||||
"@esbuild/openbsd-arm64": "0.25.6",
|
||||
"@esbuild/openbsd-x64": "0.25.6",
|
||||
"@esbuild/openharmony-arm64": "0.25.6",
|
||||
"@esbuild/sunos-x64": "0.25.6",
|
||||
"@esbuild/win32-arm64": "0.25.6",
|
||||
"@esbuild/win32-ia32": "0.25.6",
|
||||
"@esbuild/win32-x64": "0.25.6"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -1370,9 +1380,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
@@ -1608,13 +1618,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-colormin": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz",
|
||||
"integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.3.tgz",
|
||||
"integrity": "sha512-xZxQcSyIVZbSsl1vjoqZAcMYYdnJsIyG8OvqShuuqf12S88qQboxxEy0ohNCOLwVPXTU+hFHvJPACRL2B5ohTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"caniuse-api": "^3.0.0",
|
||||
"colord": "^2.9.3",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
@@ -1627,13 +1637,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-convert-values": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.6.tgz",
|
||||
"integrity": "sha512-MD/eb39Mr60hvgrqpXsgbiqluawYg/8K4nKsqRsuDX9f+xN1j6awZCUv/5tLH8ak3vYp/EMXwdcnXvfZYiejCQ==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.5.tgz",
|
||||
"integrity": "sha512-0VFhH8nElpIs3uXKnVtotDJJNX0OGYSZmdt4XfSfvOMrFw1jKfpwpZxfC4iN73CTM/MWakDEmsHQXkISYj4BXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1827,13 +1837,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-merge-rules": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz",
|
||||
"integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.5.tgz",
|
||||
"integrity": "sha512-ZonhuSwEaWA3+xYbOdJoEReKIBs5eDiBVLAGpYZpNFPzXZcEE5VKR7/qBEQvTZpiwjqhhqEQ+ax5O3VShBj9Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"caniuse-api": "^3.0.0",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"postcss-selector-parser": "^7.1.0"
|
||||
@@ -1880,13 +1890,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-minify-params": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz",
|
||||
"integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.3.tgz",
|
||||
"integrity": "sha512-vUKV2+f5mtjewYieanLX0xemxIp1t0W0H/D11u+kQV/MWdygOO7xPMkbK+r9P6Lhms8MgzKARF/g5OPXhb8tgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"cssnano-utils": "^5.0.1",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
@@ -2008,13 +2018,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-normalize-unicode": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz",
|
||||
"integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.3.tgz",
|
||||
"integrity": "sha512-EcoA29LvG3F+EpOh03iqu+tJY3uYYKzArqKJHxDhUYLa2u58aqGq16K6/AOsXD9yqLN8O6y9mmePKN5cx6krOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2090,13 +2100,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-reduce-initial": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz",
|
||||
"integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.3.tgz",
|
||||
"integrity": "sha512-RFvkZaqiWtGMlVjlUHpaxGqEL27lgt+Q2Ixjf83CRAzqdo+TsDyGPtJUbPx2MuYIJ+sCQc2TrOvRnhcXQfgIVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.25.1",
|
||||
"browserslist": "^4.24.5",
|
||||
"caniuse-api": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2164,14 +2174,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-svgo": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz",
|
||||
"integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.2.tgz",
|
||||
"integrity": "sha512-5Dzy66JlnRM6pkdOTF8+cGsB1fnERTE8Nc+Eed++fOWo1hdsBptCsbG8UuJkgtZt75bRtMJIrPeZmtfANixdFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"svgo": "^4.0.0"
|
||||
"svgo": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || ^20.9.0 || >= 18"
|
||||
@@ -2326,13 +2336,6 @@
|
||||
"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": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -2425,25 +2428,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
|
||||
"integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^11.1.0",
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^3.0.1",
|
||||
"css-tree": "^2.3.1",
|
||||
"css-what": "^6.1.0",
|
||||
"csso": "^5.0.5",
|
||||
"picocolors": "^1.1.1",
|
||||
"sax": "^1.4.1"
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"svgo": "bin/svgo.js"
|
||||
"svgo": "bin/svgo"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.1.0",
|
||||
"cssnano-preset-advanced": "^7.0.8",
|
||||
"esbuild": "^0.25.8",
|
||||
"cssnano": "^7.0.7",
|
||||
"cssnano-preset-advanced": "^7.0.7",
|
||||
"esbuild": "^0.25.6",
|
||||
"playwright": "^1.52.0",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-import": "^16.1.1",
|
||||
|
||||
Reference in New Issue
Block a user