From 97b5c8d43ff7fdacc2e3d3bb4ded254db36801b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Mass=C3=A9?= Date: Mon, 15 Jan 2018 13:48:37 +0100 Subject: [PATCH] first version --- .gitignore | 1 + Dockerfile | 5 ++ README.md | 41 ++++++++++++ src/itix.fr/forward/main.go | 124 ++++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 src/itix.fr/forward/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b37f526 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +apicast-sidecar-proxy diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ce05422 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM scratch +COPY apicast-sidecar-proxy /apicast-sidecar-proxy +EXPOSE 9090 9091 +ENTRYPOINT [ "/apicast-sidecar-proxy" ] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..8aef7ab --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# A sidecar for Apicast, enabling proxy support + +## Why this project ? + +[Apicast](https://github.com/3scale/apicast) is an API gateway that fetches his +configuration and asks for authorization on the 3scale services (hosted in their +cloud). The gateway, however, is deployed on-premise in the customer network. + +More and more customers enforce the use of an HTTP proxy for outgoing connections. +Sometimes, an authentication is required to connect to the proxy. + +Currently, apicast does not fully support HTTP proxies. Hence this project provides +a sidecar container for Apicast that enables HTTP proxies support. + +## Deployment + +TODO + +## Development + +### Build +``` +GOOS=linux GOARCH=amd64 go build -o apicast-sidecar-proxy src/itix.fr/forward/main.go +``` + +### Package + +``` +VERSION=1.0 +git tag TODO +docker build -t apicast-sidecar-proxy:$VERSION . +``` + +### Pushing your image to DockerHub (Optional) + +``` +docker login https://index.docker.io/v1/ +docker images apicast-sidecar-proxy:$VERSION --format '{{ .ID }}' +docker tag $(docker images apicast-sidecar-proxy:$VERSION --format '{{ .ID }}') index.docker.io//apicast-sidecar-proxy:$VERSION +docker push index.docker.io//apicast-sidecar-proxy:$VERSION +``` diff --git a/src/itix.fr/forward/main.go b/src/itix.fr/forward/main.go new file mode 100644 index 0000000..767d9e7 --- /dev/null +++ b/src/itix.fr/forward/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strconv" +) + +// The MyResponseWriter is a wrapper around the standard http.ResponseWriter +// We need it to retrieve the http status code of an http response +type MyResponseWriter struct { + Underlying http.ResponseWriter + Status int +} + +func (mrw *MyResponseWriter) Header() http.Header { + return mrw.Underlying.Header() +} + +func (mrw *MyResponseWriter) Write(b []byte) (int, error) { + return mrw.Underlying.Write(b) +} + +func (mrw *MyResponseWriter) WriteHeader(s int) { + mrw.Status = s + mrw.Underlying.WriteHeader(s) +} + +func SetupReverseProxy(local_port int, target string) { + // Parse the target URL and perform some sanity checks + url, err := url.Parse(target) + if err != nil { + panic(err) + } + + // Initialize a Reverse Proxy object with a custom director + proxy := httputil.NewSingleHostReverseProxy(url) + underlying_director := proxy.Director + proxy.Director = func(req *http.Request) { + // Let the underlying director do the mandatory job + underlying_director(req) + + // Custom Handling + // --------------- + // + // Filter out the "Host" header sent by the client + // otherwise the target server won't be able to find the + // matching virtual host. The correct host header will be + // added automatically by the net/http package. + req.Host = "" + } + handler := http.NewServeMux() + handler.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) { + // Log the incoming request (including headers) + fmt.Printf("%v %v HTTP/1.1\n", req.Method, req.URL) + req.Header.Write(os.Stdout) + fmt.Println() + + // Wrap the standard response writer with our own + // implementation because we need the status code of the + // response and that field is not exported by default + mrw := &MyResponseWriter{Underlying: rw} + + // Let the reverse proxy handle the request + proxy.ServeHTTP(mrw, req) + + // Log the response + fmt.Printf("%v %v\n", mrw.Status, http.StatusText(mrw.Status)) + mrw.Header().Write(os.Stdout) + fmt.Println() + }) + + fmt.Printf("Listening on port %v for incoming requests...\n", local_port) + err = http.ListenAndServe(fmt.Sprintf(":%v", local_port), handler) + if (err != nil) { + fmt.Println("ERROR: %s", err) + } +} + +func main() { + portal_endpoint := os.Getenv("THREESCALE_PORTAL_ENDPOINT") + backend_endpoint := os.Getenv("BACKEND_ENDPOINT_OVERRIDE") + + portal_port_opt := os.Getenv("PORTAL_LISTEN_PORT") + if (portal_port_opt == "") { + portal_port_opt = "9090" + fmt.Println("WARNING: No PORTAL_LISTEN_PORT environment variable found, defaulting to '9090'...") + } + + backend_port_opt := os.Getenv("BACKEND_LISTEN_PORT") + if (backend_port_opt == "") { + backend_port_opt = "9091" + fmt.Println("WARNING: No BACKEND_LISTEN_PORT environment variable found, defaulting to '9091'...") + } + + error := false + if portal_endpoint == "" { + fmt.Println("ERROR: No THREESCALE_PORTAL_ENDPOINT environment variable found !") + error = true + } + if backend_endpoint == "" { + fmt.Println("ERROR: No BACKEND_ENDPOINT_OVERRIDE environment variable found !") + error = true + } + portal_port, err := strconv.Atoi(portal_port_opt) + if (err != nil) { + fmt.Printf("ERROR: Cannot parse the PORTAL_LISTEN_PORT environment variable (%s): %s\n", portal_port_opt, err) + error = true + } + backend_port, err := strconv.Atoi(backend_port_opt) + if (err != nil) { + fmt.Printf("ERROR: Cannot parse the BACKEND_LISTEN_PORT environment variable (%s): %s\n", backend_port_opt, err) + error = true + } + if error { + os.Exit(1) + } + + go SetupReverseProxy(backend_port, backend_endpoint) + SetupReverseProxy(portal_port, portal_endpoint) +}