I would like to make a reusable middleware for validation throughout my API. Here, validation is done through govalidator, so I just need to pass the validation rules and a DTO where the request is mapped into.
func ValidationMiddleware(next http.HandlerFunc, validationRules govalidator.MapData, dto interface{}) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
utils.SetResponseHeaders(rw)
opts := govalidator.Options{
Request: r,
Data: &dto,
Rules: validationRules,
RequiredDefault: true,
}
v := govalidator.New(opts)
err := v.ValidateJSON()
if err != nil {
fmt.Println("Middleware found an error")
err := utils.ErrorWrapper{
StatusCode: 400,
Type: "Bad Request",
Message: err,
}
err.ThrowError(rw)
return
}
next(rw, r)
}
}
This is how the HandleFunc looks like:
var rules govalidator.MapData = govalidator.MapData{
"name": []string{"required"},
"description": []string{"required"},
"price": []string{"required", "float"},
}
func RegisterItemsRouter(router *mux.Router) {
itemsRouter := router.PathPrefix("/inventory").Subrouter()
itemsRouter.HandleFunc("/", middleware.ValidationMiddleware(addItem, rules, dto.CreateItem{
Name: "",
Description: "",
Price: govalidator.Float64{},
})).Methods("POST")
}
If no errors are found, govalidator parses the request body into the dto struct, so I would like to pass this down into the next handler and avoid trying to parse the body a second time.
How can I pass this struct down to the next HandleFunc?
From the code, it appears like you pass the request to the validator options, and the validator reads and validates the body from that. This poses several problems: An HTTP request can only be read once, so either the validator somehow returns you that unmarshaled object, or you have to read the body before validating it.
Going for the second solution, the first thing is your validator has to know the type of the object it has to unmarshal to:
func ValidationMiddleware(next http.HandlerFunc, factory func() interface{}, validationRules govalidator.MapData, dto interface{}) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
newInstance:=factory()
data, err:=ioutil.ReadAll(r.Body)
json.Unmarshal(data,newInstance)
// Validate newInstance here
r.WithContext(context.WithValue(r.Context(),"data",newInstance))
next(wr,r)
}
}
Where the func factory is a function that creates an instance of the object that will be unmarshaled for a request:
func RegisterItemsRouter(router *mux.Router) {
itemsRouter := router.PathPrefix("/inventory").Subrouter()
itemsRouter.HandleFunc("/", middleware.ValidationMiddleware(addItem, func() interface{} { return &TheStruct{}}, rules, dto.CreateItem{
Name: "",
Description: "",
Price: govalidator.Float64{},
})).Methods("POST")
}
This way, when a new request comes, a new instance of TheStruct will be created and unmarshaled, then validated. If the validation is ok, it will be placed into the context, so the next middleware or the handler can get it:
func handler(wr http.ResponseWriter,r *http.Request) {
item:=r.Context().Value("data").(*TheStruct)
...
}
Related
i receive a response body "bad request" with "httptest.Client().Postform"
type testServer struct {
*httptest.Server
}
func newTestServer(t *testing.T, h http.Handler) *testServer {
ts := httptest.NewTLSServer(h)
jar, err := cookiejar.New(nil)
if err != nil {
t.Fatal(err)
}
ts.Client().Jar = jar
ts.Client().CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return &testServer{ts}
}
func (ts *testServer) postForm(t *testing.T, urlPath string, form url.Values) (int, http.Header, string) {
rs, err := ts.Client().PostForm(ts.URL+urlPath, form)
if err != nil {
t.Fatal(err)
}
defer rs.Body.Close()
body, err := io.ReadAll(rs.Body)
if err != nil {
t.Fatal(err)
}
bytes.TrimSpace(body)
return rs.StatusCode, rs.Header, string(body)
}
I don't know where is the problem, i have also verified the url it's correct.
Always badrequest with POST but with GET request it's works fine.
this is the handler object :
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.Authenticated)
router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
standart := alice.New(app.recoverPanic, app.logRequest, securityHeaders)
return standart.Then(router)
}
the test function :https://go.dev/play/p/k45-JYTYCOS
the app.userSignupPost:
func (app *application) userSignupPost(w http.ResponseWriter, r *http.Request) {
var form userSignupForm
err := app.decodPostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.CheckField(validator.NotBlank(form.Name), "name", "this field must not be blank")
form.CheckField(validator.NotBlank(form.Email), "email", "this field must not be blank")
form.CheckField(validator.Matches(form.Email, validator.EmailRX), "email", "this field must be a valid email address")
form.CheckField(validator.NotBlank(form.Password), "password", "this field must not be blank")
form.CheckField(validator.MinChars(form.Password, 8), "password", "password must bee at least 8 caracter long")
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "signup.tmpl.html", data)
return
}
err = app.users.Insert(form.Name, form.Email, form.Password)
if err != nil {
if errors.Is(err, models.ErrDuplicateEmail) {
form.AddFieldError("email", "Email already exist")
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "signup.tmpl.html", data)
} else {
fmt.Println("error user postform")
app.serverError(w, err)
}
return
}
app.sessionManager.Put(r.Context(), "flash", "Signup Successful. Please log in")
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
}
It appears that you're using https://github.com/justinas/alice to register handlers - you don't want to do this. That package is for middleware chaining - e.g. "before all requests to this URL, first authenticate the request" - you'd put the authentication into a middleware function and then add it to the chain.
So every POST /user/signup request is getting passed first to app.userSignup() (what you are using to handle GET requests). This is because calling alice.ThenFunc() appends the passed handler to the chain and then returns the entire chain as a handler - you need to read this part of the Alice docs carefully before using it.
Replace this line:
router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
with
router.Handler(http.MethodPost, "/user/signup", http.HandlerFunc(app.userSignupPost))
You may not need the additional decoration of http.HandlerFunc() - try it with and without to see what works. I cannot say for sure without knowing what the body of app.userSignupPost() looks like (same for the other handler functions as well).
You'll then need to do the same for the other handler registration lines - you shouldn't be using middleware chaining for your end handlers. An http.Handler is used for saying, "send any request to path /PP/ppp with method XXXX to this function." Middleware chaining is for preprocessing (authentication, authorization, etc.) - a whole host of things can be done there, but end request handling shouldn't be one of them.
I'm still curious if your use of PostForm() is going to cause you issues for the reason I cited in my comment on your question - try a raw Post() and see if the behavior differs, but after refactoring to take out the alice goop (at least temporarily). When testing a handler, I'd start off with a much more minimal approach - test that the handler itself works before muddying the waters with both alice and what looks like this package.
I think i found the problem , the session cookie are not the same for get and post request. i don't know why it has changed.They use the same http.Client()
What I want to achieve here is to create a very generic middleware called, Expects that actually validates the current request according to the parameters provided. It will raise a Bad Request if the required params are not present or are empty. In Python (Flask) this would be very simple like:
#app.route('/endpoint', methods=['POST'])
#expects(['param1', 'param2'])
def endpoint_handler():
return 'Hello World'
The definition of expects would look like this (a very minimal example):
def expects(fields):
def decorator(view_function):
#wraps(view_function)
def wrapper(*args, **kwargs):
# get current request data
data = request.get_json(silent=True) or {}
for f in fields:
if f not in data.keys():
raise Exception("Bad Request")
return view_function(*args, **kwargs)
return wrapper
return decorator
I am just confused a little about how would I achieve that in Go. What I tried so far is:
type RequestParam interface {
Validate() (bool, error)
}
type EndpointParamsRequired struct {
SomeParam string `json:"some_param"`
}
func (p *EndpointParamsRequired) Validate() {
// My validation logic goes here
if len(p.SomeParam) == 0 {
return false, "Missing field"
}
}
func Expects(p RequestParam, h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Check if present in JSON request
// Unmarshall JSON
...
if _, err := p.Validate(); err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Bad request: %s", err)
return
}
}
}
and from main.go file:
func main() {
var (
endopintParams EndpointParamsRequired
)
r.HandleFunc("/endpoint", Expects(&endopintParams, EndpointHandler)).Methods("POST")
}
It actually works for the first time and validates the request, but after one valid request all the consecutive requests are successful even if the json does not contain the required param. Does that have anything to do with the global endopintParams I'm creating?
In order to perform Authorization, some attributes from the request is to be read so that input for Authorization Server can be made
For example, this is the interceptor. Here prepareAuthZInput is called to preparing the input
func AuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
input := prepareAuthZInput(info.FullMethod, req)
}
In this function, there's a big if-else part which checks for the actual type for the request, type casts it and then performs the input preparation.
func prepareAuthZInput(method string, req interface{}) {
var input Input
if methodName = "/Data/Call" {
callRequest, ok := req.(CallRequest)
if ok {
// prepare input from callRequest
}
} else if methodName = "/Data/Receive" {
receiveRequest, ok := req.(ReceiveRequest)
if ok {
// prepare input from receiveRequest
}
}
return input
}
How can I improve this code?
When doing something like this, it's typical to add auth data to the metadata instead of the request messages. This way the server doesn't need to inspect all the possible request payload types.
If you must use the request payload, it would be more idiomatic to use a type switch instead:
switch r := req.(type) {
case CallRequest: // r is a CallRequest...
case ReceiveRequest: // r is a ReceiveRequest...
default:
return status.Errorf(codes.Unimplemented, "unknown request type: %T", req)
}
I have REST services:
each request has a header with JWT token
each controller get parameters from request (variables, body..) and pass them to data layer
I need to pass JWT token from header of each request into corresponding data layer method like this:
func (a *App) UpdateOrder(_ http.ResponseWriter, r *http.Request) (interface{}, error) {
bodyData := new(models.Order)
err = json.NewDecoder(r.Body).Decode(&bodyData)
if err != nil {
return nil, err
}
user, err := a.Saga.GetUserByToken(r.Header.Get("Authorization")) // here
// error handling ...
a.DbLayer.UpdateOrder(id, bodyData, user) // and there
}
In this case I must write the same code for each controller to get the user by token, and pass this user to database layer explicitly.
Is there a way to pass this user for each request without writing this code in each controller ?
I know about middleware and I can get user by token in my middleware. But how can I pass this user from middleware to corresponding database level method ?
May be I am looking for something like "global variables" for goroutine ? I can get user in my middleware and set it to something like "global variable". I can get the value of this "global variable" in the database layer. But it must be "global variable" for the current web request and concurrent web requests mustn't affect to each other.
Is there a some mechanism in Go, http module or gorilla\mux to implement what I have called "global variables" ?
You are describing contexts.
Originally there was the gorilla context package, which provides a pseudoglobal context object - essentially a map[interface{}]interface{} with a reference intrinsicly available to all players in the middleware/controller/datalayer stack.
See this except from an excellent guide to the package (all credit to the author, Matt Silverlock).
type contextKey int
// Define keys that support equality.
const csrfKey contextKey = 0
const userKey contextKey = 1
var ErrCSRFTokenNotPresent = errors.New("CSRF token not present in the request context.")
// We'll need a helper function like this for every key:type
// combination we store in our context map else we repeat this
// in every middleware/handler that needs to access the value.
func GetCSRFToken(r *http.Request) (string, error) {
val, ok := context.GetOk(r, csrfKey)
if !ok {
return "", ErrCSRFTokenNotPresent
}
token, ok := val.(string)
if !ok {
return "", ErrCSRFTokenNotPresent
}
return token, nil
}
// A bare-bones example
func CSRFMiddleware(h http.Handler) http.Handler {
return func(w http.ResponseWriter, r *http.Request) {
token, err := GetCSRFToken(r)
if err != nil {
http.Error(w, "No good!", http.StatusInternalServerError)
return
}
// The map is global, so we just call the Set function
context.Set(r, csrfKey, token)
h.ServeHTTP(w, r)
}
}
After the gorilla package's inception, a context package was added to the standard library. It's slightly different, in that contexts are no longer pseudoglobal, but instead passed from method to method. Under this, the context comes attached to the initial request - available via request.Context. Layers below the handler can accept a context value as a part of their signature, and read values from it.
Here's a simplified example:
type contextKey string
var (
aPreSharedKey = contextKey("a-preshared-key")
)
func someHandler(w http.ResponseWriter, req *http.Request) {
ctx := context.WithValue(req.Context, aPreSharedKey, req.Header.Get("required-header"))
data, err := someDataLayerFunction(ctx)
if err != nil {
fmt.Fprintf(w, "uhoh", http.StatusBadRequest)
return
}
fmt.Fprintf(w, data, http.StatusOK)
}
func someDataLayerFunction(ctx context.Context) (string, error) {
val, ok := ctx.Value(aPreSharedKey).(string)
if !ok {
return nil, errors.New("required context value missing")
}
return val
}
For more details and a less contrived example, check out google's excellent blog on the context package's use.
I am trying to add context to Authorization middleware. The ContextHandler is a handler which will be passed to api handlers to take care of connections and config variables. A struct Method ServeHTTP also has been added to the ContextHandler so that it satisfies the net/Http interface for handling requests properly.
CheckAuth is the middle ware which takes in the request to check token validation etc, If token is valid, executes the ServeHTTP method and if not, Returns the appropriate error in the response.
Code compiles, but i am getting error in the ServeHTTP method.
type ContextHandler struct {
*AppContext
Handler func(*AppContext, http.ResponseWriter, *http.Request)(int, error)
}
type AppContext struct {
Db *mgo.Session
Config *simplejson.Json
}
func (ah *ContextedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
status, err := ah.handler(ah.AppContext, w, r)
if err != nil {
switch status {
case http.StatusNotFound:
http.NotFound(w, r)
case http.StatusInternalServerError:
http.Error(w, http.StatusText(status), status)
default:
http.Error(w, http.StatusText(405), 405)
}}}
func CheckAuth(h http.Handler) http.Handler {
log.Println("Entered in CheckAuth")
f := func( w http.ResponseWriter, r *http.Request) {
authorizationToken := r.Header.Get("Authorization")
if authorizationToken != ""{
secret := []byte("somejunk")
var credentials authorization
token, err := jwt.ParseWithClaims(authorizationToken, &credentials, func(t *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err == nil && token.Valid {
//If everything is fine serve the Http request
h.ServeHTTP( w, r)
return
} else {
//Some response returned
json.NewEncoder(w).Encode(response)
return
}
//Check if user exists in the database
if dberr != nil {
//SOmeresponse to be returned
json.NewEncoder(w).Encode(response)
return
}
}else{
response := simplejson.New()
//response authorization header is missing
json.NewEncoder(w).Encode(response)
return
}
}
return http.HandlerFunc(f)
}
func Initdb(configfile *simplejson.Json) *mgo.Session {
//return mongodbsession, copy and close while using it
}
In main.go file in the parent package
func main() {
var FileUploadContextHandler *ContextedHandler = &ContextedHandler{&context, filesystem.FileUpload}
router.Methods("POST").Path("/decentralizefilesystem/fileupload").Name("FileUpload").Handler(CheckAuth(FileUploadContextHandler))
}
I am getting this error
2018/07/08 20:45:38 http: panic serving 127.0.0.1:52732: runtime error: invalid memory address or nil pointer dereference
goroutine 35 [running]:
net/http.(*conn).serve.func1(0xc4202ce140)
/usr/local/go/src/net/http/server.go:1726 +0xd0
panic(0x6fe680, 0x92cb10)
/usr/local/go/src/runtime/panic.go:502 +0x229
gitlab.com/mesha/Gofeynmen/vendor/gopkg.in/mgo%2ev2.(*Session).Copy(0x0, 0x7ff9485fb060)
/home/feynman/goworkspace/src/gitlab.com/mesha/Gofeynmen/vendor/gopkg.in/mgo.v2/session.go:1589 +0x22
gitlab.com/mesha/Gofeynmen/appsettings.CheckAuth.func1(0x7ff9485fb060, 0xc420276300, 0xc4202e4200)
/home/feynman/goworkspace/src/gitlab.com/mesha/Gofeynmen/appsettings/appsettings.go:115 +0x361
net/http.HandlerFunc.ServeHTTP(0xc420290180, 0x7ff9485fb060, 0xc420276300, 0xc4202e4200)
/usr/local/go/src/net/http/server.go:1947 +0x44
github.com/gorilla/mux.(*Router).ServeHTTP(0xc42024a310, 0x7ff9485fb060, 0xc420276300, 0xc4202e4200)
/home/feynman/goworkspace/src/github.com/gorilla/mux/mux.go:162 +0xed
github.com/gorilla/handlers.loggingHandler.ServeHTTP(0x7a8120, 0xc42000e018, 0x7a7b20, 0xc42024a310, 0x7aad60, 0xc4202f0000, 0xc4202e4000)
/home/feynman/goworkspace/src/github.com/gorilla/handlers/handlers.go:69 +0x123
github.com/gorilla/handlers.(*cors).ServeHTTP(0xc4202c4090, 0x7aad60, 0xc4202f0000, 0xc4202e4000)
/home/feynman/goworkspace/src/github.com/gorilla/handlers/cors.go:52 +0xa3b
net/http.serverHandler.ServeHTTP(0xc4202da0d0, 0x7aad60, 0xc4202f0000, 0xc4202e4000)
/usr/local/go/src/net/http/server.go:2694 +0xbc
net/http.(*conn).serve(0xc4202ce140, 0x7ab120, 0xc42025e100)
/usr/local/go/src/net/http/server.go:1830 +0x651
created by net/http.(*Server).Serve
/usr/local/go/src/net/http/server.go:2795 +0x27b
It's likely an attempt to dereference ah from (ah *ContextedHandler), when ah is not a pointer to a ContextedHandler.
The types in this assignment don't match up:
var FileUploadContextHandler *ContextedHandler =
ContextedHandler{&context, filesystem.FileUpload}
On the left side you have type *ContextedHandler. On the right side you have type ContextedHandler.
Did you mean
var FileUploadContextHandler *ContextedHandler =
&ContextedHandler{&context, filesystem.FileUpload}
Or did you mean
var FileUploadContextHandler ContextedHandler =
ContextedHandler{&context, filesystem.FileUpload}
?
The argument passed to the CheckAuth function appears to not match the function signature either:
CheckAuth(FileUploadContextHandler)
FileUploadContextHandler is type *ContextedHandler. The function signature is:
func CheckAuth(h contextHandlerFunc) contextHandlerFunc
The type definition of contextHandlerFunc does not appear to be part of the code you shared.
A problem with this line:
router.Methods("POST").Path("/decentralizefilesystem/fileupload").Name("FileUpload").Handler(CheckAuth(FileUploadContextHandler))
...would be easier to track down if you broke it up into variable assignments on several lines and then figured out which line the panic pointed to.