lib/handlerfactory/request_handler.go (201 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package handlerfactory allows creating HTTP handlers for services. package handlerfactory import ( "net/http" "regexp" "runtime/debug" "github.com/gorilla/mux" /* copybara-comment */ "google.golang.org/grpc/codes" /* copybara-comment */ "google.golang.org/grpc/status" /* copybara-comment */ "github.com/golang/protobuf/proto" /* copybara-comment */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/globalflags" /* copybara-comment: globalflags */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/httputils" /* copybara-comment: httputils */ "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/storage" /* copybara-comment: storage */ glog "github.com/golang/glog" /* copybara-comment */ ) // extractVars extracts variables from a request. // defined here to faciliated testing. // TODO: do not rely on registeration of routes at global mux for parsing names, // pass it explicitly. var extractVars = mux.Vars // Options contains the information about a handler service. // Essentially the service interface + some options to the HTTP wrapper for it. type Options struct { TypeName string NameField string PathPrefix string HasNamedIdentifiers bool NameChecker map[string]*regexp.Regexp Service func() Service } // Service is the role interface for a service that will be wrapped. type Service interface { Setup(r *http.Request, tx storage.Tx) (int, error) // TODO: Have LookupItem() return an error instead, so different errors can be handled // properly, e.g. permission denied error vs. lookup error. LookupItem(r *http.Request, name string, vars map[string]string) bool NormalizeInput(r *http.Request, name string, vars map[string]string) error Get(r *http.Request, name string) (proto.Message, error) Post(r *http.Request, name string) (proto.Message, error) Put(r *http.Request, name string) (proto.Message, error) Patch(r *http.Request, name string) (proto.Message, error) Remove(r *http.Request, name string) (proto.Message, error) CheckIntegrity(r *http.Request) *status.Status Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error } // MakeHandler created a HTTP handler wrapper around a given service. func MakeHandler(s storage.Store, opts *Options) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { resp, err := Process(s, opts, r) if err != nil { httputils.WriteError(w, err) return } if resp != nil { httputils.WriteResp(w, resp) } } } // Process computes the response for a request. func Process(s storage.Store, opts *Options, r *http.Request) (_ proto.Message, ferr error) { defer func() { if c := recover(); c != nil { glog.Errorf("CRASH %s %s: %v\n%s", r.Method, r.URL.Path, c, string(debug.Stack())) if ferr == nil { ferr = status.Errorf(codes.Internal, "internal error") } } }() hi := opts.Service() var op func(*http.Request, string) (proto.Message, error) switch r.Method { case http.MethodGet: op = hi.Get case http.MethodPost: op = hi.Post case http.MethodPut: op = hi.Put case http.MethodPatch: op = hi.Patch case http.MethodDelete: op = hi.Remove default: return nil, status.Errorf(codes.InvalidArgument, "request method not supported: %q", r.Method) } // TODO: move inside each service and don't pass NameChecker here. name, vars, err := ValidateResourceName(r, opts.NameField, opts.NameChecker) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "%v", err) } typ := opts.TypeName desc := r.Method + " " + typ tx, err := s.Tx(r.Method != http.MethodGet) if err != nil { return nil, status.Errorf(codes.Unavailable, "service dependencies not available; try again later") } defer func() { err := tx.Finish() if ferr == nil { ferr = err } }() // Get rid of Setup and move creation of transaction inside service methods. // if _, err = hi.Setup(r, tx); err != nil { return nil, err } // TODO: Replace NormalizeInput with a ParseReq that returns a request proto message. // TODO: Explicitly pass the message to the service methods. if err := hi.NormalizeInput(r, name, vars); err != nil { return nil, toStatusErr(codes.InvalidArgument, err, r) } // TODO: get rid of LookupItem and move this inside the service methods. exists := hi.LookupItem(r, name, vars) switch r.Method { case http.MethodPost: if exists { return nil, status.Errorf(codes.AlreadyExists, "%s already exists: %q", typ, name) } case http.MethodGet, http.MethodPatch, http.MethodPut, http.MethodDelete: if !exists { if opts.HasNamedIdentifiers { return nil, status.Errorf(codes.NotFound, "%s not found: %q", typ, name) } return nil, status.Errorf(codes.NotFound, "%s not found", typ) } } if r.Method == http.MethodGet { resp, err := op(r, name) if err != nil { return nil, toStatusErr(codes.InvalidArgument, err, r) } return resp, nil } resp, err := RunRMWTx(r, tx, op, hi.CheckIntegrity, hi.Save, name, vars, typ, desc) if err != nil { return nil, err } return resp, nil } // ValidateResourceName checks if the resource name is valid. // Returns the resource name and vars in it. func ValidateResourceName(r *http.Request, field string, nameRE map[string]*regexp.Regexp) (string, map[string]string, error) { nameVar := "name" if len(field) > 0 { nameVar = field } vars := extractVars(r) name := vars[nameVar] for k, v := range vars { if err := httputils.CheckName(k, v, nameRE); err != nil { return "", nil, err } } return name, vars, nil } // RunRMWTx performs a RMW operation. // Saves the transaction after performing integraty check. // Rolls back the transaction on any failure. // TODO: move outside this package. Service handlers should call it. func RunRMWTx( r *http.Request, tx storage.Tx, op func(*http.Request, string) (proto.Message, error), check func(*http.Request) *status.Status, save func(*http.Request, storage.Tx, string, map[string]string, string, string) error, name string, vars map[string]string, typ string, desc string, ) (proto.Message, error) { resp, err := op(r, name) if err != nil { return nil, toStatusErr(codes.InvalidArgument, err, r) } if st := check(r); st != nil { tx.Rollback() return nil, st.Err() } if err := save(r, tx, name, vars, desc, typ); err != nil { tx.Rollback() return nil, toStatusErr(codes.Internal, err, r) } return resp, nil } // toStatusErr make a status err with given code, if err already status err, just return. // TODO: use status err as early as possible, than we can get rid of this func. func toStatusErr(code codes.Code, err error, r *http.Request) error { if _, ok := status.FromError(err); ok { return err } if globalflags.EnableDevLog { glog.WarningDepth(1, r.Method, r.URL.Path, "still using non status err") } return status.Errorf(code, "%v", err) } // Empty is a empty Service implementation for mixin. type Empty struct { } // Setup empty impl func (s *Empty) Setup(r *http.Request, tx storage.Tx) (int, error) { return 0, nil } // LookupItem empty impl func (s *Empty) LookupItem(r *http.Request, name string, vars map[string]string) bool { return r.Method != http.MethodPost } // NormalizeInput empty impl func (s *Empty) NormalizeInput(r *http.Request, name string, vars map[string]string) error { return nil } // Get return 404 func (s *Empty) Get(r *http.Request, name string) (proto.Message, error) { return nil, status.Errorf(codes.NotFound, "GET %s not exist", r.URL.Path) } // Post return 404 func (s *Empty) Post(r *http.Request, name string) (proto.Message, error) { return nil, status.Errorf(codes.NotFound, "POST %s not exist", r.URL.Path) } // Put return 404 func (s *Empty) Put(r *http.Request, name string) (proto.Message, error) { return nil, status.Errorf(codes.NotFound, "PUT %s not exist", r.URL.Path) } // Patch return 404 func (s *Empty) Patch(r *http.Request, name string) (proto.Message, error) { return nil, status.Errorf(codes.NotFound, "PATCH %s not exist", r.URL.Path) } // Remove return 404 func (s *Empty) Remove(r *http.Request, name string) (proto.Message, error) { return nil, status.Errorf(codes.NotFound, "DELETE %s not exist", r.URL.Path) } // CheckIntegrity empty impl func (s *Empty) CheckIntegrity(r *http.Request) *status.Status { return nil } // Save empty impl func (s *Empty) Save(r *http.Request, tx storage.Tx, name string, vars map[string]string, desc, typeName string) error { return nil }