Skip to content

Instantly share code, notes, and snippets.

@tqbf
Created April 8, 2021 22:21
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save tqbf/ebd504a625813e6b8c5913fc28cc9515 to your computer and use it in GitHub Desktop.
Save tqbf/ebd504a625813e6b8c5913fc28cc9515 to your computer and use it in GitHub Desktop.
I would be astonished if this code actually worked.
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