I feel like I'm close to having this working but so far I"m running into an issue building a small reverse proxy in Go to a GCP Cloud Run instance. The request 'goes through' but the response from the request is the default GCP Cloud Run 404. It appears when making the request back to Cloud Run the Host header is being ignored and therefore the request is not being routed correction.
What might I be missing here?
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
const apiUrl = "MY_CLOUD_RUN.a.run.app"
func main() {
http.HandleFunc("/", proxy)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func proxy(res http.ResponseWriter, req *http.Request) {
// gets past CORS checks
if req.Method == http.MethodOptions {
headers := res.Header()
headers.Add("Access-Control-Allow-Origin", "*")
headers.Add("Vary", "Origin")
headers.Add("Vary", "Access-Control-Request-Method")
headers.Add("Vary", "Access-Control-Request-Headers")
headers.Add("Access-Control-Allow-Headers", "*")
headers.Add("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
res.WriteHeader(http.StatusOK)
return
}
p := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: apiUrl,
})
p.Director = func(req *http.Request) {
req.Header.Add("X-Forwarded-Host", req.Host)
req.Header.Add("X-Origin-Host", apiUrl)
req.Header.Add("Host", apiUrl)
req.Header.Add("Access-Control-Allow-Origin", "*")
req.URL.Scheme = "https"
req.URL.Host = apiUrl
}
p.ModifyResponse = func(res *http.Response) error {
res.Header.Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
res.Header.Set("Access-Control-Allow-Credentials", "true")
res.Header.Set("Access-Control-Allow-Origin", "*")
res.Header.Set("Access-Control-Allow-Headers", "*")
return nil
}
p.ServeHTTP(res, req)
}
This is a bit more elaborate than the original initial write-up but what we wound up with was as follows.
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"time"
"golang.org/x/oauth2"
"google.golang.org/api/idtoken"
)
var port = ":8080"
var backend = "[CLOUD_RUN_INSTANCE_TO_PROXY].a.run.app"
func main() {
logger := log.New(os.Stdout, "proxy: ", log.LstdFlags)
logger.Println(fmt.Sprintf("Proxy server is starting for: %s on port: %s", backend, port))
router := http.NewServeMux()
router.Handle("/", proxyHandler())
server := &http.Server{
Addr: port,
Handler: logging(logger)(router),
ErrorLog: logger,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 15 * time.Second,
}
done := make(chan bool)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
go func() {
<-quit
logger.Println("Proxy server is shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
}
close(done)
}()
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", port, err)
}
<-done
logger.Println("Server stopped")
}
func proxyHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
headers := w.Header()
headers.Add("Access-Control-Allow-Origin", "*")
headers.Add("Access-Control-Allow-Headers", "*")
headers.Add("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
w.WriteHeader(http.StatusOK)
return
}
path := fmt.Sprintf("https://%s%s", backend, r.RequestURI)
at, _ := idTokenTokenSource(path)
p := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "https",
Host: backend,
})
p.Director = func(r *http.Request) {
if at != nil {
at.SetAuthHeader(r)
}
}
p.ModifyResponse = func(res *http.Response) error {
res.Header.Set("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
res.Header.Set("Access-Control-Allow-Credentials", "true")
res.Header.Set("Access-Control-Allow-Origin", "*")
res.Header.Set("Access-Control-Allow-Headers", "*")
return nil
}
r.URL.Scheme = "https"
r.URL.Host = backend
r.Header.Set("X-Forwarded-Host", r.Header.Get("Host"))
r.Host = backend
if at != nil {
at.SetAuthHeader(r)
}
p.ServeHTTP(w, r)
})
}
func logging(l *log.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
requestId := r.Header.Get("X-Request-Id")
if requestId == "" {
requestId = fmt.Sprintf("%d", time.Now().UnixNano())
}
w.Header().Set("X-Request-Id", requestId)
l.Println(requestId, r.Method, r.URL.Path, r.RemoteAddr, r.UserAgent())
}()
next.ServeHTTP(w, r)
})
}
}
func idTokenTokenSource(audience string) (*oauth2.Token, error) {
ts, err := idtoken.NewTokenSource(context.Background(), audience)
if err != nil {
return nil, err
}
t, err := ts.Token()
if err != nil {
return nil, err
}
return t, nil
}
A good chunk of some of the graceful shutdown, http setup, and logging came from: https://gist.github.com/enricofoltran/10b4a980cd07cb02836f70a4ab3e72d7