Skip to content

Commit

Permalink
Merge pull request #32 from dunglas/forwarded
Browse files Browse the repository at this point in the history
Add support for X-Forwarded-Proto and X-Forwarded-Host
  • Loading branch information
dunglas authored Nov 19, 2019
2 parents 351c089 + d477064 commit 2958ec4
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 52 deletions.
1 change: 0 additions & 1 deletion docs/gateway/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ The Vulcain Gateway Server follows [the twelve-factor app methodology](https://1
| `READ_TIMEOUT` | maximum duration for reading the entire request, including the body, set to `0s` to disable (default), example: `2m` |
| `SUBSCRIBER_JWT_KEY` | must contain the secret key to valid subscribers' JWT, can be omitted if `JWT_KEY` is set |
| `WRITE_TIMEOUT` | maximum duration before timing out writes of the response, set to `0s` to disable (default), example: `2m` |
| `USE_FORWARDED_HEADERS` | set to `1` to use the `X-Forwarded-For`, and `X-Real-IP` for the remote (client) IP address, `X-Forwarded-Proto` or `X-Forwarded-Scheme` for the scheme (http or https), `X-Forwarded-Host` for the host and the RFC 7239 `Forwarded` header, which may include both client IPs and schemes. If this option is enabled, the reverse proxy must override or remove these headers or you will be at risk. |

If `ACME_HOSTS` or both `CERT_FILE` and `KEY_FILE` are provided, an HTTPS server supporting HTTP/2 connection will be started.
If not, an HTTP server will be started (**not compatible with HTTP/2 Server Push, and not secure**).
3 changes: 3 additions & 0 deletions fixtures/api/jsonld.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func (h *JSONLDHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}

m := http.NewServeMux()
m.HandleFunc("/forwarded", func(rw http.ResponseWriter, req *http.Request) {
fmt.Fprint(rw, "X-Forwarded-Host: "+req.Header.Get("X-Forwarded-Host")+"\nX-Forwarded-Proto: "+req.Header.Get("X-Forwarded-Proto"))
})
m.HandleFunc("/books.jsonld", func(rw http.ResponseWriter, req *http.Request) {
fmt.Fprint(rw, BooksContent)
})
Expand Down
8 changes: 8 additions & 0 deletions gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ func (g *Gateway) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusBadGateway)
g.cleanupAfterRequest(pusher, explicitRequestID, explicitRequest, false)
}

proto := "https"
if req.TLS == nil {
proto = "http"
}

req.Header.Set("X-Forwarded-Proto", proto)
req.Header.Set("X-Forwarded-Host", req.Host)
rp.ServeHTTP(rw, req)
}

Expand Down
2 changes: 0 additions & 2 deletions gateway/json_pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,9 @@ func partsToTree(t Type, parts []string, root *node) {
switch t {
case Preload:
child.preload = true
break

case Fields:
child.fields = true
break
}

partsToTree(t, parts[1:], child)
Expand Down
26 changes: 12 additions & 14 deletions gateway/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@ import (

// Options stores the gateway's Options
type Options struct {
Debug bool
Addr string
Upstream *url.URL
MaxPushes int
AcmeHosts []string
AcmeCertDir string
CertFile string
KeyFile string
ReadTimeout time.Duration
WriteTimeout time.Duration
Compress bool
UseForwardedHeaders bool
OpenAPIFile string
Debug bool
Addr string
Upstream *url.URL
MaxPushes int
AcmeHosts []string
AcmeCertDir string
CertFile string
KeyFile string
ReadTimeout time.Duration
WriteTimeout time.Duration
Compress bool
OpenAPIFile string
}

// NewOptionsFromEnv creates a new option instance from environment
Expand Down Expand Up @@ -69,7 +68,6 @@ func NewOptionsFromEnv() (*Options, error) {
readTimeout,
writeTimeout,
os.Getenv("COMPRESS") != "0",
os.Getenv("USE_FORWARDED_HEADERS") == "1",
os.Getenv("OPENAPI_FILE"),
}

Expand Down
26 changes: 12 additions & 14 deletions gateway/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@ import (

func TestNewOptionsFormNew(t *testing.T) {
testEnv := map[string]string{
"UPSTREAM": "http://example.com",
"MAX_PUSHES": "-1",
"ACME_CERT_DIR": "/tmp",
"ACME_HOSTS": "example.com,example.org",
"ADDR": "127.0.0.1:8080",
"CERT_FILE": "foo",
"COMPRESS": "0",
"DEBUG": "1",
"KEY_FILE": "bar",
"READ_TIMEOUT": "1m",
"WRITE_TIMEOUT": "40s",
"USE_FORWARDED_HEADERS": "1",
"OPENAPI_FILE": "openapi.yaml",
"UPSTREAM": "http://example.com",
"MAX_PUSHES": "-1",
"ACME_CERT_DIR": "/tmp",
"ACME_HOSTS": "example.com,example.org",
"ADDR": "127.0.0.1:8080",
"CERT_FILE": "foo",
"COMPRESS": "0",
"DEBUG": "1",
"KEY_FILE": "bar",
"READ_TIMEOUT": "1m",
"WRITE_TIMEOUT": "40s",
"OPENAPI_FILE": "openapi.yaml",
}
for k, v := range testEnv {
os.Setenv(k, v)
Expand All @@ -44,7 +43,6 @@ func TestNewOptionsFormNew(t *testing.T) {
time.Minute,
40 * time.Second,
false,
true,
"openapi.yaml",
}, opts)
assert.Nil(t, err)
Expand Down
17 changes: 7 additions & 10 deletions gateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ func (g *Gateway) Serve() {
g.server.TLSConfig = certManager.TLSConfig()

// Mandatory for Let's Encrypt http-01 challenge
go http.ListenAndServe(":http", certManager.HTTPHandler(nil))
go func() {
if err := http.ListenAndServe(":http", certManager.HTTPHandler(nil)); err != nil {
log.Fatal(err)
}
}()
}

log.WithFields(log.Fields{"protocol": "https", "addr": g.options.Addr}).Info("Vulcain started")
Expand All @@ -68,18 +72,11 @@ func (g *Gateway) Serve() {

// chainHandlers configures and chains handlers
func (g *Gateway) chainHandlers() http.Handler {
var useForwardedHeadersHandlers http.Handler
if g.options.UseForwardedHeaders {
useForwardedHeadersHandlers = handlers.ProxyHeaders(g)
} else {
useForwardedHeadersHandlers = g
}

var compressHandler http.Handler
if g.options.Compress {
compressHandler = handlers.CompressHandler(useForwardedHeadersHandlers)
compressHandler = handlers.CompressHandler(g)
} else {
compressHandler = useForwardedHeadersHandlers
compressHandler = g
}

loggingHandler := handlers.CombinedLoggingHandler(os.Stderr, compressHandler)
Expand Down
26 changes: 21 additions & 5 deletions gateway/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func createTestingUtils(openAPIfile string) (*httptest.Server, *Gateway, http.Cl
return upstream, g, client
}

func TestH2NoPush(t *testing.T) {
func TestForwardedHeaders(t *testing.T) {
upstream, g, client := createTestingUtils("")
defer upstream.Close()

Expand All @@ -66,7 +66,23 @@ func TestH2NoPush(t *testing.T) {

assert.Equal(t, []string{"</books/1.jsonld?preload=%2Fauthor>; rel=preload; as=fetch", "</books/2.jsonld?preload=%2Fauthor>; rel=preload; as=fetch"}, resp.Header["Link"])
assert.Equal(t, `{"hydra:member":["/books/1.jsonld?preload=%2Fauthor","/books/2.jsonld?preload=%2Fauthor"]}`, string(b))
g.server.Shutdown(context.Background())
_ = g.server.Shutdown(context.Background())
}

func TestH2NoPush(t *testing.T) {
upstream, g, client := createTestingUtils("")
defer upstream.Close()

// loop until the gateway is ready
var resp *http.Response
for resp == nil {
resp, _ = client.Get(gatewayURL + "/forwarded")
}

b, _ := ioutil.ReadAll(resp.Body)

assert.Equal(t, "X-Forwarded-Host: 127.0.0.1:4343\nX-Forwarded-Proto: https", string(b))
_ = g.server.Shutdown(context.Background())
}

// Unfortunately, Go's HTTP client doesn't support Pushes yet (https://github.com/golang/go/issues/18594)
Expand All @@ -85,7 +101,7 @@ func TestH2Push(t *testing.T) {
}
}

g.server.Shutdown(context.Background())
_ = g.server.Shutdown(context.Background())
}

func TestH2PushLimit(t *testing.T) {
Expand All @@ -101,7 +117,7 @@ func TestH2PushLimit(t *testing.T) {
t.Log(string(stdoutStderr))
}

g.server.Shutdown(context.Background())
_ = g.server.Shutdown(context.Background())
}

func TestH2PushOpenAPI(t *testing.T) {
Expand All @@ -116,5 +132,5 @@ func TestH2PushOpenAPI(t *testing.T) {
t.Log(string(stdoutStderr))
}

g.server.Shutdown(context.Background())
_ = g.server.Shutdown(context.Background())
}
12 changes: 6 additions & 6 deletions spec/vulcain.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ server of the exact data it needs:

* `Preload` informs the server that relations of the main requested resource will be necessary.
The server can then reduce the number of round-trips by sending the related resources ahead
of time using HTTP/2 [!@RFC7540] Server Push. When using Server Push isn't possible (resources
of time using HTTP/2 [@!RFC7540] Server Push. When using Server Push isn't possible (resources
served by a different authority, server not supporting HTTP/2...), the server can hint the
client to fetch those resources as early as possible by using the `preload` link relation
[@W3C.CR-preload-20171026] and the `103` status code [!@RFC8297].
[@!W3C.CR-preload-20171026] and the `103` status code [@!RFC8297].

* `Fields` informs the server of the list of fields of the retrieved resources that will be used.
In order to improve performance and reduce bandwidth usage, the server can omit the fields not
Expand All @@ -58,7 +58,7 @@ references between resources.

The `Preload` HTTP header allows the client to ask the server to transmit resources linked to the
main resource it will need as soon as possible. To do so, the `Preload` header `MUST` contain a
selector (see #Selectors) referencing links to resources that `SHOULD` be preloaded.
selector [#selectors] referencing links to resources that `SHOULD` be preloaded.

The server `MUST` recursively follow links referenced by the selector. When a selector traverses
several resources, all the traversed resources `SHOULD` be sent to the client. If several links
Expand Down Expand Up @@ -144,7 +144,7 @@ Preload: /author

If it's not possible or beneficial to use HTTP/2 Server Push (reference to a resource not served by
the same authority, client or server not supporting HTTP/2, client having disabled Server Push...),
`preload` link relations [@W3C.CR-preload-20171026] `SHOULD` be used as a fallback.
`preload` link relations [@!W3C.CR-preload-20171026] `SHOULD` be used as a fallback.

The server `MUST NOT` add `preload` link relations if the related resources are pushed using HTTP/2
Server Push.
Expand Down Expand Up @@ -254,8 +254,8 @@ The following table defines the default selector format for common formats:
Format | Selector format | Identifier
--------|------------------------------------------------|----------------
JSON | Extended JSON Pointer (#extended-json-pointer) | `json-pointer`
XML | XPath [@W3C.REC-xpath-19991116] | `xpath`
HTML | CSS selectors [@W3C.REC-selectors-3-20181106] | `css`
XML | XPath [@!W3C.REC-xpath-19991116] | `xpath`
HTML | CSS selectors [@!W3C.REC-selectors-3-20181106] | `css`

The client and the server can negotiate the use of other selector formats using the `Prefer` HTTP
header.
Expand Down

0 comments on commit 2958ec4

Please sign in to comment.