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

Support user configurable header forwarding & context metadata #336

Merged
merged 1 commit into from
Apr 18, 2017

Conversation

tamalsaha
Copy link
Collaborator

@tamalsaha tamalsaha commented Mar 23, 2017

Fixes #311

@tmc / @yugui , if the general idea looks ok to you, I will add tests for this.

cc:@sadlil

@tamalsaha tamalsaha force-pushed the header-pass branch 3 times, most recently from f41c8ca to 15d0787 Compare March 23, 2017 18:21
@tmc
Copy link
Collaborator

tmc commented Mar 23, 2017

@tamalsaha thanks for your contribution.

A couple notes:

  1. Mutating a global to modify behavior should be avoided, I'd prefer passing some sort of Option.
  2. It's not clear to me that we should supply a set of Matcher implementations.

@tamalsaha
Copy link
Collaborator Author

tamalsaha commented Mar 23, 2017

Thanks @tmc .

Mutating a global to modify behavior should be avoided, I'd prefer passing some sort of Option.

Do you have any suggestion about that? The only passed thing I can see if the Mux

func Register{{$svc.GetName}}Handler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {

It's not clear to me that we should supply a set of Matcher implementations.

I have no options about this. I thought it could be handy.

@tmc
Copy link
Collaborator

tmc commented Mar 23, 2017

I would prefer the Register methods taking a variadic options slice or the mux expanding a bit in responsibility.

@tamalsaha
Copy link
Collaborator Author

@tmc, I have looked into the code and template. It seems to me that extending ServeMux might be easier way. I can also turn my matchers into ServeMuxOption. So, mux sounds good? Then I will try to update this pr. Now, this will be a pretty big change.

@tamalsaha
Copy link
Collaborator Author

@tmc, I think it is ready for review. I have extended ServeMux to add the matchers.

@tmc
Copy link
Collaborator

tmc commented Mar 23, 2017

$DAYJOB beckons but I can review this sometime soon. In the mean time you might take a look at somewhat related efforts in #323 and #305. Also, please sign the Google CLA. Thanks!

@tamalsaha
Copy link
Collaborator Author

@tmc, I have signed the CLA.

I think #323 is taken care by this pr.

return func(mux *ServeMux) {
mux.headerMatchers = append(mux.headerMatchers, func(key string) bool {
return strings.HasPrefix(strings.ToLower(key), h)
})
Copy link
Contributor

@nilium nilium Mar 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be handled without the ToLower by doing an EqualFold on the head of the key. For example, https://play.golang.org/p/lh_JIYnZ0s

I only mention this because EqualFold is a bit more involved than comparing two lowercase strings. This may not really matter, though, since I think the character set available to HTTP headers is fairly restrictive (i.e., ASCII). So, nitpicking a bit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @nilium . I updated accordingly.

@tamalsaha
Copy link
Collaborator Author

@tmc, I have added support for context annotators in my pr. Looks like we need this too!

@tamalsaha tamalsaha changed the title Allow user configurable header forwarding Allow user configurable header forwarding & context annotation Mar 24, 2017
@tamalsaha tamalsaha force-pushed the header-pass branch 2 times, most recently from 41d05d7 to 7af137c Compare March 24, 2017 12:02
@tamalsaha tamalsaha changed the title Allow user configurable header forwarding & context annotation Support user configurable header forwarding & context metadata Mar 24, 2017
@tamalsaha
Copy link
Collaborator Author

@ilius, I have added support for adding metadata to context via Mux options. Does this solve your use-case?

@ilius
Copy link

ilius commented Mar 28, 2017

@tamalsaha Not sure. Does it support returning error (in REST response) before reaching grpc client?

We need to read request cookie, set grpc context or return error, on each request
As I have done in #323

@tamalsaha
Copy link
Collaborator Author

tamalsaha commented Mar 28, 2017

I have few questions:

  • When you read cookie, do you need to send it via a direct key on Context or as part of metadata in Context? From looking at the metadata code, it seems to perform some encoding, decoding based on the data.

  • If your intention is to reject a call if it is missing Cookie? If so, why not just pass the cookie from http request to context metadata. Then in the grpc side, you can add an interceptor that will reject requests that are missing cookie. Does that work for you?

@ilius
Copy link

ilius commented Mar 28, 2017

  • We use metadata for putting context item (JWT token), because otherwise it won't be transferred via grpc

  • We reject the call if the cookie is missing, or invalid (invalid signed JWT token)

@tamalsaha
Copy link
Collaborator Author

tamalsaha commented Mar 28, 2017

@ilius, in that case, you will be able to handle your use case using this pr, like below:

gwMux := runtime.NewServeMux(runtime.EqualFoldMatcher("Cookie"))

Then on the grpc side, you can check for JWT token in cookie and reject the request if it fails in a grpc interceptor. IMO, this is better than failing at the gateway layer for 2 reasons:

  • You keep all your application logic in one place instead of passing some to the gateway.
  • If/when applications can direct http/2 calls to the grpc service, it will also work if JWT token in passed via cookie.

@tmc tmc mentioned this pull request Apr 15, 2017
3 tasks
Copy link
Collaborator

@tmc tmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamalsaha thanks for this! I have a few small comments but this is looking great.

import "strings"

// EqualMatcher performs a case-sensitive equality match for request metadata keys
func EqualMatcher(s string) ServeMuxOption {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer a WithHeaderMatcher Option that accepts a func(string) bool to both support more scenarios but also to have a smaller api surface area.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking further, perhaps the signature should be func(string) (string, bool) to allow transformations in addition to simple predicate filtering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tmc , I am not sure what you mean by WithHeaderMatcher option? Mind elaborating a bit?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean to something like:

type HeaderMatcherFunc func(string) (string, bool)
func WithHeaderMatcher(fn HeaderMatcherFunc) ServeMuxOption {
    return func(s *ServeMux) {
        s.headerMatcher = fn
   }
}

runtime/mux.go Outdated
@@ -36,12 +39,21 @@ func WithForwardResponseOption(forwardResponseOption func(context.Context, http.
}
}

// WithMetadata returns metadata to be passed with context.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment should elaborate a bit more

runtime/mux.go Outdated
// NewServeMux returns a new ServeMux whose internal mapping is empty.
func NewServeMux(opts ...ServeMuxOption) *ServeMux {
serveMux := &ServeMux{
handlers: make(map[string][]handler),
forwardResponseOptions: make([]func(context.Context, http.ResponseWriter, proto.Message) error, 0),
marshalers: makeMarshalerMIMERegistry(),
headerMatchers: make([]func(string) bool, 0),
metadataAdders: make([]func(context.Context, *http.Request) metadata.MD, 0),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we call this metadataAnnotators?

@tamalsaha
Copy link
Collaborator Author

@tmc, tests are passing.

@tmc
Copy link
Collaborator

tmc commented Apr 17, 2017

I wonder if we should expose the direction to the header matcher?

@tamalsaha
Copy link
Collaborator Author

Like a IncomingHeaderMatcher, OutgoingHeaderMatcher ?

@tmc
Copy link
Collaborator

tmc commented Apr 18, 2017

@tamalsaha I think those names are fine.

@tmc
Copy link
Collaborator

tmc commented Apr 18, 2017

@tamalsaha
Copy link
Collaborator Author

tamalsaha commented Apr 18, 2017

Updated pr to add separate incoming/outgoing HeaderMatcher. IncomingHeaderMatcher is set to DefaultHeaderMatcher to maintain current behavior.

@tmc tmc merged commit 893772d into grpc-ecosystem:master Apr 18, 2017
@tmc
Copy link
Collaborator

tmc commented Apr 18, 2017

LGTM

@sadlil sadlil deleted the header-pass branch April 18, 2017 03:50
@sadlil sadlil restored the header-pass branch April 18, 2017 03:50
@ilius
Copy link

ilius commented Apr 18, 2017

@tamalsaha I don't understand. We have no access to cookie in our grpc server handlers. Only context and input(request). We must use context for storing token. Cookie is only for REST (http1). So we must copy the token from cookie to GRPC context when translating REST to GRPC.

@tamalsaha
Copy link
Collaborator Author

@ilius , if you want to forward the Cookie in http request to grpc, you can add an incomingHeaderMatcher for that.

runtime.NewServeMux(runtime.WithIncomingHeaderMatcher(func(h string) (string, bool) {
		if strings.EqualFold(h, "Cookie") {
			return h, true
		}
		return "", false
	}));

If you want to forward a specific key from Cookie, you can also do that by returning that in the if block.

@tamalsaha tamalsaha deleted the header-pass branch April 18, 2017 05:50
@ilius
Copy link

ilius commented Apr 18, 2017

And where to put this header in the grpc context?

@tamalsaha
Copy link
Collaborator Author

It will be automatically added to the Metadata in grpc context. See LN # 66 & # 95 https://github.com/grpc-ecosystem/grpc-gateway/blob/master/runtime/context.go#L66

@ilius
Copy link

ilius commented Apr 18, 2017

Thanks

@ilius
Copy link

ilius commented Apr 19, 2017

@tamalsaha Sorry, I think runtime.WithIncomingHeaderMatcher transforms headers, not cookies

@tamalsaha
Copy link
Collaborator Author

tamalsaha commented Apr 19, 2017

@ilius , I am not sure I understand you. Using runtime.WithIncomingHeaderMatcher, you can pass the full http request's Cookie header to grpc. You can also send a specific key from the Cookie, if you prefer that. And custom application specific logic for validation that Cookie/Cookie-key should be done in your grpc server (directly in the method or using a interceptor). Does that make sense?

@ilius
Copy link

ilius commented Apr 19, 2017

Thanks, I get it now (the name mistakened me)

@lingyuan2014
Copy link

This is awesome. Been waiting for 2+ months for this feature. Would like to see this in a new release. (It's kinda unfortunate this missed 1.2.2 by just one commit)

@rogchap
Copy link
Contributor

rogchap commented Mar 4, 2022

For anyone reading this and looking for a way to get a *http.Cookie from the request:

In a normal REST request you would access Cookies via req.Cookies() or req.Cookie("my-cookie")

With all the suggestions above we are only mapping the request headers to the gRPC metadata (FYI: cookies are transmitted in the request header)

This means you have access to the raw cookie string in the metadata, not the http.Cookie struct.

There is no ParseCookie() function in the standard library 😞 see: golang/go#25194

So as a workaround you may need to do something like this (code not tested):

	md, _ := metadata.FromIncomingContext(ctx)
	rawCookies := md.Get("Grpc-Metadata-Cookie")
	dummyReq, _ := http.NewRequest("", "", nil)
	for _, rawCookie := range rawCookies {
		dummyReq.Header.Add("Cookie", rawCookie)
	}
	myCookie, _ := dummyReq.Cookie("my-cookie")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Why does not gateway forward headers as-is?
7 participants