Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make reverse proxy setup reusable as a library #6

Merged
merged 2 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 6 additions & 55 deletions proxy_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,22 @@ import (
"crypto/tls"
"flag"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"

"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"

"fortio.org/fortio/dflag"
"fortio.org/fortio/dflag/configmap"
"fortio.org/fortio/fhttp"
"fortio.org/fortio/log"
"fortio.org/fortio/version"
"fortio.org/proxy/config"
"fortio.org/proxy/rp"
)

func GetRoutes() []config.Route {
routes := configs.Get().(*[]config.Route)
return *routes
}

func setDestination(req *http.Request, url *url.URL) {
req.URL.Scheme = url.Scheme
req.URL.Host = url.Host
}

func Director(req *http.Request) {
routes := GetRoutes()
log.LogVf("Directing %+v", req)
for _, route := range routes {
log.LogVf("Evaluating req %q vs route %q and path %q vs prefix %q for dest %s",
req.Host, route.Host, req.URL.Path, route.Prefix, route.Destination.URL.String())
if route.MatchServerReq(req) {
fhttp.LogRequest(req, route.Destination.Str)
setDestination(req, &route.Destination.URL)
return
}
}
}

var (
configs = dflag.DynJSON(flag.CommandLine, "routes.json", &[]config.Route{}, "json list of `routes`")
email = dflag.DynString(flag.CommandLine, "email", "", "`Email` to attach to cert requests.")
certsFor = dflag.DynStringSet(flag.CommandLine, "certs-domains", []string{}, "Coma seperated list of `domains` to get certs for")
fullVersion = flag.Bool("version", false, "Show full version info and exit.")
Expand All @@ -64,7 +35,6 @@ var (
configDir = flag.String("config", "",
"Config directory `path` to watch for changes of dynamic flags (empty for no watch)")
httpPort = flag.String("http-port", "disabled", "`port` to listen on for non tls traffic (or 'disabled')")
h2Target = flag.Bool("h2", false, "Whether destinations support h2c prior knowledge")
acert *autocert.Manager
)

Expand All @@ -77,16 +47,6 @@ func hostPolicy(ctx context.Context, host string) error {
return fmt.Errorf("acme/autocert: %q not in allowed list", host)
}

func printRoutes() {
if !log.Log(log.Info) {
return
}
log.Printf("Initial Routes (routes.json dynamic flag):")
for _, r := range GetRoutes() {
log.Printf("host %q\t prefix %q\t -> %s", r.Host, r.Prefix, r.Destination.URL.String())
}
}

func debugGetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
log.LogVf("GetCert from %s for %q", hello.Conn.RemoteAddr().String(), hello.ServerName)
return acert.GetCertificate(hello)
Expand Down Expand Up @@ -124,31 +84,22 @@ func main() {
fhttp.RedirectToHTTPS(*redirect)
}

printRoutes()
rp := httputil.ReverseProxy{Director: Director}
revp := rp.ReverseProxy()

// TODO: make h2c vs regular client more dynamic based on route config instead of all or nothing
// (or maybe some day it will just ge the default behavior of the base http client)
if *h2Target {
rp.Transport = &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
}
s := &http.Server{
// TODO: make these timeouts configurable
ReadTimeout: 6 * time.Second,
WriteTimeout: 6 * time.Second,
IdleTimeout: 15 * time.Second,
ReadHeaderTimeout: 3 * time.Second,
// The reverse proxy
Handler: &rp,
Handler: revp,
}

if *httpPort != "disabled" {
fhttp.HTTPServerWithHandler("http-reverse-proxy", *httpPort, &rp)
fhttp.HTTPServerWithHandler("http-reverse-proxy", *httpPort, revp)
}

if *port == "disabled" {
log.Infof("No TLS server port.")
select {}
Expand Down
77 changes: 77 additions & 0 deletions rp/reverse_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package rp

import (
"crypto/tls"
"flag"
"net"
"net/http"
"net/http/httputil"
"net/url"

"fortio.org/fortio/dflag"
"fortio.org/fortio/fhttp"
"fortio.org/fortio/log"
"fortio.org/proxy/config"
"golang.org/x/net/http2"
)

var (
configs = dflag.DynJSON(flag.CommandLine, "routes.json", &[]config.Route{}, "json list of `routes`")
h2Target = flag.Bool("h2", false, "Whether destinations support h2c prior knowledge")
)

// GetRoutes gets the current routes from the dynamic flag routes.json as object (deserialized).
func GetRoutes() []config.Route {
routes := configs.Get().(*[]config.Route)
return *routes
}

func setDestination(req *http.Request, url *url.URL) {
req.URL.Scheme = url.Scheme
req.URL.Host = url.Host
}

// Director is the object used by the ReverseProxy to pick the route/destination.
func Director(req *http.Request) {
routes := GetRoutes()
log.LogVf("Directing %+v", req)
for _, route := range routes {
log.LogVf("Evaluating req %q vs route %q and path %q vs prefix %q for dest %s",
req.Host, route.Host, req.URL.Path, route.Prefix, route.Destination.URL.String())
if route.MatchServerReq(req) {
fhttp.LogRequest(req, route.Destination.Str)
setDestination(req, &route.Destination.URL)
return
}
}
}

// PrintRoutes prints the current value of the routes config (dflag).
func PrintRoutes() {
if !log.Log(log.Info) {
return
}
log.Printf("Initial Routes (routes.json dynamic flag):")
for _, r := range GetRoutes() {
log.Printf("host %q\t prefix %q\t -> %s", r.Host, r.Prefix, r.Destination.URL.String())
}
}

// ReverseProxy returns a new reverse proxy which will route based on the config/dflags.
func ReverseProxy() *httputil.ReverseProxy {
PrintRoutes()

revp := httputil.ReverseProxy{Director: Director}

// TODO: make h2c vs regular client more dynamic based on route config instead of all or nothing
// (or maybe some day it will just ge the default behavior of the base http client)
if *h2Target {
revp.Transport = &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
}
return &revp
}
1 change: 1 addition & 0 deletions sampleConfig/certs-domains
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
demo.fortio.org,grpc.fortio.org
1 change: 1 addition & 0 deletions sampleConfig/email
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fortio@fortio.org