/* Copyright 2019 Google Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Command runlocal launches a reverse proxy that can be used to // locally test changes to the code in the agent or server packages // // Example usage: // go build -o ~/bin/inverting-proxy-run-local testing/runlocal/main.go // ~/bin/inverting-proxy-run-local --port 8081 package main import ( "bytes" "context" "encoding/json" "flag" "fmt" "html/template" "io" "io/ioutil" "log" "net/http" "net/http/httptest" "net/url" "os" "os/exec" "os/signal" "path/filepath" "strconv" "strings" "time" "github.com/google/uuid" ) const responseTemplate = ` Proxied response from {{.Path}} Received a request to {{.Path}} with backend cookie {{.BackendCookie}} ` var ( port = flag.Int("port", 0, "Port on which to listen") respTmpl = template.Must(template.New("response").Parse(responseTemplate)) ) // RunLocalProxy runs a proxy locally func RunLocalProxy(ctx context.Context) (int, error) { // This assumes that "Make build" has been run proxyArgs := fmt.Sprintf("${GOPATH}/bin/inverting-proxy --port=%d", *port) proxyCmd := exec.CommandContext(ctx, "/bin/bash", "-c", proxyArgs) var proxyOut bytes.Buffer proxyCmd.Stdout = &proxyOut proxyCmd.Stderr = &proxyOut if err := proxyCmd.Start(); err != nil { log.Fatalf("Failed to start the inverting-proxy binary: %v", err) } go func() { err := proxyCmd.Wait() log.Printf("Proxy result: %v, stdout/stderr: %q", err, proxyOut.String()) }() for i := 0; i < 30; i++ { for _, line := range strings.Split(proxyOut.String(), "\n") { if strings.Contains(line, "Listening on [::]:") { portStr := strings.TrimSpace(strings.Split(line, "Listening on [::]:")[1]) return strconv.Atoi(portStr) } } log.Printf("Waiting for the locally running proxy to start...") time.Sleep(1 * time.Second) } return 0, fmt.Errorf("Locally-running proxy failed to start up in time: %q", proxyOut.String()) } func main() { flag.Parse() ctx, cancel := context.WithCancel(context.Background()) defer cancel() backendHomeDir, err := ioutil.TempDir("", "backend-home") if err != nil { log.Fatalf("Failed to set up a temporary home directory for the test: %v", err) } gcloudCfg := filepath.Join(backendHomeDir, ".config", "gcloud") if err := os.MkdirAll(gcloudCfg, os.ModePerm); err != nil { log.Fatalf("Failed to set up a temporary home directory for the test: %v", err) } fakeMetadata := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Emulate slow responses from the metadata server, to check that the agent // is appropriately caching the results. time.Sleep(50 * time.Millisecond) if strings.HasPrefix(r.URL.Path, "/computeMetadata/v1/project/project-id") { io.WriteString(w, "12345") return } if !(strings.HasPrefix(r.URL.Path, "/computeMetadata/v1/instance/service-accounts/") && strings.HasSuffix(r.URL.Path, "/token")) { io.WriteString(w, "ok") return } var fakeToken struct { AccessToken string `json:"access_token"` ExpiresInSec int `json:"expires_in"` TokenType string `json:"token_type"` } fakeToken.AccessToken = "fakeToken" fakeToken.ExpiresInSec = 1000 fakeToken.TokenType = "Bearer" if err := json.NewEncoder(w).Encode(&fakeToken); err != nil { log.Printf("Failed to encode a fake service account credential: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } })) backendCookieName := "BackendCookie" backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("Responding to backend request to %q", r.URL.Path) bc, err := r.Cookie(backendCookieName) if err == http.ErrNoCookie || bc == nil { backendCookieVal := uuid.New().String() bc = &http.Cookie{ Name: backendCookieName, Value: backendCookieVal, HttpOnly: true, } http.SetCookie(w, bc) http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) return } var templateBuf bytes.Buffer templateVals := &struct { Path string BackendCookie string }{ Path: r.URL.Path, BackendCookie: bc.Value, } if err := respTmpl.Execute(&templateBuf, templateVals); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Add("Content-Type", "text/html") w.Write(templateBuf.Bytes()) })) go func() { <-ctx.Done() backend.Close() fakeMetadata.Close() }() backendURL, err := url.Parse(backend.URL) if err != nil { log.Fatalf("Failed to parse the backend URL: %v", err) } proxyPort, err := RunLocalProxy(ctx) proxyURL := fmt.Sprintf("http://localhost:%d", proxyPort) if err != nil { log.Fatalf("Failed to run the local inverting proxy: %v", err) } log.Printf("Started backend at localhost:%s and proxy at %s", backendURL.Port(), proxyURL) // This assumes that "Make build" has been run args := strings.Join(append( []string{"${GOPATH}/bin/proxy-forwarding-agent"}, "--debug=true", "--disable-ssl-for-test=true", "--session-cookie-name=SessionID", "--backend=testBackend", "--proxy", proxyURL+"/", "--host=localhost:"+backendURL.Port(), "--inject-banner=\\Inverting\\ Proxy\\"), " ") agentCmd := exec.CommandContext(ctx, "/bin/bash", "-c", args) agentCmd.Stdout = os.Stdout agentCmd.Stderr = os.Stderr agentCmd.Env = append(os.Environ(), "PATH=", "HOME="+backendHomeDir, "GCE_METADATA_HOST="+strings.TrimPrefix(fakeMetadata.URL, "http://")) if err := agentCmd.Start(); err != nil { log.Fatalf("Failed to start the agent binary: %v", err) } defer func() { cancel() err := agentCmd.Wait() log.Printf("Agent result: %v", err) }() sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt) <-sigChan }