-
-
Save tqbf/ebd504a625813e6b8c5913fc28cc9515 to your computer and use it in GitHub Desktop.
I would be astonished if this code actually worked.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING | |
// | |
// I have made no effort to see if this code, which is extracted | |
// and drastically simplified from our production code, actually | |
// works. It's here for illustrative purposes only. | |
import ( | |
"bytes" | |
"context" | |
"crypto/hmac" | |
"crypto/sha256" | |
"encoding/base64" | |
"encoding/json" | |
"errors" | |
"fmt" | |
"net/http" | |
"net/url" | |
"os" | |
"strings" | |
"testing" | |
"time" | |
"github.com/docker/distribution/configuration" | |
"github.com/docker/distribution/registry/handlers" | |
"github.com/gorilla/mux" | |
_ "github.com/docker/distribution/registry/storage/driver/filesystem" | |
) | |
type contextKey string | |
var ( | |
ErrMalformedInput = errors.New("malformed input") | |
contextKeyOrgId = contextKey("org-id") | |
// Here's a database: | |
Tokens = map[string]int{ | |
"YELLOW": 1, | |
"SUBMARINE": 2, | |
} | |
Repositories = map[int]map[string]string{ | |
1: map[string]string{ | |
"rails": "1686ec5f68100acd3ed7c62cd96caeaef3b609f9", | |
"nginx": "487ea25fe00ee01fce93f533c9b0ee7eb5a001fa", | |
}, | |
2: map[string]string{ | |
"django": "992df07b55dc16bf63b74e1425fa6b8bd20c5cbb", | |
"haproxy": "139a516f925d7cfa15917e3f9f26c7ef80d92108", | |
}, | |
} | |
) | |
// rewrite a repository name, or return "" if there's no | |
// matching repo | |
func xlat(org int, name string) string { | |
repos, ok := Repositories[org] | |
if !ok { | |
return "" | |
} | |
iname, ok := repos[name] | |
if !ok { | |
return "" | |
} | |
return iname | |
} | |
// NewRegistry returns a listen-n-servable HTTP handler that exposes | |
// a Docker registry | |
func NewRegistry(ctx context.Context, rootpath, secret string) http.Handler { | |
cfg := &configuration.Configuration{ | |
Storage: configuration.Storage{ | |
// we don't use filesystem storage, so I have no idea if this | |
// is right. | |
"filesystem": configuration.Parameters{ | |
"rootdirectory": rootpath, | |
}, | |
"delete": configuration.Parameters{ | |
"enabled": true, | |
}, | |
}, | |
} | |
// the secret is configured explicitly so we can share it with our | |
// middleware | |
cfg.HTTP.Secret = secret | |
// that was easy | |
registry := handlers.NewApp(ctx, cfg) | |
// this code exposes the V2 Registry API, wrapped with our middleware | |
routes := mux.NewRouter() | |
v2Routes := routes.PathPrefix("/v2/").Subrouter() | |
v2Routes.Use(authN) | |
v2Routes.Handle("/", registry) | |
repoRoute := v2Routes.PathPrefix("/{appName:[a-z0-9-]+}/").Subrouter() | |
repoRoute.Use(authZ(secret)) | |
repoRoute.PathPrefix("/blobs/").Methods("POST", "HEAD", "PUT", "PATCH", "DELETE", "GET").Handler(registry) | |
repoRoute.PathPrefix("/manifests/").Handler(registry) | |
return routes | |
} | |
// allow only requests with valid tokens; extract the organization | |
// from the token and pass it down the chain in the context | |
func authN(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | |
auth := req.Header.Get("Authorization") | |
token := strings.TrimPrefix(auth, "Bearer ") | |
if len(token) == len(auth) { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"not allowed"}]}`, 401) | |
return | |
} | |
org, ok := Tokens[token] | |
if !ok { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"not allowed"}]}`, 401) | |
return | |
} | |
next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), contextKeyOrgId, org))) | |
}) | |
} | |
// _state is a base64'd, HMAC-tagged JSON blob that contains references | |
// to Docker repositories; rewrite them. I rewrote this function from | |
// our actual source code to generate this example and it is unlikely | |
// to work, but you get the gist. | |
func rewriteState(secret, name string, q url.Values) error { | |
data := q.Get("_state") | |
if data == "" { | |
return nil | |
} | |
token, err := base64.URLEncoding.DecodeString(data) | |
if err != nil { | |
return fmt.Errorf("state token: base64: %w: %s", ErrMalformedInput, err) | |
} | |
mac := hmac.New(sha256.New, []byte(secret)) | |
if len(token) < mac.Size() { | |
return fmt.Errorf("state token: %w: short token", ErrMalformedInput) | |
} | |
tag, message := token[:mac.Size()], token[mac.Size():] | |
mac.Write(message) | |
if !hmac.Equal(mac.Sum(nil), tag) { | |
return fmt.Errorf("state token: %w: mac failed", ErrMalformedInput) | |
} | |
type State struct { | |
Name string | |
UUID string | |
Offset int64 | |
StartedAt time.Time | |
} | |
state := &State{} | |
if err = json.NewDecoder(bytes.NewReader(message)).Decode(state); err != nil { | |
return fmt.Errorf("state token: json: %w: %s", ErrMalformedInput, err) | |
} | |
state.Name = name | |
buf := &bytes.Buffer{} | |
if err = json.NewEncoder(buf).Encode(state); err != nil { | |
return fmt.Errorf("state token: json encode: %w: %s", ErrMalformedInput, err) | |
} | |
message = buf.Bytes() | |
buf.Reset() | |
b64 := base64.NewEncoder(base64.URLEncoding, buf) | |
mac = hmac.New(sha256.New, []byte(secret)) | |
mac.Write(message) | |
b64.Write(mac.Sum(nil)) | |
b64.Write(message) | |
b64.Close() | |
q.Set("_state", buf.String()) | |
return nil | |
} | |
type responseRewriter struct { | |
f func(headers http.Header) | |
http.ResponseWriter | |
} | |
func (r *responseRewriter) WriteHeader(c int) { | |
r.f(r.ResponseWriter.Header()) | |
r.ResponseWriter.WriteHeader(c) | |
} | |
// the authorizer does all the lifting. | |
func authZ(secret string) func(http.Handler) http.Handler { | |
return func(next http.Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { | |
// require authenticated requests | |
org, ok := req.Context().Value(contextKeyOrgId).(int) | |
if !ok { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"not allowed"}]}`, 401) | |
return | |
} | |
// pull out the repository name | |
parts := strings.SplitN(req.URL.Path, "/", 4) | |
if len(parts) < 4 || parts[0] != "v2" { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"malformed request"}]}`, 500) | |
return | |
} | |
var ( | |
nameOrig string | |
nameNew string | |
fromOrig string | |
fromNew string | |
) | |
// translate public to internal repo names based on authorization | |
nameOrig = parts[2] | |
nameNew = xlat(org, nameOrig) | |
if nameNew == "" { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"not allowed"}]}`, 401) | |
return | |
} | |
q := req.URL.Query() | |
// catch cross-repo mounts and do the same rewrite | |
if req.Method == http.MethodPost { | |
fromOrig = q.Get("from") | |
if fromOrig != "" && q.Get("mount") != "" { | |
fromNew = xlat(org, fromOrig) | |
if fromNew == "" { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"not allowed"}]}`, 401) | |
return | |
} | |
} | |
} | |
// helper to do the URL and _state rewriting | |
fixLocation := func(loc *url.URL, name, from string) error { | |
parts[2] = name | |
loc.Path = strings.Join(parts, "/") | |
q := loc.Query() | |
if err := rewriteState(secret, name, q); err != nil { | |
return err | |
} | |
if from != "" { | |
q.Set("from", from) | |
} | |
loc.RawQuery = q.Encode() | |
return nil | |
} | |
if err := fixLocation(req.URL, nameNew, fromNew); err != nil { | |
http.Error(w, `{"errors":[{"code":"UNAUTHORIZED", "message":"not allowed"}]}`, 401) | |
return | |
} | |
// serve the request with the ResponseWriter wrapped to | |
// undo all the rewriting we did on the ingress side. | |
next.ServeHTTP(&responseRewriter{ | |
f: func(h http.Header) { | |
if loc := h.Get("Location"); loc != "" { | |
lurl, err := url.Parse(loc) | |
if err != nil { | |
return | |
} | |
fixLocation(lurl, nameOrig, "") | |
h.Set("Location", lurl.String()) | |
} | |
}, | |
ResponseWriter: w, | |
}, req) | |
}) | |
} | |
} | |
func TestServer(t *testing.T) { | |
ctx := context.Background() | |
os.Mkdir("/tmp/registry", 0755) | |
reg := NewRegistry(ctx, "/tmp/registry", "foot") | |
err := http.ListenAndServe(":5050", reg) | |
panic(err.Error()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment