Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add rate limiter in web server #26

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion W/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (
"github.com/tdewolff/minify"
"github.com/tdewolff/minify/css"
"github.com/tdewolff/minify/js"
limiter "github.com/ulule/limiter/v3"
mfasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/ulule/limiter/v3/drivers/store/memory"
"github.com/valyala/fasthttp"
)

Expand Down Expand Up @@ -46,6 +49,15 @@ type Engine struct {
CreatedAt time.Time
// assets <script and <link as string
Assets string
// "S": second,
// "M": minute,
// "H": hour,
// "D": day
//
// 300 reqs/minute: "300-M"
//
// See example https://github.com/ulule/limiter
RateLimitFormat string
}

func (engine *Engine) Log(message string) {
Expand Down Expand Up @@ -173,11 +185,35 @@ func (engine *Engine) MinifyAssets() {

// start the server
func (engine *Engine) StartServer(addressPort string) {
rateLimitFormat := `300-M` // 300 reqs/minute
if engine.RateLimitFormat != `` {
rateLimitFormat = engine.RateLimitFormat
}
rate, errLimiter := limiter.NewRateFromFormatted(rateLimitFormat)
var isErrLimiter bool
if errLimiter != nil {
L.IsError(errLimiter, `Failed to add rate limiter`)
isErrLimiter = true
}
limitStore := memory.NewStore()
instance := limiter.New(
limitStore,
rate,
limiter.WithTrustForwardHeader(true),
)
middleware := mfasthttp.NewMiddleware(instance)
engine.MinifyAssets()
L.LOG.Notice(engine.Name + ` ` + S.IfElse(engine.DebugMode, `[DEVELOPMENT]`, `[PRODUCTION]`) + ` server with ` + I.ToStr(len(Routes)) + ` route(s) on ` + addressPort +
"\n Ajax Error Log: " + engine.LogPath +
"\n Work Directory: " + engine.BaseDir)
err := fasthttp.ListenAndServe(addressPort, engine.Router.Handler)

var err error
if isErrLimiter {
// not using rate limiter
err = fasthttp.ListenAndServe(addressPort, engine.Router.Handler)
} else {
err = fasthttp.ListenAndServe(addressPort, middleware.Handle(engine.Router.Handler))
}
L.IsError(err, `Failed to listen on `+addressPort)
engine.Logger.Close()
}
Expand Down
23 changes: 23 additions & 0 deletions W2/internal/example2/openapi-conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"openapi": "3.0.3",
"info": {
"title": "Example2",
"description": "API Spec for Example2",
"termsOfService": "https://example2.com/term-of-service",
"contact": {
"name": "API Support",
"email": "[email protected]"
},
"license": {
"name": "MIT License",
"url": "https://en.wikipedia.org/wiki/MIT_License"
},
"version": "1.0.0"
},
"servers": [
{
"url": "https://example2.com",
"description": "Example2 Server"
}
]
}
194 changes: 187 additions & 7 deletions W2/internal/example2/presentation/1_codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"testing"
"time"

"github.com/goccy/go-json"
"github.com/kokizzu/gotro/L"
"github.com/kokizzu/gotro/M"
"github.com/kokizzu/gotro/S"
)

Expand All @@ -33,6 +36,7 @@ func BenchmarkGenerateViews(b *testing.B) {
JsApiGenFile: "../svelte/jsApi.GEN.js",
CmdRunGenFile: "./cmd_run.GEN.go",
WebViewGenFile: "./web_view.GEN.go",
SwaggerGenFile: "../svelte/swagger.json",
}
p.StartCodegen()

Expand All @@ -43,6 +47,10 @@ const NL = "\n"
const TAB = "\t"
const generatedComment = NL + `// Code generated by 1_codegen_test.go DO NOT EDIT.` + NL

func tabs(total int) string {
return S.Repeat(TAB, total)
}

