Skip to content

Commit

Permalink
Merge pull request #75 from m-lab/sandbox-soltesz-storage
Browse files Browse the repository at this point in the history
Add support for a storage reverse proxy to the epoxy server
  • Loading branch information
stephen-soltesz committed Feb 20, 2019
2 parents 0e79eda + 0f89af2 commit 68fc618
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 9 deletions.
9 changes: 9 additions & 0 deletions cmd/epoxy_boot_server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ var (
// serverCert and serverKey are the filenames for the iPXE server certificate.
serverCert = os.Getenv("IPXE_CERT_FILE")
serverKey = os.Getenv("IPXE_KEY_FILE")

// storagePrefixURL is the prefix URL for storage proxy requests. If empty, the
// storage proxy is disabled.
storagePrefixURL = os.Getenv("STORAGE_PREFIX_URL")
)

const (
Expand Down Expand Up @@ -159,6 +163,9 @@ func newRouter(env *handler.Env) *mux.Router {
addRoute(router, "POST", "/v1/boot/{hostname}/{sessionID}/extension/{operation}",
http.HandlerFunc(env.HandleExtension))

// Add proxy for accessing storage, such as GCS.
addRoute(router, "GET", "/v1/storage/{path:.*}",
http.HandlerFunc(env.HandleStorageProxy))
return router
}

Expand Down Expand Up @@ -264,6 +271,8 @@ func main() {
Config: dsCfg,
ServerAddr: publicHostname,
AllowForwardedRequests: allowForwardedRequests,
Project: projectID,
StoragePrefixURL: storagePrefixURL,
}

startMetricsServerAsync(dsCfg)
Expand Down
51 changes: 51 additions & 0 deletions handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/m-lab/epoxy/metrics"
"github.com/m-lab/epoxy/storage"
"github.com/m-lab/epoxy/template"
"github.com/m-lab/go/rtx"
)

// Config provides access to Host records.
Expand All @@ -55,6 +56,10 @@ type Env struct {
// then the ePoxy server substitutes the value in the "X-Forwarded-For" request
// header for the request "remote address".
AllowForwardedRequests bool
// Project is the GCP project name in which the server is running.
Project string
// StoragePrefixURL is the target URL prefix for storage proxy requests.
StoragePrefixURL string
}

