aboutsummaryrefslogtreecommitdiff
path: root/cmd/goget/main.go
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2025-08-24 11:09:08 -0700
committerFranck Cuny <franck@fcuny.net>2025-08-24 11:25:39 -0700
commit01af0739c6a24d80edaa4000fa9509e6e90f0566 (patch)
tree4c03062c122749e998d7c79f70334b6f6ead064c /cmd/goget/main.go
parentset some environment variables in the dev shells (diff)
downloadx-01af0739c6a24d80edaa4000fa9509e6e90f0566.tar.gz
add Go vanity URL service for custom import paths
Go vanity URLs allow developers to use custom domain names for Go module imports instead of being tied to specific hosting platforms like GitHub. This service implements the go-import meta tag protocol, allowing Go tools to automatically discover the actual Git repository location while presenting a clean, branded import path to users.
Diffstat (limited to 'cmd/goget/main.go')
-rw-r--r--cmd/goget/main.go165
1 files changed, 165 insertions, 0 deletions
diff --git a/cmd/goget/main.go b/cmd/goget/main.go
new file mode 100644
index 0000000..ae8b3a6
--- /dev/null
+++ b/cmd/goget/main.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "slices"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "golang.org/x/mod/semver"
+)
+
+const (
+ forgeDomain = "code.fcuny.net"
+ forgeUser = "fcuny"
+ goPkgDomain = "go.fcuny.net"
+)
+
+var (
+ goGetReqs = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "goget_requests_total",
+ Help: "go get requests processed, by repository name.",
+ }, []string{"name"})
+
+ modules = []string{"x"}
+)
+
+func main() {
+ ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
+ defer cancel()
+
+ metricsMux := http.NewServeMux()
+ metricsMux.Handle("/metrics", promhttp.Handler())
+ metricsServer := &http.Server{
+ Addr: ":9091", Handler: metricsMux,
+ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second,
+ }
+
+ go func() {
+ log.Println("Starting metrics server on :9091")
+ if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Printf("Metrics server error: %v", err)
+ }
+ }()
+
+ s := &http.Server{
+ Addr: ":8080",
+ Handler: handler(),
+ ReadTimeout: 10 * time.Second,
+ WriteTimeout: 10 * time.Second,
+ IdleTimeout: 1 * time.Minute,
+ }
+
+ go func() {
+ log.Println("Starting main server on :8080")
+ if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Printf("Main server error: %v", err)
+ }
+ }()
+
+ <-ctx.Done()
+ log.Println("Shutdown signal received, starting graceful shutdown...")
+
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer shutdownCancel()
+
+ var shutdownErr error
+
+ log.Println("Shutting down main server...")
+ if err := s.Shutdown(shutdownCtx); err != nil {
+ log.Printf("Main server shutdown error: %v", err)
+ shutdownErr = err
+ }
+
+ log.Println("Shutting down metrics server...")
+ if err := metricsServer.Shutdown(shutdownCtx); err != nil {
+ log.Printf("Metrics server shutdown error: %v", err)
+ if shutdownErr == nil {
+ shutdownErr = err
+ }
+ }
+
+ if shutdownErr != nil {
+ log.Printf("Shutdown completed with errors")
+ os.Exit(1)
+ }
+
+ log.Println("Shutdown completed successfully")
+}
+
+func handler() http.Handler {
+ mux := http.NewServeMux()
+
+ mux.Handle(goPkgDomain+"/{name}", siteHandler(modules))
+ mux.Handle(goPkgDomain+"/{name}/", siteHandler(modules))
+
+ goGetMux := http.NewServeMux()
+ for _, name := range modules {
+ module := goPkgDomain + "/" + name
+ goGetMux.Handle(
+ module+"/",
+ goImportHandler(module, "https://"+forgeDomain+"/"+forgeUser+"/"+name),
+ )
+ }
+
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ status := http.StatusMethodNotAllowed
+ http.Error(rw, http.StatusText(status), status)
+ return
+ }
+
+ if r.URL.Query().Get("go-get") == "1" {
+ goGetMux.ServeHTTP(rw, r)
+ return
+ }
+ mux.ServeHTTP(rw, r)
+ })
+}
+
+func goImportHandler(module, repo string) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ goGetReqs.WithLabelValues(module).Inc()
+ w.Header().Set("Content-Type", "text/html; charset=UTF-8")
+ if _, err := fmt.Fprintf(w, `<head><meta name="go-import" content="%s git %s">`, module, repo); err != nil {
+ log.Printf("Error writing response: %v", err)
+ }
+ })
+}
+
+func siteHandler(names []string) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+ name := r.PathValue("name")
+ name, version, hasV := strings.Cut(name, "@")
+ if !hasV {
+ name, _, _ = strings.Cut(name, ".")
+ }
+ if hasV && !semver.IsValid(version) || !slices.Contains(names, name) {
+ http.NotFound(rw, r)
+ return
+ }
+ u := &url.URL{
+ Scheme: "https",
+ Host: forgeDomain,
+ Path: forgeUser + r.URL.Path,
+ }
+ if !hasV {
+ path, symbol, hasSymbol := strings.Cut(r.URL.Path, ".")
+ if hasSymbol {
+ u.Path = forgeUser + "/" + path
+ u.Fragment = symbol
+ }
+ }
+ http.Redirect(rw, r, u.String(), http.StatusFound)
+ })
+}