Skip to content

Instantly share code, notes, and snippets.

@imjasonh
Last active April 11, 2022 20:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save imjasonh/4073fc2f41615e73a9be24ca74eb9fa8 to your computer and use it in GitHub Desktop.
Save imjasonh/4073fc2f41615e73a9be24ca74eb9fa8 to your computer and use it in GitHub Desktop.
registry proxy for distroless.dev/* -> ghcr.io/distroless/*
#!/usr/bin/env bash
set -euxo pipefail
gcloud run deploy redir \
--project=kontaindotme \
--region=us-central1 \
--allow-unauthenticated \
--image=$(KO_DOCKER_REPO=gcr.io/kontaindotme ko publish -P ./) \
--memory=128Mi \
--cpu=1 \
--concurrency=1000 \
--timeout=5 # seconds
module github.com/imjasonh/redir
go 1.17
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
)
// TODO: accept registry flag (example.dev/bar -> gcr.io/foo/bar)
// - GCR's token endpoint is /v2/token, which you find out by hitting /v2/ and parsing WWW-Authenticate header
// - Also support anonymous and Basic-type auth?
// Redirect requests for distroless.dev/static -> ghcr.io/distroless/static
// If repo is empty, example.dev/foo/bar -> ghcr.io/foo/bar
var repo = flag.String("repo", "distroless", "GHCR repo to redirect to")
func main() {
flag.Parse()
http.HandleFunc("/v2/", handler)
http.HandleFunc("/token", handler)
http.Handle("/new", http.RedirectHandler("https://github.com/"+*repo+"/template/generate", http.StatusSeeOther))
http.Handle("/", http.RedirectHandler("https://github.com/"+*repo, http.StatusSeeOther))
log.Println("Starting...")
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
log.Println("handler:", r.Method, r.URL)
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "registry is read-only", http.StatusBadRequest)
return
}
switch r.URL.Path {
case "/v2/":
proxyV2(w, r)
case "/token":
proxyToken(w, r)
default:
proxy(w, r)
}
return
}
func proxyV2(w http.ResponseWriter, r *http.Request) {
resp, err := http.Get("https://ghcr.io/v2/")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
for k, v := range resp.Header {
for _, vv := range v {
log.Println("<-- header", k, vv)
if k == "Www-Authenticate" {
vv = strings.Replace(vv, `realm="https://ghcr.io/`, fmt.Sprintf(`realm="https://%s/`, r.Host), 1)
log.Println("CHANGED:", k, vv)
}
w.Header().Add(k, vv)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func proxyToken(w http.ResponseWriter, r *http.Request) {
vals := r.URL.Query()
if *repo != "" {
scope := vals.Get("scope")
scope = strings.Replace(scope, "repository:", "repository:"+*repo+"/", 1)
vals.Set("scope", scope)
}
url := "https://ghcr.io/token?" + vals.Encode()
log.Println("proxyToken:", url)
resp, err := http.Get(url)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
for k, v := range resp.Header {
for _, vv := range v {
w.Header().Add(k, vv)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func proxy(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, "/")
url := "https://ghcr.io/v2/"
if *repo != "" {
url += *repo + "/"
}
url += strings.Join(parts[2:], "/")
log.Println("--> GET", url)
req, _ := http.NewRequest(r.Method, url, nil)
for k, v := range r.Header {
for _, vv := range v {
req.Header.Add(k, vv)
if k == "Authorization" {
vv = "REDACTED"
}
log.Println("--> header", k, vv)
}
}
// If the request is coming in without auth, get some auth.
// This is useful for testing, but should never happen in real life.
if req.Header.Get("Authorization") == "" {
t, err := getToken(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
req.Header.Set("Authorization", "Bearer "+t)
}
resp, err := http.DefaultTransport.RoundTrip(req) // Transport doesn't follow redirects.
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
log.Println("<--", resp.StatusCode)
for k, v := range resp.Header {
for _, vv := range v {
log.Println("<-- header", k, vv)
w.Header().Add(k, vv)
}
}
w.WriteHeader(resp.StatusCode)
if parts[len(parts)-2] != "blobs" {
io.Copy(w, resp.Body)
}
}
func getToken(r *http.Request) (string, error) {
parts := strings.Split(r.URL.Path, "/")
parts = parts[2 : len(parts)-2]
if *repo != "" {
parts = append([]string{*repo}, parts...)
}
url := fmt.Sprintf("https://ghcr.io/token?scope=repository:%s:pull&service=ghcr.io", strings.Join(parts, "/"))
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
all, _ := ioutil.ReadAll(resp.Body)
return "", fmt.Errorf("GET %s: %d %s", url, resp.StatusCode, string(all))
}
var t struct {
Token string `json:"token"`
}
if err := json.NewDecoder(resp.Body).Decode(&t); err != nil {
return "", err
}
return t.Token, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment