firebase / firebase-admin-go

Firebase Admin Go SDK
Apache License 2.0
1.13k stars 244 forks source link

FR :Passing an Header while making FCM request via proxy #397

Closed bulletblp4 closed 4 years ago

bulletblp4 commented 4 years ago

[REQUIRED] Step 2: Describe your environment

[REQUIRED] Step 3: Describe the problem

I need to access the FCM endpoint via a proxy, which also need an header to be passed. My app currently makes 2 calls via different proxy, hence when I add the env var as HTTPS_PROXY in my go app it slows down the other request.

I need to figure out a way to call fcm via proxy url and and its header. Currently in the options package (while creating the client) I dont see a way to pass custom header nor do I see that option in message.AndroidConig similar to what we have for message.APNSConfig.Headers

Can we add an api to pass custom header if there isnt one already

Steps to reproduce:

I am creating a custom client and passing API and httpClient as opts. And Proxy is wrapped in the httpClient Passed in the opts

Relevant Code:

func initFCMClient(projectId string) (c *messaging.Client, err error) {

    config := &firebase.Config{ProjectID: "projectId"}
    proxyUrl, err := url.Parse("http://proxy:8080/v1")
    transport := http.Transport{}
    transport.Proxy = http.ProxyURL(proxyUrl)// set proxy
    myClient := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl)}}
    key := "secretKey"
    oClient := option.WithHTTPClient(myClient)
    oAuth := option.WithAPIKey(key)
    app, err := firebase.NewApp(context.Background(), config, oClient, oAuth)
    if err != nil {
        return nil, err
    }
    background := context.Background()
    c, err = app.Messaging(background)
    if err != nil {
        return nil, err
    }
    return c, err
}
google-oss-bot commented 4 years ago

This issue does not seem to follow the issue template. Make sure you provide all the required information.

hiranya911 commented 4 years ago

Start by taking a look at the ProxyConnectHeader field in http.Transport. You can set custom headers on proxy CONNECT requests by configuring this property.

If that's not what you want, you should be able to customize the RoundTripper or the Proxy to do what you want. Here's an example:

func customProxy(fixedURL *url.URL) func(*http.Request) (*url.URL, error) {
    return func(req *http.Request) (*url.URL, error) {
        req.Header.Add("My-Header", "value")
        return http.ProxyURL(fixedURL)
    }
}

myClient := &http.Client{Transport: &http.Transport{Proxy: customProxy(proxyUrl)}}

The SDK doesn't provide any APIs for customizing HTTP headers. The APNSConfig.Headers field only sets APNS headers for the notification, not HTTP headers.

bulletblp4 commented 4 years ago

So the client when calling the method client.Send doesn't seem to be picking up the proxy.

I get an error "unknown error while making an http call: Post https://fcm.googleapis.com/v1/projects/api-XXX/messages:send: Bad Request"

where as it should be picking up the proxy-url (http://fcm-proxy:8080/v1/projects/api-XXX/messages:send) I believe. I am doing a similar thing for my apns libs and those seems to call the apns proxy.

func initFCMClient(projectId string) (c *messaging.Client, err error) {

    transport := &http.Transport{
        Proxy: func(request *http.Request) (*url.URL, error) {
            request.Header.Add("HOST","fcm.googleapis.com")
            return url.Parse("http://fcm-proxy:8080/v1")
        },
        IdleConnTimeout: 60 * time.Second,
    }

    config := &firebase.Config{ProjectID: "api-XXX"}
    myClient := &http.Client{Transport: transport}
    oClient := option.WithHTTPClient(myClient)
    oAuth := option.WithAPIKey("secretkey")
    app, err := firebase.NewApp(context.Background(), config, oClient, oAuth)
    if err != nil {
        return nil, err
    }

    background := context.Background()
    c, err = app.Messaging(background)

    if err != nil {
        return nil, err
    }
    return c, err
}
hiranya911 commented 4 years ago

The proxy URL is not visible to the SDK, so it will never get logged in an error message raised by our APIs. Your issue is probably due to bad authentication.

oAuth := option.WithAPIKey("secretkey")

FCM v1 API do not support API key authentication. You must provide valid OAuth2 credentials.

PS: You can also easily verify whether your custom proxy URL is picked up by adding some logging code to your custom Proxy function.

hiranya911 commented 4 years ago

Spent a bit more time on this, and it actually looks like an issue with the proxy code. I'm experimenting with the following example:

    transport := &http.Transport{
        MaxConnsPerHost: 10,
        Proxy: func(request *http.Request) (*url.URL, error) {
            log.Printf("using proxy for %s", request.Host)
            request.Header.Add("Host", request.Host)
            return url.Parse("http://10.0.0.203:9001")          
        },
    }

        myClient := &http.Client{Transport: transport}
    req, err := http.NewRequestWithContext(ctx, "GET", "http://jsonplaceholder.typicode.com/posts/1", nil)
    if err != nil {
        log.Fatal(err)
    }

    resp, err := myClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)
    log.Println(resp.StatusCode, string(body))

