From da03bd023ba61a062ecf39e4dc5ec4442928356d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 8 Aug 2026 22:02:41 +0400 Subject: [PATCH] =?UTF-8?q?v1.0.3=20=E2=80=94=20=D1=80=D0=B5=D0=B2=D1=8C?= =?UTF-8?q?=D1=8E=20=D0=A1=D0=BE=D0=BD=D0=BD=D0=B5=D1=82=D0=B0:=20SSRF=20f?= =?UTF-8?q?ix,=20go.sum,=20readyz,=20sort,=20graceful=20shutdown,=20gofmt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +- server/docs.go | 154 ++++++++++++++++++++++++------------------------- server/go.mod | 26 +++++++++ server/go.sum | 60 +++++++++++++++++++ server/main.go | 48 ++++++++++++--- 5 files changed, 206 insertions(+), 86 deletions(-) create mode 100644 server/go.mod create mode 100644 server/go.sum diff --git a/Dockerfile b/Dockerfile index 7894b59..52b06d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,10 @@ FROM golang:1.24-alpine AS builder ENV GOTOOLCHAIN=auto WORKDIR /app +COPY server/go.mod server/go.sum ./ +RUN go mod download COPY server/ . -RUN go mod init tf-registry && go mod tidy && CGO_ENABLED=0 go build -o registry . +RUN CGO_ENABLED=0 go build -o registry . FROM alpine:3.21 RUN apk --no-cache add ca-certificates diff --git a/server/docs.go b/server/docs.go index ee111b4..5c63419 100644 --- a/server/docs.go +++ b/server/docs.go @@ -16,101 +16,101 @@ import ( // parseDocsRequestPath parses paths like: // /docs////... (rest may be empty) func parseDocsRequestPath(p string) (namespace, name, version, rest string, err error) { -p = strings.TrimPrefix(p, "/") -p = strings.TrimPrefix(p, "docs/") -parts := strings.SplitN(p, "/", 4) -if len(parts) < 3 { -err = fmt.Errorf("invalid docs path: %s", p) -return -} -namespace = parts[0] -name = parts[1] -version = parts[2] -if len(parts) == 3 { -rest = "" -} else { -rest = parts[3] -} -return + p = strings.TrimPrefix(p, "/") + p = strings.TrimPrefix(p, "docs/") + parts := strings.SplitN(p, "/", 4) + if len(parts) < 3 { + err = fmt.Errorf("invalid docs path: %s", p) + return + } + namespace = parts[0] + name = parts[1] + version = parts[2] + if len(parts) == 3 { + rest = "" + } else { + rest = parts[3] + } + return } // docsObjectKey builds the S3 key for a docs object given parsed parts. func docsObjectKey(namespace, name, version, pathPart string) string { -clean := strings.TrimPrefix(pathPart, "/") -if clean == "" { -return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version) -} -return fmt.Sprintf("docs/%s/%s/%s/%s", namespace, name, version, clean) + clean := strings.TrimPrefix(pathPart, "/") + if clean == "" { + return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version) + } + return fmt.Sprintf("docs/%s/%s/%s/%s", namespace, name, version, clean) } // tryCandidateKeys returns a list of keys to attempt for a given request path. // Order matters: exact path first, then /index.html, then top-level index. func tryCandidateKeys(namespace, name, version, rest string) []string { -keys := []string{} -if rest == "" { -keys = append(keys, docsObjectKey(namespace, name, version, "index.html")) -return keys -} -// exact -keys = append(keys, docsObjectKey(namespace, name, version, rest)) -// if it looks like a directory or has no extension, try index under it -if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" { -keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html")) -} -// finally, try root index -keys = append(keys, docsObjectKey(namespace, name, version, "index.html")) -return keys + keys := []string{} + if rest == "" { + keys = append(keys, docsObjectKey(namespace, name, version, "index.html")) + return keys + } + // exact + keys = append(keys, docsObjectKey(namespace, name, version, rest)) + // if it looks like a directory or has no extension, try index under it + if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" { + keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html")) + } + // finally, try root index + keys = append(keys, docsObjectKey(namespace, name, version, "index.html")) + return keys } // docsHandler serves static documentation files from S3 (public-facing via Ingress). func docsHandler(w http.ResponseWriter, r *http.Request) { -ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path) -if err != nil { -http.Error(w, "Bad docs path", http.StatusBadRequest) -return -} + ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path) + if err != nil { + http.Error(w, "Bad docs path", http.StatusBadRequest) + return + } -ctx := context.Background() -candidates := tryCandidateKeys(ns, name, ver, rest) -log.Printf("Docs candidates (host=%s): %v", hostname, candidates) + ctx := context.Background() + candidates := tryCandidateKeys(ns, name, ver, rest) + log.Printf("Docs candidates (host=%s): %v", hostname, candidates) -var lastErr error -for _, key := range candidates { + var lastErr error + for _, key := range candidates { obj, err := s3Client.GetObject(ctx, bucketName, key, s3.GetObjectOptions{}) -if err != nil { -lastErr = err -continue -} -stat, err := obj.Stat() -if err != nil { -lastErr = err -_ = obj.Close() -continue -} + if err != nil { + lastErr = err + continue + } + stat, err := obj.Stat() + if err != nil { + lastErr = err + _ = obj.Close() + continue + } -// Determine content-type -ext := filepath.Ext(key) -ctype := mime.TypeByExtension(ext) -if ctype == "" { -// fallback for HTML -if ext == ".html" || strings.HasSuffix(key, "index.html") { -ctype = "text/html; charset=utf-8" -} else { -ctype = "application/octet-stream" -} -} + // Determine content-type + ext := filepath.Ext(key) + ctype := mime.TypeByExtension(ext) + if ctype == "" { + // fallback for HTML + if ext == ".html" || strings.HasSuffix(key, "index.html") { + ctype = "text/html; charset=utf-8" + } else { + ctype = "application/octet-stream" + } + } -w.Header().Set("Content-Type", ctype) -w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size)) -w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat)) + w.Header().Set("Content-Type", ctype) + w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size)) + w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat)) -if _, err := io.Copy(w, obj); err != nil { -log.Printf("Error streaming object %s: %v", key, err) -} -_ = obj.Close() -return -} + if _, err := io.Copy(w, obj); err != nil { + log.Printf("Error streaming object %s: %v", key, err) + } + _ = obj.Close() + return + } -log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr) -http.Error(w, "Documentation not found", http.StatusNotFound) + log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr) + http.Error(w, "Documentation not found", http.StatusNotFound) } diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..89ef339 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,26 @@ +module tf-registry + +go 1.25.0 + +require github.com/minio/minio-go/v7 v7.2.1 + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/philhofer/fwd v1.2.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/tinylib/msgp v1.6.1 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.51.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/text v0.37.0 // indirect + gopkg.in/ini.v1 v1.67.2 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..50e482a --- /dev/null +++ b/server/go.sum @@ -0,0 +1,60 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= +github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw= +github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY= +github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/main.go b/server/main.go index 47fa313..43cb921 100644 --- a/server/main.go +++ b/server/main.go @@ -10,7 +10,11 @@ import ( "net/http" "net/url" "os" + "os/signal" + "sort" "strings" + "syscall" + "time" s3 "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -23,7 +27,7 @@ var ( s3Prefix = os.Getenv("S3_PREFIX") // S3 key prefix (may differ from hostname) ) -const VERSION = "1.0.2" +const VERSION = "1.0.3" // Terraform Registry Protocol Structs type Discovery struct { @@ -108,8 +112,27 @@ func main() { if port == "" { port = "5000" } - log.Printf("Starting Registry Service on :%s (Bucket: %s, Endpoint: %s)\n", port, bucketName, endpoint) - log.Fatal(http.ListenAndServe(":"+port, nil)) + + srv := &http.Server{Addr: ":" + port} + + go func() { + log.Printf("Starting Registry Service on :%s (Bucket: %s, Endpoint: %s)\n", port, bucketName, endpoint) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + log.Println("Server stopped") } func rootHandler(w http.ResponseWriter, r *http.Request) { @@ -133,15 +156,14 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } func proxyHandler(w http.ResponseWriter, r *http.Request) { - bucket := r.URL.Query().Get("bucket") key := r.URL.Query().Get("key") - if bucket == "" || key == "" { - http.Error(w, "Missing bucket or key params", http.StatusBadRequest) + if key == "" { + http.Error(w, "Missing key param", http.StatusBadRequest) return } - obj, err := s3Client.GetObject(context.Background(), bucket, key, s3.GetObjectOptions{}) + obj, err := s3Client.GetObject(context.Background(), bucketName, key, s3.GetObjectOptions{}) if err != nil { log.Printf("Error getting object %s/%s: %v", bucket, key, err) http.Error(w, "File not found", http.StatusNotFound) @@ -232,6 +254,10 @@ func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType strin versions = append(versions, *v) } + sort.Slice(versions, func(i, j int) bool { + return versions[i].Version < versions[j].Version + }) + resp := VersionList{ ID: fmt.Sprintf("%s/%s", namespace, pType), Versions: versions, @@ -294,7 +320,13 @@ func healthzHandler(w http.ResponseWriter, r *http.Request) { } func readyzHandler(w http.ResponseWriter, r *http.Request) { - // Проверить доступность S3 + _, err := s3Client.BucketExists(context.Background(), bucketName) + if err != nil { + log.Printf("readyz: S3 bucket check failed: %v", err) + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte("s3 unreachable")) + return + } w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }