Merge pull request 'feat: add rate limiting to the API' (#21) from rate-limiting into master

Reviewed-on: https://git.freecumextremist.com/grumbulon/pomme/pulls/21
This commit is contained in:
grumbulon 2023-01-27 13:24:40 +00:00
commit 6cd9416375
9 changed files with 126 additions and 19 deletions

View file

@ -19,7 +19,7 @@ const docTemplate = `{
"paths": {
"/api/login": {
"post": {
"description": "login",
"description": "login to Pomme\nRate limited: 5 requests every 5 second",
"consumes": [
"application/json"
],
@ -29,7 +29,7 @@ const docTemplate = `{
"tags": [
"accounts"
],
"summary": "auth a regular user",
"summary": "authenticate as a regular user",
"parameters": [
{
"type": "string",
@ -69,7 +69,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "parse your zonefile -- you must specify \"Bearer\" before entering your token",
"description": "parse your zonefile\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token",
"consumes": [
"multipart/form-data"
],
@ -119,7 +119,7 @@ const docTemplate = `{
"Bearer": []
}
],
"description": "upload a file -- you must specify \"Bearer\" before entering your token",
"description": "upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token",
"consumes": [
"multipart/form-data"
],

View file

@ -10,7 +10,7 @@
"paths": {
"/api/login": {
"post": {
"description": "login",
"description": "login to Pomme\nRate limited: 5 requests every 5 second",
"consumes": [
"application/json"
],
@ -20,7 +20,7 @@
"tags": [
"accounts"
],
"summary": "auth a regular user",
"summary": "authenticate as a regular user",
"parameters": [
{
"type": "string",
@ -60,7 +60,7 @@
"Bearer": []
}
],
"description": "parse your zonefile -- you must specify \"Bearer\" before entering your token",
"description": "parse your zonefile\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token",
"consumes": [
"multipart/form-data"
],
@ -110,7 +110,7 @@
"Bearer": []
}
],
"description": "upload a file -- you must specify \"Bearer\" before entering your token",
"description": "upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user.\nRate limited: 10 requests every 10 second\nyou must specify \"Bearer\" before entering your token",
"consumes": [
"multipart/form-data"
],

View file

@ -37,7 +37,9 @@ paths:
post:
consumes:
- application/json
description: login
description: |-
login to Pomme
Rate limited: 5 requests every 5 second
parameters:
- description: Username
in: query
@ -60,15 +62,17 @@ paths:
description: Unauthorized
schema:
$ref: '#/definitions/api.httpError'
summary: auth a regular user
summary: authenticate as a regular user
tags:
- accounts
/api/parse:
post:
consumes:
- multipart/form-data
description: parse your zonefile -- you must specify "Bearer" before entering
your token
description: |-
parse your zonefile
Rate limited: 10 requests every 10 second
you must specify "Bearer" before entering your token
parameters:
- description: Zonefile name
in: query
@ -100,8 +104,10 @@ paths:
post:
consumes:
- multipart/form-data
description: upload a file -- you must specify "Bearer" before entering your
token
description: |-
upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user.
Rate limited: 10 requests every 10 second
you must specify "Bearer" before entering your token
parameters:
- description: Zonefile to upload
in: formData

2
go.mod
View file

@ -5,6 +5,7 @@ go 1.19
require (
github.com/glebarez/sqlite v1.6.0
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/httprate v0.7.1
github.com/go-chi/jwtauth/v5 v5.1.0
github.com/miekg/dns v1.1.50
github.com/swaggo/swag v1.8.9
@ -14,6 +15,7 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect

4
go.sum
View file

@ -2,6 +2,8 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -23,6 +25,8 @@ github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/httplog v0.2.5 h1:S02eG9NTrB/9kk3Q3RA3F6CR2b+v8WzB8IxK+zq3dBo=
github.com/go-chi/httplog v0.2.5/go.mod h1:/pIXuFSrOdc5heKIJRA5Q2mW7cZCI2RySqFZNFoZjKg=
github.com/go-chi/httprate v0.7.1 h1:d5kXARdms2PREQfU4pHvq44S6hJ1hPu4OXLeBKmCKWs=
github.com/go-chi/httprate v0.7.1/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=

View file

@ -2,13 +2,16 @@ package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"git.freecumextremist.com/grumbulon/pomme/internal"
"git.freecumextremist.com/grumbulon/pomme/internal/db"
"github.com/go-chi/chi/v5"
"github.com/go-chi/httplog"
"github.com/go-chi/httprate"
"github.com/go-chi/jwtauth/v5"
)
@ -56,6 +59,25 @@ func API() http.Handler {
// Protected routes
api.Group(func(api chi.Router) {
api.Use(httprate.Limit(
10, // requests
10*time.Second, // per duration
httprate.WithKeyFuncs(httprate.KeyByIP, httprate.KeyByEndpoint),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
err := json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusTooManyRequests,
Message: "API rate limit exceded",
})
if err != nil {
internalServerError(w, "internal server error")
return
}
}),
))
api.Use(jwtauth.Verifier(tokenAuth))
api.Use(jwtauth.Authenticator)
@ -65,6 +87,25 @@ func API() http.Handler {
// Open routes
api.Group(func(api chi.Router) {
api.Use(httprate.Limit(
5, // requests
5*time.Second, // per duration
httprate.WithKeyFuncs(httprate.KeyByIP, httprate.KeyByEndpoint),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
err := json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusTooManyRequests,
Message: "API rate limit exceded",
})
if err != nil {
internalServerError(w, "internal server error")
return
}
}),
))
api.Use(setDBMiddleware)
api.With(setDBMiddleware).Post("/create", NewUser)
api.With(setDBMiddleware).Post("/login", Login)

View file

@ -31,8 +31,11 @@ type httpInternalServerError struct {
// Auth godoc
//
// @Summary auth a regular user
// @Description login
// @Summary authenticate as a regular user
// @Description login to Pomme
//
// @Description Rate limited: 5 requests every 5 second
//
// @Tags accounts
// @Accept json
// @Produce json

View file

@ -2,6 +2,7 @@ package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
@ -34,7 +35,11 @@ type Zone struct {
// Upload godoc
//
// @Summary upload a zonefile
// @Description upload a file -- you must specify "Bearer" before entering your token
// @Description upload takes files from the user and stores it locally to be parsed. Uploads are associated with a specific user.
//
// @Description Rate limited: 10 requests every 10 second
// @Description you must specify "Bearer" before entering your token
//
// @Tags DNS
// @Accept mpfd
// @Produce json
@ -62,6 +67,13 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) {
defer file.Close() //nolint: errcheck
ok := validateContentType(file)
if !ok {
http.Error(w, "file must be text/plain", http.StatusUnsupportedMediaType)
return
}
name := strings.Split(header.Filename, ".")
if _, err = io.Copy(&buf, file); err != nil {
@ -71,7 +83,7 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) {
}
if err = util.MakeLocal(name[0], claims["username"].(string), buf); err != nil {
internalServerError(w, "internal server error")
internalServerError(w, err.Error())
return
}
@ -93,12 +105,30 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) {
})
buf.Reset()
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusCreated,
Message: "Successfully uploaded zonefile",
})
if err != nil {
internalServerError(w, "internal server error")
return
}
}
// Parse godoc
//
// @Summary parse your zonefile
// @Description parse your zonefile -- you must specify "Bearer" before entering your token
// @Description parse your zonefile
//
// @Description Rate limited: 10 requests every 10 second
// @Description you must specify "Bearer" before entering your token
//
// @Tags DNS
// @Accept mpfd
// @Produce json
@ -189,3 +219,20 @@ func (zone *ZoneRequest) Parse() error {
return nil
}
func validateContentType(file io.Reader) bool {
bytes, err := io.ReadAll(file)
if err != nil {
return false
}
mimeType := http.DetectContentType(bytes)
mime := strings.Contains(mimeType, "text/plain")
switch mime {
case true:
return true
default:
return false
}
}

View file

@ -7,6 +7,10 @@ import (
)
func MakeLocal(filename, username string, buf bytes.Buffer) error {
if _, err := os.Stat(fmt.Sprintf("/tmp/tmpfile-%s-%s", filename, username)); !os.IsNotExist(err) {
return fmt.Errorf("file %s already exists: %w", filename, err)
}
defer buf.Reset()
f, err := os.Create("/tmp/tmpfile-" + filename + "-" + username) //nolint: gosec