├── files
└── README.txt
├── www
├── index.html
└── 404.html
├── .gitignore
├── dockerfiles
├── alpine
│ └── Dockerfile
└── scratch
│ └── Dockerfile
├── LICENSE
├── Dockerfile
├── README.md
├── go.mod
├── goreleaser.yml
├── CLAUDE.md
├── cmd
└── asws.go
└── go.sum
/files/README.txt:
--------------------------------------------------------------------------------
1 | This is a file in the files directory.
--------------------------------------------------------------------------------
/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ASWS
4 |
5 |
6 | ASWS
7 | Welcome to the ASWS server.
8 |
9 |
--------------------------------------------------------------------------------
/www/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ASWS
4 |
5 |
6 | ASWS - 404
7 | Welcome to the ASWS 404 page.
8 |
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .claude
3 | dist
4 | asws
5 |
6 | # Binaries for programs and plugins
7 | *.exe
8 | *.dll
9 | *.so
10 | *.dylib
11 |
12 | # Test binary, build with `go test -c`
13 | *.test
14 |
15 | # Output of the go coverage tool, specifically when used with LiteIDE
16 | *.out
17 |
18 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
19 | .glide/
--------------------------------------------------------------------------------
/dockerfiles/alpine/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.18.3 as Builder
2 |
3 | RUN mkdir -p /www && mkdir -p /files
4 | COPY www/index.html www/index.html
5 | COPY files/README.txt files/README.txt
6 | COPY asws /bin/asws
7 |
8 | RUN echo "nobody:x:65534:65534:Nobody:/:" > /etc/passwd
9 |
10 | ENV PATH=/bin
11 |
12 | WORKDIR /
13 |
14 | ENV IP=0.0.0.0
15 |
16 | USER nobody
17 | ENTRYPOINT ["/bin/asws"]
--------------------------------------------------------------------------------
/dockerfiles/scratch/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.12.1 as Builder
2 |
3 | RUN mkdir -p /www && mkdir -p /files
4 | COPY www/index.html www/index.html
5 | COPY files/README.txt files/README.txt
6 | COPY asws /bin/asws
7 |
8 | RUN echo "nobody:x:65534:65534:Nobody:/:" > /etc_passwd
9 |
10 | WORKDIR /
11 |
12 | FROM scratch
13 |
14 | ENV PATH=/bin
15 |
16 | WORKDIR /
17 |
18 | ENV IP=0.0.0.0
19 |
20 | COPY --from=builder /etc_passwd /etc/passwd
21 | COPY --from=builder /www/index.html /www/index.html
22 | COPY --from=builder /files/README.txt /files/README.txt
23 | COPY --from=builder /bin/asws /bin/asws
24 |
25 | USER nobody
26 | ENTRYPOINT ["/bin/asws"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Craig Johnston
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG app=asws
2 | ARG project=github.com/txn2/asws
3 | ARG buildsrc=./cmd/asws.go
4 |
5 | FROM golang:1.20.6-alpine AS builder
6 |
7 | ARG app
8 | ARG project
9 | ARG buildsrc
10 | ARG version
11 |
12 | ENV PROJECT=${project} \
13 | APP=${app} \
14 | BUILDSRC=${buildsrc} \
15 | CGO_ENABLED=0 \
16 | GOOS=linux \
17 | GOARCH=amd64
18 |
19 | RUN mkdir -p /go/src/ \
20 | && mkdir -p /go/bin \
21 | && mkdir -p /go/pkg
22 |
23 | ENV PATH=/go/bin:$PATH
24 |
25 | RUN mkdir -p /go/src/$PROJECT/
26 | ADD . /go/src/$PROJECT/
27 |
28 | WORKDIR /go/src/$PROJECT/
29 |
30 | RUN go build -ldflags "-X main.Version=${version} -extldflags \"-static\"" -o /go/bin/app $BUILDSRC
31 | RUN echo "nobody:x:65534:65534:Nobody:/:" > /etc_passwd
32 |
33 | RUN mkdir -p /www && mkdir -p /files
34 | COPY www/index.html /www/index.html
35 | COPY files/README.txt /files/README.txt
36 |
37 | FROM scratch
38 |
39 | ENV IP=0.0.0.0
40 | ENV PATH=/bin
41 |
42 | COPY --from=builder /etc_passwd /etc/passwd
43 | COPY --from=builder /www/index.html /www/index.html
44 | COPY --from=builder /files/README.txt /files/README.txt
45 | COPY --from=builder /go/bin/app /bin/asws
46 |
47 | WORKDIR /
48 |
49 | USER nobody
50 | ENTRYPOINT ["/bin/asws"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [ASWS] Static Web Server
2 |
3 | ## Serve a Static Site
4 |
5 | The following example exposes port 2701 on your local machine and forwards all traffic to port 80 on the [asws docker container]:
6 |
7 | ```bash
8 | docker run -e DEBUG=true -p 2701:80 -v "$(pwd)"/www:/www txn2/asws:v1.6.1
9 | ```
10 |
11 | ## SPA (Single Page Application) Mode
12 |
13 | For modern React, Vue, or Angular applications with client-side routing, enable SPA fallback mode:
14 |
15 | ```bash
16 | docker run -e SPA_FALLBACK=true -p 8080:80 -v "$(pwd)"/dist:/www txn2/asws:latest
17 | ```
18 |
19 | This mode serves `index.html` with a 200 status code for any non-file request, allowing client-side routers to handle routing.
20 |
21 | **Note:** SPA_FALLBACK takes precedence over NOT_FOUND_REDIRECT and NOT_FOUND_FILE.
22 |
23 | ## Environment Variable Defaults
24 |
25 | - PORT="80"
26 | - STATIC_DIR="./www"
27 | - STATIC_PATH="./www"
28 | - SPA_FALLBACK="false"
29 | - FS_ENABLED="no"
30 | - FS_DIR="./files"
31 | - FS_PATH="/files"
32 | - DEBUG="false"
33 | - METRICS="true"
34 | - METRICS_PORT="9696"
35 |
36 | ### Build Release
37 |
38 | Build test release:
39 | ```bash
40 | goreleaser --skip-publish --rm-dist --skip-validate
41 | ```
42 |
43 | Build and release:
44 | ```bash
45 | GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist
46 | ```
47 |
48 |
49 | [asws docker container]: https://hub.docker.com/r/txn2/asws/
50 | [ASWS]: https://github.com/txn2/asws
51 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/txn2/asws
2 |
3 | go 1.24.7
4 |
5 | require (
6 | github.com/gin-contrib/zap v1.1.5
7 | github.com/gin-gonic/gin v1.11.0
8 | github.com/prometheus/client_golang v1.23.2
9 | go.uber.org/zap v1.27.0
10 | )
11 |
12 | require (
13 | github.com/beorn7/perks v1.0.1 // indirect
14 | github.com/bytedance/gopkg v0.1.3 // indirect
15 | github.com/bytedance/sonic v1.14.2 // indirect
16 | github.com/bytedance/sonic/loader v0.4.0 // indirect
17 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
18 | github.com/cloudwego/base64x v0.1.6 // indirect
19 | github.com/gabriel-vasile/mimetype v1.4.11 // indirect
20 | github.com/gin-contrib/sse v1.1.0 // indirect
21 | github.com/go-playground/locales v0.14.1 // indirect
22 | github.com/go-playground/universal-translator v0.18.1 // indirect
23 | github.com/go-playground/validator/v10 v10.28.0 // indirect
24 | github.com/goccy/go-json v0.10.5 // indirect
25 | github.com/goccy/go-yaml v1.18.0 // indirect
26 | github.com/json-iterator/go v1.1.12 // indirect
27 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect
28 | github.com/leodido/go-urn v1.4.0 // indirect
29 | github.com/mattn/go-isatty v0.0.20 // indirect
30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
31 | github.com/modern-go/reflect2 v1.0.2 // indirect
32 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
33 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect
34 | github.com/prometheus/client_model v0.6.2 // indirect
35 | github.com/prometheus/common v0.66.1 // indirect
36 | github.com/prometheus/procfs v0.16.1 // indirect
37 | github.com/quic-go/qpack v0.6.0 // indirect
38 | github.com/quic-go/quic-go v0.57.0 // indirect
39 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
40 | github.com/ugorji/go/codec v1.3.1 // indirect
41 | go.uber.org/mock v0.6.0 // indirect
42 | go.uber.org/multierr v1.11.0 // indirect
43 | go.yaml.in/yaml/v2 v2.4.2 // indirect
44 | golang.org/x/arch v0.22.0 // indirect
45 | golang.org/x/crypto v0.45.0 // indirect
46 | golang.org/x/net v0.47.0 // indirect
47 | golang.org/x/sys v0.38.0 // indirect
48 | golang.org/x/text v0.31.0 // indirect
49 | google.golang.org/protobuf v1.36.10 // indirect
50 | )
51 |
--------------------------------------------------------------------------------
/goreleaser.yml:
--------------------------------------------------------------------------------
1 | env:
2 | - GO111MODULE=on
3 | - GOPROXY=https://proxy.golang.org/
4 | before:
5 | hooks:
6 | - go mod download
7 |
8 | builds:
9 | - id: asws
10 | main: ./cmd/asws.go
11 | binary: asws
12 | goos:
13 | - linux
14 | - darwin
15 | goarch:
16 | - "386"
17 | - amd64
18 | - arm
19 | - arm64
20 | mod_timestamp: '{{ .CommitTimestamp }}'
21 | env:
22 | - CGO_ENABLED=0
23 | flags:
24 | - -trimpath
25 | - -tags=netgo
26 | - -a
27 | - -v
28 | ldflags: -s -w -X main.Version={{.Version}}
29 |
30 | checksum:
31 | name_template: '{{ .ProjectName }}_checksums.txt'
32 |
33 | changelog:
34 | sort: asc
35 | filters:
36 | exclude:
37 | - '^docs:'
38 | - '^test:'
39 | - Merge pull request
40 | - Merge branch
41 | - go mod tidy
42 |
43 | nfpms:
44 | - file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
45 | homepage: https://github.com/txn2/asws
46 | description: Statis web server.
47 | maintainer: Craig Johnston https://twitter.com/cjimti
48 | license: Apache 2.0
49 | vendor: https://github.com/txn2
50 | formats:
51 | - apk
52 | - deb
53 | - rpm
54 |
55 | release:
56 | github:
57 | owner: txn2
58 | name: asws
59 | name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}"
60 |
61 | dockers:
62 | -
63 | goos: linux
64 | goarch: amd64
65 | goarm: ''
66 | ids:
67 | - asws
68 | dockerfile: dockerfiles/scratch/Dockerfile
69 | extra_files:
70 | - www
71 | - files
72 | image_templates:
73 | - "txn2/asws:latest"
74 | - "txn2/asws:{{ .Tag }}"
75 | - "txn2/asws:v{{ .Major }}"
76 | - "txn2/asws:latest-scratch"
77 | - "txn2/asws:{{ .Tag }}-scratch"
78 | - "txn2/asws:v{{ .Major }}-scratch"
79 | build_flag_templates:
80 | - "--label=org.label-schema.schema-version=1.0"
81 | - "--label=org.label-schema.version={{.Version}}"
82 | - "--label=org.label-schema.name={{.ProjectName}}"
83 | -
84 | goos: linux
85 | goarch: amd64
86 | goarm: ''
87 | ids:
88 | - asws
89 | dockerfile: dockerfiles/alpine/Dockerfile
90 | extra_files:
91 | - www
92 | - files
93 | image_templates:
94 | - "txn2/asws:latest-alpine"
95 | - "txn2/asws:{{ .Tag }}-alpine"
96 | - "txn2/asws:v{{ .Major }}-alpine-3"
97 | - "txn2/asws:{{ .Tag }}-alpine-3"
98 | build_flag_templates:
99 | - "--label=org.label-schema.schema-version=1.0"
100 | - "--label=org.label-schema.version={{.Version}}"
101 | - "--label=org.label-schema.name={{.ProjectName}}"
102 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | ASWS (A Static Web Server) is a lightweight, configurable static web server built with Go. It uses the Gin web framework and includes built-in Prometheus metrics support. The project is designed to be deployed as a Docker container, with multi-architecture support via GoReleaser.
8 |
9 | ## Architecture
10 |
11 | ### Single Binary Application
12 | - Main entry point: `cmd/asws.go`
13 | - Self-contained Go binary with no external dependencies
14 | - All configuration via environment variables or command-line flags
15 | - No test files present in the codebase
16 |
17 | ### Core Components
18 |
19 | **Web Server (`cmd/asws.go`)**
20 | - Uses Gin web framework in release mode (or debug mode when DEBUG=true)
21 | - Structured logging via uber/zap with request logging middleware
22 | - Two-server architecture:
23 | - Main server: Serves static content (configurable IP/port)
24 | - Metrics server: Prometheus metrics endpoint (runs in goroutine on separate port)
25 |
26 | **Static File Serving**
27 | - Primary static path: Configurable via STATIC_PATH/STATIC_DIR (default: "/" serves from "./www")
28 | - Optional filesystem mode: Additional file browsing via FS_PATH/FS_DIR (default: "/files" from "./files")
29 | - Custom 404 handling: Can serve custom 404.html or redirect to a path
30 |
31 | **Metrics & Observability**
32 | - Prometheus metrics exposed on separate port (default: 2112)
33 | - Service info metrics include Go version, service version, and service name
34 | - Request logging integrated with zap logger
35 |
36 | ## Common Commands
37 |
38 | ### Development
39 |
40 | **Build the application:**
41 | ```bash
42 | go build -o asws ./cmd/asws.go
43 | ```
44 |
45 | **Run locally:**
46 | ```bash
47 | go run ./cmd/asws.go -debug=true -port=8080
48 | ```
49 |
50 | **Run with custom configuration:**
51 | ```bash
52 | DEBUG=true PORT=8080 STATIC_DIR=./www go run ./cmd/asws.go
53 | ```
54 |
55 | ### Docker
56 |
57 | **Build Docker image:**
58 | ```bash
59 | docker build -t asws:local .
60 | ```
61 |
62 | **Run Docker container:**
63 | ```bash
64 | docker run -e DEBUG=true -p 2701:80 -v "$(pwd)"/www:/www txn2/asws:latest
65 | ```
66 |
67 | ### Release Management
68 |
69 | **Test release locally (without publishing):**
70 | ```bash
71 | goreleaser --skip-publish --rm-dist --skip-validate
72 | ```
73 |
74 | **Create and publish release:**
75 | ```bash
76 | GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist
77 | ```
78 |
79 | ## Configuration
80 |
81 | All configuration uses environment variables or command-line flags (flags override env vars). Key variables:
82 |
83 | - **IP/PORT**: Bind address and main server port (default: 127.0.0.1:8080)
84 | - **STATIC_DIR/STATIC_PATH**: Where and how to serve static files (default: ./www at /)
85 | - **FS_ENABLED/FS_DIR/FS_PATH**: Optional file browsing endpoint (default: disabled)
86 | - **NOT_FOUND_REDIRECT/NOT_FOUND_REDIRECT_PATH**: Redirect behavior for 404s (default: false)
87 | - **NOT_FOUND_FILE**: Custom 404 page (default: ./www/404.html)
88 | - **DEBUG**: Enable debug mode and verbose logging (default: false)
89 | - **METRICS/METRICS_PORT**: Prometheus metrics server (default: enabled on 2112)
90 | - **APP_NAME/APP_VERSION**: Application metadata for logging
91 |
92 | See `cmd/asws.go:20-35` for complete configuration reference with defaults.
93 |
94 | ## Release Process
95 |
96 | GoReleaser handles multi-platform builds and Docker image creation:
97 |
98 | - **Platforms**: Linux and Darwin (macOS) for 386, amd64, arm, arm64
99 | - **Docker variants**:
100 | - Scratch-based images (minimal size, multi-tag: latest, versioned, major version)
101 | - Alpine-based images (with shell/debugging tools)
102 | - **Artifacts**: Binaries, checksums, deb/rpm/apk packages
103 | - **Version injection**: Version string injected at build time via ldflags (`-X main.Version={{.Version}}`)
104 |
105 | The build is configured in `goreleaser.yml`.
106 |
107 | ## Directory Structure
108 |
109 | ```
110 | cmd/ - Main application entry point
111 | www/ - Default static content directory
112 | files/ - Default file browsing directory (when FS_ENABLED=true)
113 | dockerfiles/ - Docker build configurations (scratch and alpine variants)
114 | ```
115 |
116 | ## Dependencies
117 |
118 | Core dependencies:
119 | - `github.com/gin-gonic/gin` - HTTP web framework
120 | - `github.com/gin-contrib/zap` - Zap logging middleware for Gin
121 | - `go.uber.org/zap` - Structured logging
122 | - `github.com/prometheus/client_golang` - Prometheus metrics
123 |
124 | ## Notes
125 |
126 | - No automated tests exist in this codebase
127 | - The application is designed for container deployment with security considerations (runs as nobody user in Docker)
128 | - CGO is disabled for static binary compilation
129 | - Version information is injected at build time, not hardcoded
--------------------------------------------------------------------------------
/cmd/asws.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "net/http"
6 | "os"
7 | "runtime"
8 | "time"
9 |
10 | "github.com/prometheus/client_golang/prometheus"
11 | "github.com/prometheus/client_golang/prometheus/promauto"
12 |
13 | ginzap "github.com/gin-contrib/zap"
14 | "github.com/gin-gonic/gin"
15 | "github.com/prometheus/client_golang/prometheus/promhttp"
16 | "go.uber.org/zap"
17 | )
18 |
19 | var (
20 | ipEnv = getEnv("IP", "127.0.0.1")
21 | portEnv = getEnv("PORT", "8080")
22 | staticDirEnv = getEnv("STATIC_DIR", "./www")
23 | staticPathEnv = getEnv("STATIC_PATH", "/")
24 | notFoundRedirectEnv = getEnv("NOT_FOUND_REDIRECT", "false")
25 | notFoundRedirectPathEnv = getEnv("NOT_FOUND_REDIRECT_PATH", "/")
26 | notFoundFileEnv = getEnv("NOT_FOUND_FILE", "./www/404.html")
27 | spaFallbackEnv = getEnv("SPA_FALLBACK", "false")
28 | fsEnabledEnv = getEnv("FS_ENABLED", "false")
29 | fsDirEnv = getEnv("FS_DIR", "./files")
30 | fsPathEnv = getEnv("FS_PATH", "/files")
31 | debugEnv = getEnv("DEBUG", "false")
32 | metricsEnv = getEnv("METRICS", "true")
33 | metricsPortEnv = getEnv("METRICS_PORT", "2112")
34 | appNameEnv = getEnv("APP_NAME", "")
35 | appVersionEnv = getEnv("APP_VERSION", "")
36 | )
37 |
38 | var Version = "0.0.0"
39 | var Service = "asws"
40 |
41 | func main() {
42 |
43 | var (
44 | ip = flag.String("ip", ipEnv, "bind ip")
45 | port = flag.String("port", portEnv, "port")
46 | staticDir = flag.String("staticDir", staticDirEnv, "static dir")
47 | staticPath = flag.String("staticPath", staticPathEnv, "static path")
48 | notFoundRedirect = flag.String("notFoundRedirect", notFoundRedirectEnv, "redirect on not found?")
49 | notFoundRedirectPath = flag.String("notFoundRedirectPath", notFoundRedirectPathEnv, "not found redirect path")
50 | notFoundFile = flag.String("notFoundFile", notFoundFileEnv, "not found file to serve")
51 | spaFallback = flag.String("spaFallback", spaFallbackEnv, "SPA fallback mode (serve index.html on 404)")
52 | fsEnabled = flag.String("fsEnabled", fsEnabledEnv, "filesystem enabled")
53 | fsDir = flag.String("fsDir", fsDirEnv, "filesystem directory")
54 | fsPath = flag.String("fsPath", fsPathEnv, "filesystem path")
55 | debug = flag.String("debug", debugEnv, "debug")
56 | metrics = flag.String("metrics", metricsEnv, "metrics")
57 | metricsPort = flag.String("metricsPort", metricsPortEnv, "metrics port")
58 | appName = flag.String("appName", appNameEnv, "web app name (for logging)")
59 | appVersion = flag.String("appVersion", appVersionEnv, "web app version (for logging)")
60 | )
61 | flag.Parse()
62 |
63 | // add some useful info to metrics
64 | promauto.NewCounter(prometheus.CounterOpts{
65 | Namespace: "www",
66 | Name: "info",
67 | ConstLabels: prometheus.Labels{
68 | "go_version": runtime.Version(),
69 | "version": Version,
70 | "service": Service,
71 | },
72 | }).Inc()
73 |
74 | gin.SetMode(gin.ReleaseMode)
75 |
76 | if *debug == "true" {
77 | gin.SetMode(gin.DebugMode)
78 | }
79 |
80 | r := gin.New()
81 |
82 | zapCfg := zap.NewProductionConfig()
83 | zapCfg.DisableCaller = true
84 | zapCfg.DisableStacktrace = true
85 |
86 | baseLogger, _ := zapCfg.Build()
87 |
88 | if *debug == "true" {
89 | baseLogger, _ = zap.NewDevelopment()
90 | }
91 |
92 | logger := baseLogger.With(
93 | zap.String("asws_version", Version),
94 | zap.Stringp("app", appName),
95 | zap.Stringp("version", appVersion),
96 | )
97 |
98 | r.Use(ginzap.Ginzap(logger, time.RFC3339, true))
99 |
100 | if *fsEnabled == "true" {
101 | r.StaticFS(*fsPath, http.Dir(*fsDir))
102 | }
103 |
104 | r.Static(*staticPath, *staticDir)
105 |
106 | r.NoRoute(func(c *gin.Context) {
107 | // SPA fallback mode - serve index.html with 200 status for non-file requests
108 | if *spaFallback == "true" {
109 | indexPath := *staticDir + "/index.html"
110 | content, err := os.ReadFile(indexPath)
111 | if err != nil {
112 | logger.Error("SPA index.html not found", zap.String("file", indexPath))
113 | c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
114 | return
115 | }
116 |
117 | c.Writer.WriteHeader(http.StatusOK) // Important: 200 not 404
118 | c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
119 | _, err = c.Writer.Write(content)
120 | if err != nil {
121 | logger.Error("SPA write error", zap.Error(err))
122 | c.String(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
123 | return
124 | }
125 | return
126 | }
127 |
128 | // Redirect mode (existing functionality)
129 | if *notFoundRedirect == "true" {
130 | c.Redirect(http.StatusTemporaryRedirect, *notFoundRedirectPath)
131 | c.Abort()
132 | return
133 | }
134 |
135 | // Custom 404 file mode (existing functionality)
136 | content, err := os.ReadFile(*notFoundFile)
137 | if err != nil {
138 | logger.Error("404 content not found", zap.String("file", *notFoundFile))
139 | c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
140 | return
141 | }
142 |
143 | c.Writer.WriteHeader(http.StatusNotFound)
144 | _, err = c.Writer.Write(content)
145 | if err != nil {
146 | logger.Error("404 write error", zap.Error(err))
147 | c.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
148 | return
149 | }
150 | })
151 |
152 | // metrics server (run in go routine)
153 | if *metrics == "true" {
154 | go func() {
155 | http.Handle("/metrics", promhttp.Handler())
156 |
157 | logger.Info("Starting ASWS Metrics Server",
158 | zap.String("type", "start_asws_metrics"),
159 | zap.String("port", *metricsPort),
160 | zap.String("ip", *ip),
161 | )
162 |
163 | err := http.ListenAndServe(*ip+":"+*metricsPort, nil)
164 | if err != nil {
165 | logger.Fatal("Error Starting ASWS Metrics Server", zap.Error(err))
166 | os.Exit(1)
167 | }
168 | }()
169 | }
170 |
171 | logger.Info("Starting ASWS Server",
172 | zap.String("type", "start_asws"),
173 | zap.String("port", *port),
174 | zap.String("ip", *ip),
175 | )
176 |
177 | // Gin web server
178 | err := r.Run(*ip + ":" + *port)
179 | if err != nil {
180 | logger.Fatal(err.Error())
181 | }
182 | }
183 |
184 | // getEnv gets an environment variable or sets a default if
185 | // one does not exist.
186 | func getEnv(key, fallback string) string {
187 | value := os.Getenv(key)
188 | if len(value) == 0 {
189 | return fallback
190 | }
191 |
192 | return value
193 | }
194 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
4 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
5 | github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
6 | github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
7 | github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
8 | github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
11 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
12 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16 | github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
17 | github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
18 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
19 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
20 | github.com/gin-contrib/zap v1.1.5 h1:qKwhWb4DQgPriCl1AHLLob6hav/KUIctKXIjTmWIN3I=
21 | github.com/gin-contrib/zap v1.1.5/go.mod h1:lAchUtGz9M2K6xDr1rwtczyDrThmSx6c9F384T45iOE=
22 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
23 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
24 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
25 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
26 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
27 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
28 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
29 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
30 | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
31 | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
32 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
33 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
34 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
35 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
36 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
37 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
38 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
39 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
40 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
41 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
42 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
43 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
44 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
45 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
46 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
47 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
48 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
49 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
50 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
51 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
52 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
53 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
54 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
58 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
59 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
62 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
63 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
66 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
67 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
68 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
69 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
70 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
71 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
72 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
73 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
74 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
75 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
76 | github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
77 | github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
78 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
79 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
81 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
82 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
83 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
85 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
86 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
87 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
88 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
89 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
90 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
91 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
92 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
93 | github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
94 | github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
95 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
96 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
97 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
98 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
99 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
100 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
101 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
102 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
103 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
104 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
105 | golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
106 | golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
107 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
108 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
109 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
110 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
111 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
112 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
113 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
114 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
115 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
116 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
117 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
118 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
119 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
120 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
121 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
122 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
123 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
124 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
125 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
126 |
--------------------------------------------------------------------------------