2024-10-24 15:04:04 +02:00
|
|
|
package cacheproxy
|
|
|
|
|
|
|
|
import (
|
2024-10-24 16:24:28 +02:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
2024-10-24 15:04:04 +02:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
2024-10-25 17:15:43 +02:00
|
|
|
"net/http/httputil"
|
|
|
|
"net/url"
|
2024-10-24 15:04:04 +02:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
|
|
|
|
"github.com/nektos/act/pkg/common"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
urlBase = "/_apis/artifactcache"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Handler struct {
|
|
|
|
router *httprouter.Router
|
|
|
|
listener net.Listener
|
|
|
|
server *http.Server
|
|
|
|
logger logrus.FieldLogger
|
|
|
|
|
|
|
|
outboundIP string
|
|
|
|
|
|
|
|
cacheServerHost string
|
|
|
|
|
2024-10-25 17:15:43 +02:00
|
|
|
repositoryName string
|
|
|
|
repositorySecret string
|
2024-10-24 15:04:04 +02:00
|
|
|
}
|
|
|
|
|
2024-10-25 17:15:43 +02:00
|
|
|
func StartHandler(repoName string, targetHost string, outboundIP string, port uint16, cacheSecret string, logger logrus.FieldLogger) (*Handler, error) {
|
2024-10-24 15:04:04 +02:00
|
|
|
h := &Handler{}
|
|
|
|
|
|
|
|
if logger == nil {
|
|
|
|
discard := logrus.New()
|
|
|
|
discard.Out = io.Discard
|
|
|
|
logger = discard
|
|
|
|
}
|
|
|
|
logger = logger.WithField("module", "artifactcache")
|
|
|
|
h.logger = logger
|
|
|
|
|
|
|
|
h.repositoryName = repoName
|
2024-10-25 17:15:43 +02:00
|
|
|
repoSecret, err := calculateMAC(repoName, cacheSecret)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to decode cacheSecret")
|
|
|
|
}
|
|
|
|
h.repositorySecret = repoSecret
|
2024-10-24 15:04:04 +02:00
|
|
|
|
|
|
|
if outboundIP != "" {
|
|
|
|
h.outboundIP = outboundIP
|
|
|
|
} else if ip := common.GetOutboundIP(); ip == nil {
|
|
|
|
return nil, fmt.Errorf("unable to determine outbound IP address")
|
|
|
|
} else {
|
|
|
|
h.outboundIP = ip.String()
|
|
|
|
}
|
|
|
|
|
2024-10-25 17:15:43 +02:00
|
|
|
h.cacheServerHost = targetHost
|
|
|
|
|
|
|
|
proxy, err := h.newReverseProxy(targetHost)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to set up proxy to target host")
|
|
|
|
}
|
|
|
|
|
2024-10-24 15:04:04 +02:00
|
|
|
router := httprouter.New()
|
2024-11-09 16:56:20 +01:00
|
|
|
router.HandlerFunc("GET", urlBase+"/cache", proxyRequestHandler(proxy))
|
|
|
|
router.HandlerFunc("POST", urlBase+"/caches", proxyRequestHandler(proxy))
|
|
|
|
router.HandlerFunc("PATCH", urlBase+"/caches/:id", proxyRequestHandler(proxy))
|
|
|
|
router.HandlerFunc("POST", urlBase+"/caches/:id", proxyRequestHandler(proxy))
|
|
|
|
router.HandlerFunc("GET", urlBase+"/artifacts/:id", proxyRequestHandler(proxy))
|
|
|
|
router.HandlerFunc("POST", urlBase+"/clean", proxyRequestHandler(proxy))
|
2024-10-24 15:04:04 +02:00
|
|
|
|
|
|
|
h.router = router
|
|
|
|
|
|
|
|
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
server := &http.Server{
|
|
|
|
ReadHeaderTimeout: 2 * time.Second,
|
|
|
|
Handler: router,
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
if err := server.Serve(listener); err != nil && errors.Is(err, net.ErrClosed) {
|
|
|
|
logger.Errorf("http serve: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
h.listener = listener
|
|
|
|
h.server = server
|
|
|
|
|
|
|
|
return h, nil
|
|
|
|
}
|
|
|
|
|
2024-10-25 17:15:43 +02:00
|
|
|
func proxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
proxy.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) newReverseProxy(targetHost string) (*httputil.ReverseProxy, error) {
|
|
|
|
url, err := url.Parse(targetHost)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-11-09 16:56:20 +01:00
|
|
|
proxy := &httputil.ReverseProxy{
|
|
|
|
Rewrite: func(r *httputil.ProxyRequest) {
|
|
|
|
r.SetURL(url)
|
|
|
|
r.Out.Host = r.In.Host // if desired
|
|
|
|
h.injectAuth(r)
|
|
|
|
},
|
|
|
|
}
|
2024-10-25 17:15:43 +02:00
|
|
|
return proxy, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) injectAuth(r *httputil.ProxyRequest) {
|
|
|
|
r.Out.SetBasicAuth(h.repositoryName, h.repositorySecret)
|
|
|
|
}
|
|
|
|
|
2024-10-24 15:04:04 +02:00
|
|
|
func (h *Handler) ExternalURL() string {
|
|
|
|
// TODO: make the external url configurable if necessary
|
|
|
|
return fmt.Sprintf("http://%s:%d",
|
|
|
|
h.outboundIP,
|
|
|
|
h.listener.Addr().(*net.TCPAddr).Port)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *Handler) Close() error {
|
|
|
|
if h == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
var retErr error
|
|
|
|
if h.server != nil {
|
|
|
|
err := h.server.Close()
|
|
|
|
if err != nil {
|
|
|
|
retErr = err
|
|
|
|
}
|
|
|
|
h.server = nil
|
|
|
|
}
|
|
|
|
if h.listener != nil {
|
|
|
|
err := h.listener.Close()
|
|
|
|
if errors.Is(err, net.ErrClosed) {
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
retErr = err
|
|
|
|
}
|
|
|
|
h.listener = nil
|
|
|
|
}
|
|
|
|
return retErr
|
|
|
|
}
|
|
|
|
|
2024-10-25 17:15:43 +02:00
|
|
|
func calculateMAC(repoName string, cacheSecret string) (string, error) {
|
|
|
|
sec, err := hex.DecodeString(cacheSecret)
|
2024-10-24 15:04:04 +02:00
|
|
|
if err != nil {
|
2024-10-25 17:15:43 +02:00
|
|
|
return "", err
|
2024-10-24 15:04:04 +02:00
|
|
|
}
|
2024-10-25 17:15:43 +02:00
|
|
|
mac := hmac.New(sha256.New, sec)
|
|
|
|
mac.Write([]byte(repoName))
|
|
|
|
macBytes := mac.Sum(nil)
|
|
|
|
return hex.EncodeToString(macBytes), nil
|
2024-10-24 15:04:04 +02:00
|
|
|
}
|