This works as long as the request URL scheme is http. As soon as I switch to https, I get the above Not Found error (which seems to be a low-level networking error rather than an HTTP 404). So it essentially boils down to above code failing to proxy HTTPS requests over an HTTP connection.

I'm not an expert in this area of Go. Hopefully somebody can point out a solution that works.

hiranya911 commented 4 years ago

Sending via a regular CONNECT proxy

Turned out the problem was actually in my mock proxy server (I was using a simple reverse proxy script that couldn't handle HTTP to HTTPS bridging properly). Once I used an actual CONNECT proxy server (e.g. https://github.com/elazarl/goproxy), everything worked out fine. Here's my working example:

func main() {
    transport := &http.Transport{
        Proxy: func(request *http.Request) (*url.URL, error) {
            log.Printf("using proxy for %s", request.Host)
            return url.Parse("http://10.0.0.203:8080")
        },
    }

    cred, err := google.FindDefaultCredentials(
        context.Background(),
        "https://www.googleapis.com/auth/cloud-platform")
    if err != nil {
        log.Fatal(err)
    }

    trp := &oauth2.Transport{
        Base:   transport,
        Source: cred.TokenSource,
    }
    myClient := &http.Client{Transport: trp}
    oClient := option.WithHTTPClient(myClient)
    app, err := firebase.NewApp(context.Background(), nil, oClient)
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()
    c, err := app.Messaging(ctx)
    if err != nil {
        log.Fatal(err)
    }

    m := &messaging.Message{Topic: "test"}
    r, err := c.Send(ctx, m)
    if err != nil {
        log.Fatal(err)
    }

    log.Print(r)
}

Note that I'm using oauth2.Transport as a wrapper to inject credentials. Without it FCM request will fail with an HTTP 401 error. I didn't have to mess with the Host header in my proxy, as the Go http implementation handled the proxy protocol correctly.

Sending via a reverse proxy

Since you're attempting to manipulate the Host header on the request, I suspect you're trying to send the request via a reverse proxy. This is different from a regular HTTP proxy. To get the request to pass through a reverse proxy, here's what I had to do:

type addHeader struct {
    host     string
    delegate http.RoundTripper
}

func (ah *addHeader) RoundTrip(req *http.Request) (*http.Response, error) {
    req.Host = ah.host
    return ah.delegate.RoundTrip(req)
}

func main() {
    cred, err := google.FindDefaultCredentials(
        context.Background(),
        "https://www.googleapis.com/auth/cloud-platform")
    if err != nil {
        log.Fatal(err)
    }

    trp := &oauth2.Transport{
        Base:   &http.Transport{},
        Source: cred.TokenSource,
    }
    myClient := &http.Client{
        Transport: &addHeader{
            delegate: trp,
            host:     "fcm.googleapis.com",
        },
    }

    oClient := option.WithHTTPClient(myClient)
    ep := option.WithEndpoint("http://localhost:9001/v1")
    app, err := firebase.NewApp(context.Background(), nil, oClient, ep)
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()
    c, err := app.Messaging(ctx)
    if err != nil {
        log.Fatal(err)
    }

    m := &messaging.Message{Topic: "test"}
    r, err := c.Send(ctx, m)
    if err != nil {
        log.Fatal(err)
    }

    log.Print(r)
}

I'm using a custom RoundTripper to set the Host header on the request, and using option.WithEndpoint() to point to the reverse proxy.

bulletblp4 commented 4 years ago

Thank you ! that worked...

hiranya911 commented 4 years ago

I'm going to close this based on the information provided in my previous comments. Most developers should be able to setup proxy settings via environment variables. For more niche use cases one can use a combination of http and options packages as shown above. I wish there was a way to do options.WithHttpProxy() or options.WithTransport() instead of options.HTTPClient(). But that should be a feature request for the options package.