aboutsummaryrefslogblamecommitdiff
path: root/cmd/goget/main.go
blob: ae8b3a6fd2da0483a2cb6be1df74f46ff53e7ce5 (plain) (tree)




































































































































































                                                                                                                            
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)
	})
}