package api import ( "context" "database/sql" "encoding/json" "fmt" "math/rand" "net/http" "reflect" "time" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambdacontext" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" ) func (api Api) NewContext(baseCtx context.Context, requestID string, request events.APIGatewayProxyRequest) (Context, error) { serviceContext, err := api.Service.NewContext(baseCtx, requestID, nil) if err != nil { return nil, err } return &apiContext{ Context: serviceContext, request: request, }, nil } //this is native handler for lambda passed into lambda.Start() //to run locally, this is called from app.ServeHTTP() func (api Api) Handler(baseCtx context.Context, apiGatewayProxyReq events.APIGatewayProxyRequest) (res events.APIGatewayProxyResponse, err error) { res = events.APIGatewayProxyResponse{ StatusCode: http.StatusInternalServerError, Body: "undefined response", Headers: map[string]string{}, } // Replace the proxy resource with the path, has some edge cases but works for our current API implementation // Edge case being that if have path params then specify those routes explicitly if apiGatewayProxyReq.Resource == "/{proxy+}" { apiGatewayProxyReq.Resource = apiGatewayProxyReq.Path } //get request-id from HTTP headers (used when making internal service calls) //if not defined in header, get the AWS request id from the AWS context requestID, ok := apiGatewayProxyReq.Headers[api.requestIDHeaderKey] if !ok || requestID == "" { if lambdaContext, ok := lambdacontext.FromContext(baseCtx); ok && lambdaContext != nil { requestID = lambdaContext.AwsRequestID } } // service context invoke the starters and could fail, e.g. if cannot connect to db Ctx, err = api.NewContext(baseCtx, requestID, apiGatewayProxyReq) if err != nil { return res, err } //report handler crashes if api.crashReporter != nil { defer api.crashReporter.Catch(Ctx) } defer func() { //set CORS headers on every response if api.cors != nil { for n, v := range api.cors.CORS() { res.Headers[n] = v } } }() defer func() { Ctx.LogAPIRequestAndResponse(res, err) if err != nil { Ctx.Errorf("failed: %+v", err) //try to retrieve HTTP code from error if withCause, ok := err.(errors.ErrorWithCause); ok && withCause.Code() != 0 { res.StatusCode = withCause.Code() err = withCause.Cause() //drop the http layers + up } else { //no HTTP code indicate in err, //see if there are SQL errors that could indicate code if code, ok := StatusCodeFromSQLError(err); ok { res.StatusCode = code } } errorMessage := fmt.Sprintf("%c", err) jsonError, _ := json.Marshal(map[string]interface{}{"message": errorMessage}) res.Headers["Content-Type"] = "application/json" res.Body = string(jsonError) err = nil //never pass error back to lambda or http server } if api.requestIDHeaderKey != "" { res.Headers[api.requestIDHeaderKey] = Ctx.RequestID() } if err := api.Service.WriteValues(Ctx.StartTime(), time.Now(), Ctx.RequestID(), map[string]interface{}{ "direction": "incoming", "type": "api", "request_id": Ctx.RequestID(), "request": Ctx.Request(), "response": res}, ); err != nil { Ctx.Errorf("failed to audit: %+v", err) } }() //Early return OPTIONS call if apiGatewayProxyReq.HTTPMethod == "OPTIONS" { res.StatusCode = http.StatusNoContent err = nil return } rand.Seed(time.Now().Unix()) for checkName, check := range api.checks { var checkData interface{} checkData, err = check.Check(Ctx) if err != nil { err = errors.Wrapf(err, "%s", checkName) return } if err = Ctx.Set(checkName, checkData); err != nil { err = errors.Wrapf(err, "failed to set check(%s) data=(%T)%+v", checkName, checkData, checkData) return } } Ctx.Debugf("HTTP %s %s ...\n", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource) Ctx.WithFields(map[string]interface{}{ "http_method": Ctx.Request().HTTPMethod, "path": Ctx.Request().Path, "api_gateway_request_id": Ctx.Request().RequestContext.RequestID, "user_cognito_auth_provider": Ctx.Request().RequestContext.Identity.CognitoAuthenticationProvider, "user_arn": Ctx.Request().RequestContext.Identity.UserArn, }).Infof("Start API Handler") //TODO: // // Get claims and check the status of the user // Ctx.Claims, err = api.RetrieveClaims(&apiGatewayProxyReq) // if err != nil { // return events.APIGatewayProxyResponse{ // StatusCode: http.StatusBadRequest, // Body: fmt.Sprintf("%v\n", err), // Headers: utils.CorsHeaders(), // }, nil // } // if Ctx.Claims.UserID != nil { // userStatusResponse := checkUserStatus(Ctx.Claims) // if userStatusResponse != nil { // return *userStatusResponse, nil // } // } // permissionString := fmt.Sprintf("API_%s%s:%s", os.Getenv("MICRO_SERVICE_API_BASE_PATH"), apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod) // if !permissions.HasPermission(Ctx.Claims.Role, permissionString) { // response, _ := apierr.ClientError(http.StatusUnauthorized, fmt.Sprintf("You do not have access to the requested resource: %s", permissionString)) // if Ctx.Claims.Role == nil { // Ctx.Errorf("%d :: %s: %v", *Ctx.Claims.RoleID, permissionString, fmt.Errorf("you have no role")) // } else if Ctx.Claims.RoleID == nil { // Ctx.Errorf("%s: you have no role ID", permissionString) // } // return response, nil // } //route on method and path resourceHandler, err := api.router.Route(apiGatewayProxyReq.Resource, apiGatewayProxyReq.HTTPMethod) if err != nil { err = errors.Wrapf(err, "invalid route") return } if legacyHandlerFunc, ok := resourceHandler.(func(req events.APIGatewayProxyRequest) (response events.APIGatewayProxyResponse, err error)); ok { Ctx.Debugf("Calling legacy handler...") return legacyHandlerFunc(apiGatewayProxyReq) } handler, ok := resourceHandler.(handler) if !ok { //should not get here if validateAPIEndpoints() is properly checking! err = errors.HTTP(http.StatusInternalServerError, errors.Errorf("invalid handler function %T", resourceHandler), "invalid routing") } //new type of handler function //allocate, populate and validate params struct paramsStruct, paramsErr := Ctx.GetRequestParams(handler.RequestParamsType) if paramsErr != nil { err = errors.HTTP(http.StatusBadRequest, paramsErr, "invalid parameters") return } //apply claims to params struct - TODO: Removed - see if cannot force to get claims from context always // if err = Ctx.Claims.FillOnObject(Ctx.request, ¶msStruct); err != nil { // err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on parameters") // return // } Ctx.Debugf("Params: (%T) %+v", paramsStruct, paramsStruct) args := []reflect.Value{ reflect.ValueOf(paramsStruct), } var bodyStruct interface{} if handler.RequestBodyType != nil { //allocate, populate and validate request struct bodyStruct, err = Ctx.GetRequestBody(handler.RequestBodyType) if err != nil { err = errors.HTTP(http.StatusBadRequest, err, "invalid body") return } //apply claims to request struct - TODO: Removed - see if cannot force to get claims from context always // if err = Ctx.Claims.FillOnObject(Ctx.request, &bodyStruct); err != nil { // err = errors.HTTP(http.StatusInternalServerError, err, "claims failed on body") // return // } Ctx.Debugf("Body: (%T) %+v", bodyStruct, bodyStruct) args = append(args, reflect.ValueOf(bodyStruct)) } //call handler in a func with defer to catch potential crash Ctx.Infof("Calling handle %s %s ...", apiGatewayProxyReq.HTTPMethod, apiGatewayProxyReq.Resource) var results []reflect.Value results, err = func() (results []reflect.Value, err error) { defer func() { if crashErr := recover(); crashErr != nil { stack := logger.CallStack() err = errors.Errorf("handler function crashed: %v, with stack: %+v", crashErr, stack) return } }() results = handler.FuncValue.Call(args) return results, nil }() if err != nil { err = errors.Wrapf(err, "handler failed") return } //Ctx.Debugf("handler -> results: %v", results) //see if handler failed using last result of type error lastResultValue := results[len(results)-1] if !lastResultValue.IsNil() { err = lastResultValue.Interface().(error) if err != nil { return } } //handler succeeded, some handler does not have a response data (typically post/put/patch/delete) err = nil switch apiGatewayProxyReq.HTTPMethod { case http.MethodDelete: res.StatusCode = http.StatusNoContent default: res.StatusCode = http.StatusOK } if len(results) > 1 { responseStruct := results[0].Interface() Ctx.Debugf("Response type: %T", responseStruct) var bodyBytes []byte bodyBytes, err = json.Marshal(responseStruct) if err != nil { err = errors.Wrapf(err, "failed to encode response content") return } res.Headers["Content-Type"] = "application/json" res.Body = string(bodyBytes) } else { //no content delete(res.Headers, "Content-Type") res.Body = "" } return } //look for SQL errors in the error stack func StatusCodeFromSQLError(err error) (int, bool) { if err == sql.ErrNoRows { return http.StatusNotFound, true } if errWithCause, ok := err.(errors.ErrorWithCause); ok { return StatusCodeFromSQLError(errWithCause.Cause()) } //could not determine known SQL error return 0, false }