type codegen struct {
ModelDir string // ../model
models models
Expand All @@ -58,6 +66,7 @@ type codegen struct {
CmdRunGenFile string // cmd_run.GEN.go
JsApiGenFile string // jsApi.GEN.js
WebViewGenFile string // web_view.GEN.go
SwaggerGenFile string // swagger.json

ProjectName string // based on go.mod
}
Expand Down Expand Up @@ -348,6 +357,7 @@ func (c *codegen) StartCodegen() {
c.GenerateApiRoutesFile()
c.GenerateJsApiFile()
c.GenerateCmdRunFile()
c.GenerateSwaggerFile()

// parse svelte files
start = time.Now()
Expand All @@ -362,6 +372,7 @@ func (c *codegen) StartCodegen() {
}
return nil
})
L.IsError(err, `filepath.Walk`)
L.TimeTrack(start, `parsing svelte dir`)

c.GenerateWebRouteFile()
Expand All @@ -377,7 +388,7 @@ func (d *domains) parseDomainFile(path string) {
ast.Walk(d, p)
}

func (d *domains) eachSortedHandler(eachFunc func(name string, handler tmethod)) {
func (d *domains) eachSortedHandler(eachFunc func(name string, handler tmethod, isEnd bool)) {
// sort handlers
handlers := d.handlers.byRcvDotMethod
handlerNames := make([]string, 0, len(handlers))
Expand All @@ -393,8 +404,12 @@ func (d *domains) eachSortedHandler(eachFunc func(name string, handler tmethod))
}
sort.Strings(handlerNames)

for _, name := range handlerNames {
eachFunc(name, byName[name])
for idx, name := range handlerNames {
end := false
if idx == len(handlerNames)-1 {
end = true
}
eachFunc(name, byName[name], end)
}
}

Expand All @@ -410,7 +425,7 @@ import (

var allCommands = []string{
`)
c.domains.eachSortedHandler(func(name string, handler tmethod) {
c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) {
b.WriteString(TAB + `domain.` + name + `Action,` + NL)
})
b.WriteString(`}` + NL)
Expand All @@ -433,7 +448,7 @@ import (

func ApiRoutes(fw *fiber.App, d *domain.Domain) {
`)
c.domains.eachSortedHandler(func(name string, handler tmethod) {
c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) {
b.WriteString(`
// ` + name + `
fw.Post("/"+domain.` + name + `Action, func(c *fiber.Ctx) error {
Expand Down Expand Up @@ -482,7 +497,7 @@ function wrapOk( cb ) {
`)
b.WriteString(generatedComment)

c.domains.eachSortedHandler(func(name string, handler tmethod) {
c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) {
fields := c.domains.types.byName[handler.In].fields
c.jsObject(&b, name+`In`, fields, 0)

Expand Down Expand Up @@ -608,7 +623,7 @@ import (

func cmdRun(b *domain.Domain, action string, payload []byte) {
switch action {`)
c.domains.eachSortedHandler(func(name string, handler tmethod) {
c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) {
b.WriteString(`
case domain.` + name + `Action:
in := domain.` + name + `In{}
Expand Down Expand Up @@ -668,3 +683,168 @@ func (v *Views) Render` + cacheName + `(c *fiber.Ctx, m M.SX) error {

L.CreateFile(c.WebViewGenFile, b.String())
}

func (c *codegen) GenerateSwaggerFile() {
defer L.TimeTrack(time.Now(), `GenerateSwaggerFile`)

confJson, err := os.ReadFile("../openapi-conf.json")
L.PanicIf(err, `error read OpenAPI config file`)

var jsonMap M.SX
err = json.Unmarshal(confJson, &jsonMap)
L.PanicIf(err, `error unmarshall OpenAPI config`)

jsonString, err := json.MarshalIndent(jsonMap, "", " ")
L.PanicIf(err, `marshall indent`)

jsonString = jsonString[1 : len(jsonString)-1]
newJsonString := string(jsonString)
lines := strings.Split(newJsonString, NL)
lines = lines[:len(lines)-1]

newJsonString = strings.Join(lines, NL)

b := bytes.Buffer{}

b.WriteString(`{` + newJsonString + `,
"paths": {
`)

c.domains.eachSortedHandler(func(name string, handler tmethod, isEnd bool) {
coma := `,`
if isEnd {
coma = ``
}
for _, a := range allCommands {
parts := strings.Split(a, "/")
for i, part := range parts {
parts[i] = strings.Title(part)
}
newName := strings.Join(parts, "")
if name == newName {
b.WriteString(TAB + TAB + `"/` + a + `": {` + NL)
}
}
b.WriteString(TAB + TAB + TAB + `"post": {
"security": [{
"CookieAuth": []
}],
"tags": ["API"],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
`)

fieldsIn := c.domains.types.byName[handler.In].fields
c.swaggerRequest(&b, name+`In`, fieldsIn, 0)
b.WriteString(tabs(8) + `}
}
}
}
},`)
b.WriteString(`
"responses": {
"200": {
"description": "` + name + `",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
`)
fields := c.domains.types.byName[handler.Out].fields
c.swaggerResponses(&b, name+`Out`, fields, 2)
b.WriteString(TAB + TAB + TAB + TAB + TAB + TAB + TAB + TAB + TAB + `}
}
}
}
}
}
}
}` + coma + NL)
})

b.WriteString(TAB + `},
"components": {
"securitySchemes": {
"CookieAuth": {
"type": "apiKey",
"in": "cookie",
"name": "auth",
"description": "Authentication for example2"
}
}
}
}
`)

L.CreateFile(c.SwaggerGenFile, b.String())
}

func (c *codegen) swaggerFields(content *bytes.Buffer, fields []tfield, indent int) {
for idx, field := range fields {
end := false
if idx == len(fields)-1 {
end = true
}
c.swaggerField(content, field, indent, end)
}
}

func (c *codegen) swaggerField(b *bytes.Buffer, field tfield, indent int, isEnd bool) {
coma := `,`
if isEnd {
coma = ``
}
t := field.Type
// skip unecessary fields
switch t {
case `Tt.Adapter`, `Ch.Adapter`:
return
}

spaces := S.Repeat(TAB, indent*2)
b.WriteString(tabs(6) + spaces + `"` + S.CamelCase(field.Name) + `": `)

switch t {
case `int`, `uint8`, `uint16`, `uint32`, `uint64`, `int8`, `int16`, `int32`, `int64`, `float32`, `float64`:
b.WriteString(`{
` + tabs(8) + spaces + `"type": "number"
` + tabs(6) + spaces + `}` + coma)
case `string`:
b.WriteString(`{
` + tabs(8) + spaces + `"type": "string"
` + tabs(6) + spaces + `}` + coma)
case `bool`:
b.WriteString(`{
` + tabs(6) + TAB + spaces + `"type": "boolean"
` + tabs(6) + spaces + `}` + coma)
case `[]string`:
b.WriteString(`{
` + tabs(8) + spaces + `"type": "array",
` + tabs(9) + spaces + `"items": {
` + tabs(10) + spaces + `"type": "string"
` + tabs(9) + spaces + `}
` + tabs(8) + spaces + `}` + coma)
default:
ty := c.models.types.byName[t]
b.WriteString(`{
` + tabs(6) + spaces + `"type": "object",
` + tabs(6) + spaces + `"properties": {` + NL)
c.swaggerFields(b, ty.fields, indent+1)
b.WriteString(tabs(8) + spaces + `}
` + tabs(6) + spaces + `}` + coma)
}
b.WriteString(NL)
}

func (c *codegen) swaggerResponses(b *bytes.Buffer, name string, fields []tfield, indent int) {
c.swaggerFields(b, fields, indent)
}

func (c *codegen) swaggerRequest(b *bytes.Buffer, name string, fields []tfield, indent int) {
c.swaggerFields(b, fields, indent)
}
7 changes: 7 additions & 0 deletions W2/internal/example2/presentation/web_static.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ func (w *WebServer) WebStatic(fw *fiber.App, d *domain.Domain) {
})
})

fw.Get(`/apidocs`, func(ctx *fiber.Ctx) error {
return views.RenderApidocsIndex(ctx, M.SX{
`title`: `Example2 API Docs`,
`description`: `Restful API Documentation of Example2`,
})
})

}

func userInfoFromContext(c *fiber.Ctx, d *domain.Domain) (domain.UserProfileIn, *rqAuth.Users, M.SB) {
Expand Down
Loading