gofr-dev / gofr

An opinionated GoLang framework for accelerated microservice development. Built in support for databases and observability.
https://gofr.dev
Apache License 2.0
3.6k stars 236 forks source link

How access portions of the request. #1209

Open marvfinsy opened 6 days ago

marvfinsy commented 6 days ago

From the context I see u have a method to get Host. Query parameters etc.

How do i access other parts of the request like:

  1. Full url path?
  2. url path minus scheme and host?
  3. Access to request headers.

I need to get the path of the request without the scheme and host parts. Was going to create custom middleware to do this. It would add a custom header to the request with that information. Once in my route i'd access the header i added. yuk!

Of course If i can't read the generated header in my route he above is useless. Really hate the idea of altering original request adding self-generated headers.

Do i really need to do all the above to read request path information or to read headers?

thx ~Marvin marvinfoster@finsync.com mmarvb7@gmail.com

Umang01-hash commented 4 days ago

Hi @marvfinsy ,

Thanks for reaching out with your feature request for the GoFr framework. We’d like to understand your needs better:

URL Path Access: Since the URL path is directly mapped to the handler, could you explain why a separate method is necessary?

Request Headers: We currently have methods to access authentication-related headers. Could you please provide more details on the type of headers you need to access and the specific use case?

Understanding your specific needs will help us provide the best solution. Looking forward to your response.

marvfinsy commented 3 days ago

Yes i could set a variable = to the mapped path...

But my original idea was to map every URL path to the same handler. That handler can get the URL path from the http.Request.Url object. 80% to 90% of business logic seems to be the same.

There will be a "list" of acceptable headers. This common handler would also need to read those headers from the request.

Umang01-hash commented 3 days ago

@marvfinsy Thank you for sharing your feedback regarding header access. While we understand your idea of mapping every URL path to the same handler and reading headers from the request, we would love to learn more about your specific use case to ensure that our implementation aligns with your requirements.

Could you please provide an example or scenario where this functionality would be particularly useful? For instance, how would you envision using the list of acceptable headers, and what kind of business logic would depend on this header-based processing?

Your insights will help us evaluate and prioritize this feature to best serve your needs.

Looking forward to hearing from you!

devorbitus commented 1 day ago

@Umang01-hash an example application would be a Zoom "Recording Complete" webhook that requires validation of a specific timestamp from the header and then returning a specific payload

as described here:

https://developers.zoom.us/docs/api/rest/webhook-reference/#validate-your-webhook-endpoint

with a Javascript implementation here:

https://github.com/zoom/webhook-sample/blob/master/index.js#L24-L49

As far as I can tell, this would not be simple to accomplish with existing functionality.

devorbitus commented 1 day ago

However, this PR #1227 looks like it would make it easy to extract that header value that Zoom sends and then run the required calculations to return the validated expected value..

Umang01-hash commented 1 day ago

@devorbitus Thank you for highlighting this use case! While the proposed PR (#1227) simplifies header access, webhook validation for Zoom can be effectively handled at the middleware level. Middleware has full access to headers, including x-zm-request-timestamp and x-zm-signature, as well as the request body, making it an ideal place to handle the validation logic.

By performing the signature verification in middleware, we can ensure that only validated requests proceed to the handler, streamlining the process.

devorbitus commented 1 day ago

@Umang01-hash Can you show or direct me to a simplified example (even pseudocode would be helpful) of how someone can accomplish this?

The flow in the javascript code looks at the payload and when a specific validation property is detected it responds back a specific way instead of responding back with usual payload, how would a middleware implementation accomplish this?

marvfinsy commented 1 day ago

In my case i'm redirecting to a vendor site. The vendor has a unique set of rules regarding authentication etc... Some information required for authentication i've saved in a badger key-value store. I also need an http client for vendor calls. Once authenticated i can redirect based on the current endpoint path.

I could write a handler for each endpoint that calls a "common handler" and pass the endpoint path as a string to the "common handler". But it would be easier to have the one "common handler" accessing the http.Request.URL.Path for the information.

  1. For the authentication part. Not sure about accessing an http client with error handling and doing database reads/saves from middleware...
  2. For getting the endpoint_path. Reading ur response i assume i have access to the http.Request.URL object in middleware. cool... But i cannot set data in the context (thinking of Gin) to use it in a handler (can i) ??? I guess i could add a "private" header in the middleware and read it in the handler. But that feels like a extreme "hack"... :o)
Umang01-hash commented 10 hours ago

@devorbitus Here is my attempt to provide you with some implementation regarding how can we achieve our use-case using a middleware:

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"

    "gofr.dev/pkg/gofr"
    gofrHTTP "gofr.dev/pkg/gofr/http"
)

func main() {
    // Create a new application
    a := gofr.New()

    // Register the middleware for validating Zoom webhooks
    a.UseMiddleware(zoomWebhookValidationMiddleware("webhook-secret"))

    // Register the POST handler for the Zoom webhook
    a.POST("/zoom-webhook", handler)

    // Run the application
    a.Run()
}

// handler processes the validated webhook request
func handler(c *gofr.Context) (interface{}, error) {
    // Check if the request was validated by the middleware
    validated, ok := c.Value("zoomValidated").(bool)
    if !ok || !validated {
        return nil, fmt.Errorf("invalid signature")
    }

    // Parse the request body as per user-defined structure
    var requestBody map[string]interface{} // Generic map for flexibility
    if err := c.Bind(&requestBody); err != nil {
        return nil, fmt.Errorf("invalid request body: %w", err)
    }

    // Return the validated request body as a response
    return map[string]interface{}{
        "validated": validated,
        "body":      requestBody,
    }, nil
}

// zoomWebhookValidationMiddleware validates Zoom webhook requests
func zoomWebhookValidationMiddleware(secret string) gofrHTTP.Middleware {
    return func(inner http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Extract required headers for validation
            timestamp := r.Header.Get("x-zm-request-timestamp")
            signature := r.Header.Get("x-zm-signature")

            // Validate that the necessary headers are present
            if timestamp == "" || signature == "" {
                http.Error(w, "Missing required headers", http.StatusBadRequest)
                return
            }

            // Read and reset the request body
            bodyBytes, err := io.ReadAll(r.Body)
            if err != nil {
                http.Error(w, "Failed to read request body", http.StatusInternalServerError)
                return
            }
            r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))

            // Perform the signature validation
            if !validateZoomSignature(secret, timestamp, bodyBytes, signature) {
                http.Error(w, "Invalid signature", http.StatusUnauthorized)
                return
            }

            // Mark the request as validated and pass it to the next handler
            ctx := context.WithValue(r.Context(), "zoomValidated", true)
            inner.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

I have used context.WithValue to pass down any necessary information to handler in case we want to give a specific response based on request and else if it is not verified it can return from middleware itself.

Please let me know if we can provide any further assistance with respect to your use-case.

Thankyou.

Umang01-hash commented 9 hours ago

@marvfinsy addressing your concerns:

GoFr supports adding data to the context within middleware, which can be accessed in handlers: (I have shown an example of how to do this above please refer it.)

Also it's not generally suggested to use an HTTP client or database in middleware, we should perform essential tasks like authentication and pass results through the context.

marvfinsy commented 2 hours ago

thx...

Appears i can use middleware to get path from request.URL then save and read it from context... That works! Will give a "try" this weekend!

thx for help...