├── 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 | --------------------------------------------------------------------------------