Skip to content

Commit

Permalink
Release/v0.7.3 (#94)
Browse files Browse the repository at this point in the history
* Bump up APIFW version to v0.7.3

* Update Dockerfile

* Update dependencies

* Add new interface

* Update the demo

* Add suffix support to the unknown parameters detection

* Add Host header value to the config

* Bump Go version to 1.21.11
  • Loading branch information
afr1ka authored Jun 6, 2024
1 parent 40edff2 commit dbe4796
Show file tree
Hide file tree
Showing 79 changed files with 4,101 additions and 971 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
needs:
- draft-release
env:
X_GO_DISTRIBUTION: "https://go.dev/dl/go1.21.9.linux-amd64.tar.gz"
X_GO_DISTRIBUTION: "https://go.dev/dl/go1.21.11.linux-amd64.tar.gz"
strategy:
matrix:
include:
Expand Down Expand Up @@ -160,7 +160,7 @@ jobs:
needs:
- draft-release
env:
X_GO_VERSION: "1.21.9"
X_GO_VERSION: "1.21.11"
strategy:
matrix:
include:
Expand All @@ -178,7 +178,7 @@ jobs:
-
uses: addnab/docker-run-action@v3
with:
image: golang:${{ env.X_GO_VERSION }}-alpine3.18
image: golang:${{ env.X_GO_VERSION }}-alpine3.19
options: >
--volume ${{ github.workspace }}:/build
--workdir /build
Expand Down Expand Up @@ -267,11 +267,11 @@ jobs:
include:
- arch: armv6
distro: bullseye
go_distribution: https://go.dev/dl/go1.21.9.linux-armv6l.tar.gz
go_distribution: https://go.dev/dl/go1.21.11.linux-armv6l.tar.gz
artifact: armv6-libc
- arch: aarch64
distro: bullseye
go_distribution: https://go.dev/dl/go1.21.9.linux-arm64.tar.gz
go_distribution: https://go.dev/dl/go1.21.11.linux-arm64.tar.gz
artifact: arm64-libc
- arch: armv6
distro: alpine_latest
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.21-alpine3.18 AS build
FROM golang:1.21-alpine3.19 AS build

ARG APIFIREWALL_VERSION
ENV APIFIREWALL_VERSION=${APIFIREWALL_VERSION}
Expand All @@ -14,15 +14,15 @@ COPY . .

RUN go mod download -x && \
go build \
-ldflags="-X main.build=${APIFIREWALL_VERSION} -linkmode 'external' -extldflags '-static' -s -w" \
-ldflags="-X main.build=${APIFIREWALL_VERSION} -s -w" \
-buildvcs=false \
-o ./api-firewall \
./cmd/api-firewall

# Smoke test
RUN ./api-firewall -v

FROM alpine:3.18 AS composer
FROM alpine:3.19 AS composer

WORKDIR /output

Expand All @@ -32,7 +32,7 @@ COPY docker-entrypoint.sh ./usr/local/bin/docker-entrypoint.sh
RUN chmod 755 ./usr/local/bin/* && \
chown root:root ./usr/local/bin/*

FROM alpine:3.18
FROM alpine:3.19

RUN adduser -u 1000 -H -h /opt -D -s /bin/sh api-firewall

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION := 0.7.2
VERSION := 0.7.3

.DEFAULT_GOAL := build

Expand All @@ -22,7 +22,7 @@ bench:
genmocks:
mockgen -source ./internal/platform/proxy/chainpool.go -destination ./internal/platform/proxy/httppool_mock.go -package proxy
mockgen -source ./internal/platform/database/database.go -destination ./internal/platform/database/database_mock.go -package database
mockgen -source ./cmd/api-firewall/internal/updater/updater.go -destination ./cmd/api-firewall/internal/updater/updater_mock.go -package updater
mockgen -source ./internal/platform/database/updater/updater.go -destination ./internal/platform/database/updater/updater_mock.go -package updater
mockgen -source ./internal/platform/proxy/ws.go -destination ./internal/platform/proxy/ws_mock.go -package proxy
mockgen -source ./internal/platform/proxy/wsClient.go -destination ./internal/platform/proxy/wsClient_mock.go -package proxy

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ By allowing you to set the traffic requirements with the API specification, API

[API Firewall works](https://www.wallarm.com/what/the-concept-of-a-firewall) as a reverse proxy with a built-in OpenAPI 3.0 or GraphQL request and response validator. It is written in Golang and using fasthttp proxy. The project is optimized for extreme performance and near-zero added latency.

During the processing of incoming requests, the API Firewall recognizes a range of `Content-Type` header values, including:

* `application/json`
* `application/xml`
* `application/octet-stream`
* `application/x-www-form-urlencoded`
* `application/x-yaml`
* `application/yaml`
* `application/zip`
* `multipart/form-data`
* `text/csv`
* `text/plain`
* `+json` structured syntax suffixes
* `+xml` structured syntax suffixes
* `+yaml` structured syntax suffixes
* `+csv` structured syntax suffixes

## Starting API Firewall

To download, install, and start API Firewall on Docker, refer to:
Expand Down
47 changes: 24 additions & 23 deletions cmd/api-firewall/internal/handlers/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/wallarm/api-firewall/internal/platform/database"
"github.com/wallarm/api-firewall/internal/platform/router"
"github.com/wallarm/api-firewall/internal/platform/web"
"github.com/wallarm/api-firewall/pkg/APIMode/validator"
)

var (
Expand Down Expand Up @@ -96,7 +97,7 @@ func getWallarmSchemaID(ctx *fasthttp.RequestCtx, storedSpecs database.DBOpenAPI
}

// Get Wallarm Schema ID
xWallarmSchemaIDsStr := string(ctx.Request.Header.Peek(web.XWallarmSchemaIDHeader))
xWallarmSchemaIDsStr := strconv.B2S(ctx.Request.Header.Peek(web.XWallarmSchemaIDHeader))
if xWallarmSchemaIDsStr == "" {
return nil, nil, errors.New("required X-WALLARM-SCHEMA-ID header is missing")
}
Expand Down Expand Up @@ -128,8 +129,8 @@ func getWallarmSchemaID(ctx *fasthttp.RequestCtx, storedSpecs database.DBOpenAPI
return
}

// APIModeRouteHandler routes request to the appropriate handler according to the OpenAPI specification schema ID
func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {
// APIModeMainHandler routes request to the appropriate handler according to the OpenAPI specification schema ID
func (a *App) APIModeMainHandler(ctx *fasthttp.RequestCtx) {

// handle panic
defer func() {
Expand All @@ -151,18 +152,18 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {

a.Log.WithFields(logrus.Fields{
"error": err,
"host": string(ctx.Request.Header.Host()),
"path": string(ctx.Path()),
"method": string(ctx.Request.Header.Method()),
"host": strconv.B2S(ctx.Request.Header.Host()),
"path": strconv.B2S(ctx.Path()),
"method": strconv.B2S(ctx.Request.Header.Method()),
"request_id": ctx.UserValue(web.RequestID),
}).Error("error while getting schema ID")

if err := web.RespondError(ctx, fasthttp.StatusInternalServerError, ""); err != nil {
a.Log.WithFields(logrus.Fields{
"error": err,
"host": string(ctx.Request.Header.Host()),
"path": string(ctx.Path()),
"method": string(ctx.Request.Header.Method()),
"host": strconv.B2S(ctx.Request.Header.Host()),
"path": strconv.B2S(ctx.Path()),
"method": strconv.B2S(ctx.Request.Header.Method()),
"request_id": ctx.UserValue(web.RequestID),
}).Error("error while sending response")
}
Expand All @@ -188,8 +189,8 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {

// handler not found in the OAS
if handler == nil {
keyValidationErrors := strconv2.Itoa(schemaID) + web.APIModePostfixValidationErrors
keyStatusCode := strconv2.Itoa(schemaID) + web.APIModePostfixStatusCode
keyValidationErrors := strconv2.Itoa(schemaID) + validator.APIModePostfixValidationErrors
keyStatusCode := strconv2.Itoa(schemaID) + validator.APIModePostfixStatusCode

// OPTIONS methods are passed if the passOPTIONS is set to true
if a.passOPTIONS == true && strconv.B2S(ctx.Method()) == fasthttp.MethodOptions {
Expand All @@ -210,7 +211,7 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {
"method": strconv.B2S(ctx.Request.Header.Method()),
"request_id": ctx.UserValue(web.RequestID),
}).Debug("Method or path were not found")
ctx.SetUserValue(keyValidationErrors, []*web.ValidationError{{Message: ErrMethodAndPathNotFound.Error(), Code: ErrCodeMethodAndPathNotFound, SchemaID: &schemaID}})
ctx.SetUserValue(keyValidationErrors, []*validator.ValidationError{{Message: validator.ErrMethodAndPathNotFound.Error(), Code: validator.ErrCodeMethodAndPathNotFound, SchemaID: &schemaID}})
ctx.SetUserValue(keyStatusCode, fasthttp.StatusForbidden)
continue
}
Expand All @@ -229,8 +230,8 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {
}
}

responseSummary := make([]*web.APIModeResponseSummary, 0, len(schemaIDs))
responseErrors := make([]*web.ValidationError, 0)
responseSummary := make([]*validator.ValidationResponseSummary, 0, len(schemaIDs))
responseErrors := make([]*validator.ValidationError, 0)

for i := 0; i < len(schemaIDs); i++ {

Expand All @@ -240,11 +241,11 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {
return
}

statusCode, ok := ctx.UserValue(strconv2.Itoa(schemaIDs[i]) + web.APIModePostfixStatusCode).(int)
statusCode, ok := ctx.UserValue(strconv2.Itoa(schemaIDs[i]) + validator.APIModePostfixStatusCode).(int)
if !ok {
// set summary for the schema ID in pass Options mode
if a.passOPTIONS && strconv.B2S(ctx.Method()) == fasthttp.MethodOptions {
responseSummary = append(responseSummary, &web.APIModeResponseSummary{
responseSummary = append(responseSummary, &validator.ValidationResponseSummary{
SchemaID: &schemaIDs[i],
StatusCode: &statusOK,
})
Expand All @@ -257,19 +258,19 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {
statusCode = fasthttp.StatusInternalServerError
}

responseSummary = append(responseSummary, &web.APIModeResponseSummary{
responseSummary = append(responseSummary, &validator.ValidationResponseSummary{
SchemaID: &schemaIDs[i],
StatusCode: &statusCode,
})

if validationErrors, ok := ctx.UserValue(strconv2.Itoa(schemaIDs[i]) + web.APIModePostfixValidationErrors).([]*web.ValidationError); ok && validationErrors != nil {
if validationErrors, ok := ctx.UserValue(strconv2.Itoa(schemaIDs[i]) + validator.APIModePostfixValidationErrors).([]*validator.ValidationError); ok && validationErrors != nil {
responseErrors = append(responseErrors, validationErrors...)
}
}

// Add schema IDs that were not found in the DB to the response
for i := 0; i < len(notFoundSchemaIDs); i++ {
responseSummary = append(responseSummary, &web.APIModeResponseSummary{
responseSummary = append(responseSummary, &validator.ValidationResponseSummary{
SchemaID: &notFoundSchemaIDs[i],
StatusCode: &statusInternalError,
})
Expand All @@ -283,12 +284,12 @@ func (a *App) APIModeRouteHandler(ctx *fasthttp.RequestCtx) {
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
}

if err := web.Respond(ctx, web.APIModeResponse{Summary: responseSummary, Errors: responseErrors}, fasthttp.StatusOK); err != nil {
if err := web.Respond(ctx, validator.ValidationResponse{Summary: responseSummary, Errors: responseErrors}, fasthttp.StatusOK); err != nil {
a.Log.WithFields(logrus.Fields{
"request_id": ctx.UserValue(web.RequestID),
"host": string(ctx.Request.Header.Host()),
"path": string(ctx.Path()),
"method": string(ctx.Request.Header.Method()),
"host": strconv.B2S(ctx.Request.Header.Host()),
"path": strconv.B2S(ctx.Path()),
"method": strconv.B2S(ctx.Request.Header.Method()),
"error": err,
}).Error("respond error")
}
Expand Down
94 changes: 94 additions & 0 deletions cmd/api-firewall/internal/handlers/api/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package api

import (
"runtime/debug"
strconv2 "strconv"

"github.com/savsgio/gotils/strconv"
"github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
"github.com/valyala/fastjson"
"github.com/wallarm/api-firewall/internal/config"
"github.com/wallarm/api-firewall/internal/platform/loader"
apiMode "github.com/wallarm/api-firewall/internal/platform/validator"
"github.com/wallarm/api-firewall/internal/platform/web"
"github.com/wallarm/api-firewall/pkg/APIMode/validator"
)

type RequestValidator struct {
CustomRoute *loader.CustomRoute
OpenAPIRouter *loader.Router
Log *logrus.Logger
Cfg *config.APIMode
ParserPool *fastjson.ParserPool
SchemaID int
}

// Handler validates request and respond with 200, 403 (with error) or 500 status code
func (s *RequestValidator) Handler(ctx *fasthttp.RequestCtx) error {

// handle panic
defer func() {
if r := recover(); r != nil {
s.Log.Errorf("panic: %v", r)

// Log the Go stack trace for this panic'd goroutine.
s.Log.Debugf("%s", debug.Stack())
return
}
}()

keyValidationErrors := strconv2.Itoa(s.SchemaID) + validator.APIModePostfixValidationErrors
keyStatusCode := strconv2.Itoa(s.SchemaID) + validator.APIModePostfixStatusCode

// Route not found
if s.CustomRoute == nil {
s.Log.WithFields(logrus.Fields{
"host": strconv.B2S(ctx.Request.Header.Host()),
"path": strconv.B2S(ctx.Path()),
"method": strconv.B2S(ctx.Request.Header.Method()),
"request_id": ctx.UserValue(web.RequestID),
}).Debug("Method or path were not found")
ctx.SetUserValue(keyValidationErrors, []*validator.ValidationError{{Message: validator.ErrMethodAndPathNotFound.Error(), Code: validator.ErrCodeMethodAndPathNotFound, SchemaID: &s.SchemaID}})
ctx.SetUserValue(keyStatusCode, fasthttp.StatusForbidden)
return nil
}

validationErrors, err := apiMode.APIModeValidateRequest(ctx, s.ParserPool, s.CustomRoute, s.Cfg.UnknownParametersDetection)
if err != nil {
s.Log.WithFields(logrus.Fields{
"error": err,
"host": strconv.B2S(ctx.Request.Header.Host()),
"path": strconv.B2S(ctx.Path()),
"method": strconv.B2S(ctx.Request.Header.Method()),
"request_id": ctx.UserValue(web.RequestID),
}).Error("request validation error")
ctx.SetUserValue(keyStatusCode, fasthttp.StatusInternalServerError)
return nil
}

// Respond 403 with errors
if len(validationErrors) > 0 {
// add schema IDs to the validation error messages
for _, r := range validationErrors {
r.SchemaID = &s.SchemaID
r.SchemaVersion = s.OpenAPIRouter.SchemaVersion
}

s.Log.WithFields(logrus.Fields{
"error": validationErrors,
"host": strconv.B2S(ctx.Request.Header.Host()),
"path": strconv.B2S(ctx.Path()),
"method": strconv.B2S(ctx.Request.Header.Method()),
"request_id": ctx.UserValue(web.RequestID),
}).Debug("request validation error")

ctx.SetUserValue(keyValidationErrors, validationErrors)
ctx.SetUserValue(keyStatusCode, fasthttp.StatusForbidden)
return nil
}

// request successfully validated
ctx.SetUserValue(keyStatusCode, fasthttp.StatusOK)
return nil
}
Loading

0 comments on commit dbe4796

Please sign in to comment.