1
0
Fork 1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-06-02 08:38:17 -04:00
pkg-proxy/internal/handler/maven.go

188 lines
6.1 KiB
Go
Raw Permalink Normal View History

2026-01-20 21:52:44 +00:00
package handler
import (
"errors"
2026-01-20 21:52:44 +00:00
"fmt"
"net/http"
"path"
"strings"
)
const (
mavenCentralUpstream = "https://repo1.maven.org/maven2"
gradlePluginPortalUpstream = "https://plugins.gradle.org/m2"
minMavenParts = 4 // group path segments + artifact + version + filename
2026-01-20 21:52:44 +00:00
)
// MavenHandler handles Maven repository protocol requests.
type MavenHandler struct {
proxy *Proxy
upstreamURL string
pluginPortalUpstreamURL string
proxyURL string
2026-01-20 21:52:44 +00:00
}
// NewMavenHandler creates a new Maven repository handler.
func NewMavenHandler(proxy *Proxy, proxyURL, upstreamURL, pluginPortalUpstreamURL string) *MavenHandler {
if strings.TrimSpace(upstreamURL) == "" {
upstreamURL = mavenCentralUpstream
}
if strings.TrimSpace(pluginPortalUpstreamURL) == "" {
pluginPortalUpstreamURL = gradlePluginPortalUpstream
}
2026-01-20 21:52:44 +00:00
return &MavenHandler{
proxy: proxy,
upstreamURL: strings.TrimSuffix(upstreamURL, "/"),
pluginPortalUpstreamURL: strings.TrimSuffix(pluginPortalUpstreamURL, "/"),
proxyURL: strings.TrimSuffix(proxyURL, "/"),
2026-01-20 21:52:44 +00:00
}
}
// Routes returns the HTTP handler for Maven requests.
func (h *MavenHandler) Routes() http.Handler {
mux := http.NewServeMux()
// Maven repository layout: /{group}/{artifact}/{version}/{filename}
// e.g., /com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar
mux.HandleFunc("GET /", h.handleRequest)
return mux
}
// handleRequest routes Maven requests based on the path.
func (h *MavenHandler) handleRequest(w http.ResponseWriter, r *http.Request) {
urlPath := strings.TrimPrefix(r.URL.Path, "/")
if urlPath == "" {
http.NotFound(w, r)
return
}
// Check if this is an artifact file or metadata
filename := path.Base(urlPath)
if h.isMetadataFile(filename) {
h.handleMetadata(w, r, urlPath)
2026-01-20 21:52:44 +00:00
return
}
if h.isArtifactFile(filename) {
// Cache artifact files
h.handleDownload(w, r, urlPath)
return
}
// Proxy everything else (directory listings, checksums, etc.)
h.proxyUpstream(w, r)
}
func (h *MavenHandler) handleMetadata(w http.ResponseWriter, r *http.Request, urlPath string) {
cacheKey := strings.ReplaceAll(urlPath, "/", "_")
upstreamURL := fmt.Sprintf("%s/%s", h.upstreamURL, urlPath)
body, contentType, err := h.proxy.FetchOrCacheMetadata(r.Context(), "maven", cacheKey, upstreamURL, "*/*")
if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
pluginPortalURL := fmt.Sprintf("%s/%s", h.pluginPortalUpstreamURL, urlPath)
h.proxy.Logger.Info("maven metadata unavailable in primary upstream, trying Gradle Plugin Portal",
"path", urlPath)
body, contentType, err = h.proxy.FetchOrCacheMetadata(r.Context(), "maven", cacheKey, pluginPortalURL, "*/*")
}
}
if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
h.proxy.Logger.Error("metadata fetch failed", "error", err)
http.Error(w, "failed to fetch from upstream", http.StatusBadGateway)
return
}
h.proxy.writeMetadataCachedResponse(w, r, "maven", cacheKey, body, contentType)
}
2026-01-20 21:52:44 +00:00
// handleDownload serves an artifact file, fetching and caching from upstream if needed.
func (h *MavenHandler) handleDownload(w http.ResponseWriter, r *http.Request, urlPath string) {
// Parse Maven path: group/artifact/version/filename
// e.g., com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar
group, artifact, version, filename := h.parsePath(urlPath)
if artifact == "" {
h.proxyUpstream(w, r)
return
}
// Maven uses group:artifact as the package name
name := fmt.Sprintf("%s:%s", group, artifact)
h.proxy.Logger.Info("maven download request",
"group", group, "artifact", artifact, "version", version, "filename", filename)
upstreamURL := fmt.Sprintf("%s/%s", h.upstreamURL, urlPath)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "maven", name, version, filename, upstreamURL)
if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
pluginPortalURL := fmt.Sprintf("%s/%s", h.pluginPortalUpstreamURL, urlPath)
h.proxy.Logger.Info("maven artifact not found in primary upstream, trying Gradle Plugin Portal",
"group", group, "artifact", artifact, "version", version, "filename", filename)
result, err = h.proxy.GetOrFetchArtifactFromURL(r.Context(), "maven", name, version, filename, pluginPortalURL)
}
}
if err != nil {
if errors.Is(err, ErrUpstreamNotFound) {
http.Error(w, "not found", http.StatusNotFound)
return
}
2026-01-20 21:52:44 +00:00
h.proxy.Logger.Error("failed to get artifact", "error", err)
http.Error(w, "failed to fetch artifact", http.StatusBadGateway)
return
}
ServeArtifact(w, result)
}
// parsePath extracts Maven coordinates from a URL path.
// e.g., "com/google/guava/guava/32.1.3-jre/guava-32.1.3-jre.jar"
// -> ("com.google.guava", "guava", "32.1.3-jre", "guava-32.1.3-jre.jar")
func (h *MavenHandler) parsePath(urlPath string) (group, artifact, version, filename string) {
parts := strings.Split(urlPath, "/")
if len(parts) < minMavenParts {
2026-01-20 21:52:44 +00:00
return "", "", "", ""
}
filename = parts[len(parts)-1]
version = parts[len(parts)-2]
artifact = parts[len(parts)-3]
groupParts := parts[:len(parts)-3]
group = strings.Join(groupParts, ".")
return group, artifact, version, filename
}
// isArtifactFile returns true if the filename looks like a Maven artifact.
func (h *MavenHandler) isArtifactFile(filename string) bool {
// Common artifact extensions
extensions := []string{".jar", ".war", ".ear", ".pom", ".aar", ".klib", ".module"}
2026-01-20 21:52:44 +00:00
for _, ext := range extensions {
if strings.HasSuffix(filename, ext) {
return true
}
}
return false
}
// isMetadataFile returns true if the filename is Maven metadata.
func (h *MavenHandler) isMetadataFile(filename string) bool {
return filename == "maven-metadata.xml" ||
strings.HasSuffix(filename, ".sha1") ||
strings.HasSuffix(filename, ".sha256") ||
strings.HasSuffix(filename, ".sha512") ||
strings.HasSuffix(filename, ".md5") ||
strings.HasSuffix(filename, ".asc")
}
// proxyUpstream forwards a request to Maven Central without caching.
func (h *MavenHandler) proxyUpstream(w http.ResponseWriter, r *http.Request) {
h.proxy.ProxyUpstream(w, r, h.upstreamURL+r.URL.Path, nil)
2026-01-20 21:52:44 +00:00
}