var (
Expand Down Expand Up @@ -367,3 +372,49 @@ func (env *Env) HandleExtension(rw http.ResponseWriter, req *http.Request) {
srv := newReverseProxy(extURL, webreq.Encode())
srv.ServeHTTP(rw, req)
}

// newStorageReverseProxy creates an httputil.ReverseProxy that forwards requests
// to the given target URL prefix. Client request paths are concatenated onto the
// target prefix URL path.
func newStorageReverseProxy(storagePrefixURL string) *httputil.ReverseProxy {
target, err := url.Parse(storagePrefixURL)
rtx.Must(err, "Failed to parse static GCS URL")

director := func(req *http.Request) {
req.Host = target.Host
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path + req.URL.Path // Unconditionally concatenate paths.
req.URL.RawQuery = "" // Reject any given query parameters.

if _, ok := req.Header["User-Agent"]; !ok {
// User did not provide User-Agent, so explicitly disable it so our request
// does not set it to the default value.
req.Header.Set("User-Agent", "")
}
log.Println(req.RemoteAddr, req.Method, req.Host, req.Header, req.RequestURI)
log.Println("StorageProxy request:", req.URL)
}
return &httputil.ReverseProxy{Director: director}
}

// HandleStorageProxy creates a pass-through proxy for GET requests
// by concatenating the request "path" to the environment's StoragePrefixURL.
func (env *Env) HandleStorageProxy(rw http.ResponseWriter, req *http.Request) {
if env.StoragePrefixURL == "" {
// When no storage prefix url is given, then signal that this is unsupported.
http.Error(rw, "StoragePrefixURL is not specified", http.StatusNotImplemented)
return
}
if req.Method != http.MethodGet {
http.Error(rw, "Wrong HTTP method", http.StatusMethodNotAllowed)
return
}

// Gorilla strips the "/" prefix from paths, so add it back.
path := mux.Vars(req)["path"]
req.URL.Path = "/" + path

srv := newStorageReverseProxy(env.StoragePrefixURL)
srv.ServeHTTP(rw, req)
}
119 changes: 110 additions & 9 deletions handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ func TestGenerateStage1IPXE(t *testing.T) {
Stage1ChainURL: "https://storage.googleapis.com/epoxy-boot-server/stage1to2/stage1to2.ipxe",
},
}
env := &Env{fakeConfig{host: h}, "example.com:4321", true}
env := &Env{
Config: fakeConfig{host: h},
ServerAddr: "example.com:4321",
AllowForwardedRequests: true,
}
router := mux.NewRouter()
router.Methods("POST").
Path("/v1/boot/{hostname}/stage1.ipxe").
Expand Down Expand Up @@ -196,8 +200,11 @@ func TestEnv_GenerateStage1IPXE(t *testing.T) {
req := httptest.NewRequest("POST", "/v1/boot/"+h.Name+"/stage1.ipxe", nil)
req.Header.Set("X-Forwarded-For", tt.from)
rec := httptest.NewRecorder()

env := &Env{tt.config, "example.com:4321", true}
env := &Env{
Config: tt.config,
ServerAddr: "example.com:4321",
AllowForwardedRequests: true,
}
req = mux.SetURLVars(req, vars)
env.GenerateStage1IPXE(rec, req)

Expand Down Expand Up @@ -256,7 +263,11 @@ func TestEnv_GenerateJSONConfig(t *testing.T) {
req.Header.Set("X-Forwarded-For", tt.from)
rec := httptest.NewRecorder()

env := &Env{tt.config, "server.com:4321", true}
env := &Env{
Config: tt.config,
ServerAddr: "example.com:4321",
AllowForwardedRequests: true,
}
req = mux.SetURLVars(req, vars)
env.GenerateJSONConfig(rec, req)

Expand Down Expand Up @@ -334,8 +345,11 @@ func TestEnv_ReceiveReport(t *testing.T) {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("X-Forwarded-For", tt.from)
rec := httptest.NewRecorder()

env := &Env{fakeConfig{host: h, failOnLoad: false, failOnSave: false}, "server.com:4321", true}
env := &Env{
Config: fakeConfig{host: h, failOnLoad: false, failOnSave: false},
ServerAddr: "example.com:4321",
AllowForwardedRequests: true,
}
req = mux.SetURLVars(req, vars)
env.ReceiveReport(rec, req)

Expand Down Expand Up @@ -451,7 +465,11 @@ func TestEnv_HandleExtension(t *testing.T) {
req := httptest.NewRequest("POST", extURL, nil)
req.Header.Set("X-Forwarded-For", tt.from)
rec := httptest.NewRecorder()
env := &Env{fakeConfig{host: h, failOnLoad: tt.failOnLoad}, "server.com:4321", true}
env := &Env{
Config: fakeConfig{host: h, failOnLoad: tt.failOnLoad},
ServerAddr: "example.com:4321",
AllowForwardedRequests: true,
}
req = mux.SetURLVars(req, vars)
// Setup a fake extension server to handle the Request.
ts := httptest.NewServer(
Expand Down Expand Up @@ -547,8 +565,11 @@ func TestEnv_GenerateStage1JSON(t *testing.T) {
req := httptest.NewRequest("POST", "/v1/boot/"+h.Name+"/stage1.ipxe", nil)
req.Header.Set("X-Forwarded-For", tt.from)
rec := httptest.NewRecorder()

env := &Env{tt.config, "example.com:4321", true}
env := &Env{
Config: tt.config,
ServerAddr: "example.com:4321",
AllowForwardedRequests: true,
}
req = mux.SetURLVars(req, vars)
env.GenerateStage1JSON(rec, req)

Expand All @@ -558,3 +579,83 @@ func TestEnv_GenerateStage1JSON(t *testing.T) {
})
}
}

func TestEnv_HandleStorageProxy(t *testing.T) {
tests := []struct {
name string
storagePath string
method string
expectedStatus int
expectedResult string
disableStorage bool
}{
{
name: "success",
storagePath: "/stage1/vmlinuz",
method: "GET",
expectedStatus: http.StatusOK,
expectedResult: "ok",
},
{
name: "failure-not-implemented",
storagePath: "/stage1/vmlinuz",
method: "GET",
expectedStatus: http.StatusNotImplemented,
disableStorage: true,
},
{
name: "failure-method-not-allowed",
storagePath: "/stage1/vmlinuz",
method: "POST",
expectedStatus: http.StatusMethodNotAllowed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

// Setup a fake storage server to handle the proxy request.
ts := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != tt.storagePath {
t.Errorf("Requeted and expected path does not match; got %q, want %q",
r.URL.Path, tt.storagePath)
}
w.WriteHeader(tt.expectedStatus)
w.Write([]byte(tt.expectedResult))
}))
defer ts.Close()

// Create a synthetic request with an original request URL to example.com.
req := httptest.NewRequest(
tt.method, "https://epoxy-boot-api.mlab-sandbox.mlab.net/v1/storage"+tt.storagePath, nil)
rec := httptest.NewRecorder()

// These variables are normally created by the gorilla request handler.
// An original request to:
// https://epoxy-boot-api.mlab-sandbox.mlab.net/v1/storage/stage1/vmlinuz
// would extract the "path" after /v1/storage, i.e. "stage1/vmlinuz"
vars := map[string]string{"path": tt.storagePath[1:]}
req = mux.SetURLVars(req, vars)

// The env configuration is normally created by the main server.
// StoragePrefixURL is the only setting needed by HandleStorageProxy.
env := &Env{}
if !tt.disableStorage {
// Direct the proxy at our test server.
env.StoragePrefixURL = ts.URL
}

env.HandleStorageProxy(rec, req)

if tt.expectedStatus != rec.Code {
t.Errorf("HandleStorageProxy() wrong HTTP status: got %v; want %v",
rec.Code, tt.expectedStatus)
}
// Verify that the request and response actually came from the test server.
if tt.expectedResult != "" && tt.expectedResult != rec.Body.String() {
t.Errorf("HandleStorageProxy() wrong result forwarded: got %v\n; want %v\n",
rec.Body.String(), tt.expectedResult)
}
})
}
}

0 comments on commit 68fc618

Please sign in to comment.