From 01cb5f669035f37525f8f9f40d2d9ce5b5f50d66 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Sat, 7 Nov 2020 15:09:00 +0000 Subject: [PATCH 01/12] Added defer to close open statements and rows to avoid leaking SQL connections --- database/config.go | 21 +++++++------------ database/connect.go | 14 ++++++------- ...rrorstdtreatment.go => writeerrorreply.go} | 5 ++++- model/error.go | 3 ++- model/keywords.go | 1 + model/playlist_keyword.go | 1 + model/playlists.go | 1 + model/videos.go | 1 + 8 files changed, 24 insertions(+), 23 deletions(-) rename handler/{errorstdtreatment.go => writeerrorreply.go} (78%) diff --git a/database/config.go b/database/config.go index 5ef9e5d..a5784af 100644 --- a/database/config.go +++ b/database/config.go @@ -1,19 +1,12 @@ package database var ( - // DbHost is the Postgres Host - DbHost = "localhost" - // DbUser is the Postgres username - DbUser = "postgres" - // DbPassword is the password to db - DbPassword = "postgres" - // DbDatabase is the name of the database - DbDatabase = "collabyt" - // DbPort is the post open in the postgres server - DbPort = "5432" + dbHost = "localhost" + dbUser = "postgres" + dbPassword = "postgres" + dbDatabase = "collabyt" + dbPort = "5432" - // DbSsl is the should ssl be active or not - DbSsl = "disable" - // DbSource is the Source of the database - DbSource = "postgres" + dbSsl = "disable" + dbSource = "postgres" ) diff --git a/database/connect.go b/database/connect.go index d6ba9a3..d349e49 100644 --- a/database/connect.go +++ b/database/connect.go @@ -18,14 +18,14 @@ var ( func Connect() { dbInfo := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", - DbHost, - DbPort, - DbUser, - DbPassword, - DbDatabase, - DbSsl, + dbHost, + dbPort, + dbUser, + dbPassword, + dbDatabase, + dbSsl, ) - db, err := sql.Open(DbSource, dbInfo) + db, err := sql.Open(dbSource, dbInfo) if err != nil { panic(err) } diff --git a/handler/errorstdtreatment.go b/handler/writeerrorreply.go similarity index 78% rename from handler/errorstdtreatment.go rename to handler/writeerrorreply.go index 172be52..cf6bb0a 100644 --- a/handler/errorstdtreatment.go +++ b/handler/writeerrorreply.go @@ -12,7 +12,10 @@ import ( func WriteErrorReply(w http.ResponseWriter, httpCode int) { w.WriteHeader(httpCode) errRet, _ := json.Marshal( - model.Error{Description: http.StatusText(httpCode)}, + model.Error{ + ErrorCode: httpCode, + Description: http.StatusText(httpCode), + }, ) w.Write(errRet) } diff --git a/model/error.go b/model/error.go index cd9af65..cf29a12 100644 --- a/model/error.go +++ b/model/error.go @@ -3,5 +3,6 @@ package model // Error represent a instance of a error that ocurred at some point in the // application. type Error struct { - Description string `json:"Description"` + ErrorCode int `json:"code"` + Description string `json:"description"` } diff --git a/model/keywords.go b/model/keywords.go index 1ba8143..e493f60 100644 --- a/model/keywords.go +++ b/model/keywords.go @@ -75,6 +75,7 @@ func GetKeywordsByPlaylistID(db *sql.DB, playlistID int) ([]Keyword, error) { if err != nil { return []Keyword{}, err } + defer kRows.Close() var ks []Keyword for kRows.Next() { var k Keyword diff --git a/model/playlist_keyword.go b/model/playlist_keyword.go index b4ea22c..653b4ec 100644 --- a/model/playlist_keyword.go +++ b/model/playlist_keyword.go @@ -17,6 +17,7 @@ func CreateKeywordsRelation(db *sql.DB, playlistID int, wordList []Keyword) erro if err != nil { return err } + defer SQLStatement.Close() _, err = SQLStatement.Exec() if err != nil { return err diff --git a/model/playlists.go b/model/playlists.go index cadecc1..0b1823c 100644 --- a/model/playlists.go +++ b/model/playlists.go @@ -26,6 +26,7 @@ func GetPublicPlaylistsByLimitAndOffset(db *sql.DB, limit int, offset int) ([]Pl if err != nil { return []Playlist{}, err } + defer pRows.Close() var ps []Playlist for pRows.Next() { var p Playlist diff --git a/model/videos.go b/model/videos.go index a86c12b..38d77f9 100644 --- a/model/videos.go +++ b/model/videos.go @@ -17,6 +17,7 @@ func GetVideosByPlaylistID(db *sql.DB, playlistID int) ([]Video, error) { if err != nil { return []Video{}, err } + defer vRows.Close() var vs []Video for vRows.Next() { var v Video From 92d3d382f0677f7b97b23c9c13e9b2a1c275d52d Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Sun, 8 Nov 2020 15:43:45 +0000 Subject: [PATCH 02/12] Added basic minimal dockerfile --- Dockerfile | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..568d87b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:latest + +LABEL maintainer="Gustavo H. M. Silva " + +WORKDIR /app + +COPY go.mod . +COPY go.sum . +RUN go mod download +COPY . . + +ENV PORT 8080 +ENV DBHOST 0.0.0.0 +ENV DBUSER postgres +ENV DBPASSWORD postgres +ENV DBDATABASE collabyt +ENV DBPORT 5432 +ENV DBSSL disable +ENV DBSOURCE postgres + +RUN go build +CMD ["./Backend"] \ No newline at end of file From 4b83f0ffef712fbb357d6c3da37b387f2c298ef7 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:39:50 +0000 Subject: [PATCH 03/12] Refactored Model CreadeKeyword to no longer try to search existing keyword before creating a new one. --- model/keyword.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/model/keyword.go b/model/keyword.go index 7e9b74c..d116fc9 100644 --- a/model/keyword.go +++ b/model/keyword.go @@ -14,11 +14,6 @@ type Keyword struct { //already. If exists, returns the word itself from the database. func CreateKeyword(db *sql.DB, word string) (Keyword, error) { var k Keyword - sRow := db.QueryRow(`SELECT id, word FROM keyword WHERE word = $1;`, word) - err := sRow.Scan(&k.ID, &k.Word) - if err == nil { - return k, nil - } iRow := db.QueryRow( `INSERT INTO public.keyword (word) @@ -27,11 +22,8 @@ func CreateKeyword(db *sql.DB, word string) (Keyword, error) { RETURNING id, word;`, word, ) - err = iRow.Scan(&k.ID, &k.Word) - if err != nil { - return Keyword{}, err - } - return k, nil + err := iRow.Scan(&k.ID, &k.Word) + return k, err } // GetKeywordByID returns a single keyword based in it's id. From d9dcbbd15a0321deafdc6be0ff023a32904ecf43 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:40:50 +0000 Subject: [PATCH 04/12] Finished turning limiter package into a independent and parameterized package --- limiter/limiter.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/limiter/limiter.go b/limiter/limiter.go index 2a66b07..c1bb2f0 100644 --- a/limiter/limiter.go +++ b/limiter/limiter.go @@ -21,14 +21,16 @@ type Visitor struct { type Catalog struct { visitors map[string]*Visitor mu sync.RWMutex + rate rate.Limit + maxBurst int } -const eraseTimeLimit = 60 - // NewCatalog initialize the visitors map inside the type Catalog -func NewCatalog(size int) *Catalog { +func NewCatalog(size, r, maxBurst int) *Catalog { var cat Catalog - cat.visitors = make(map[string]*Visitor) + cat.visitors = make(map[string]*Visitor, size) + cat.rate = rate.Limit(r) + cat.maxBurst = maxBurst return &cat } @@ -37,7 +39,7 @@ func getVisitor(cat *Catalog, ip string) *rate.Limiter { v, exist := cat.visitors[ip] cat.mu.RUnlock() if !exist { - limiter := rate.NewLimiter(1, 3) + limiter := rate.NewLimiter(cat.rate, cat.maxBurst) cat.mu.Lock() //read cat.visitors[ip] = &Visitor{limiter, time.Now()} cat.mu.Unlock() @@ -69,13 +71,12 @@ func Limit(cat *Catalog, next http.Handler) http.Handler { } // CleanupVisitors remove unused IP's from the visitors -func CleanupVisitors(cat *Catalog) { +func CleanupVisitors(cat *Catalog, limit time.Duration) { for { time.Sleep(time.Minute) - cat.mu.Lock() for ip, v := range cat.visitors { - if time.Since(v.lastSeen) > time.Hour { + if time.Since(v.lastSeen) > limit { delete(cat.visitors, ip) } } From 601a6f6eefbdf05acff0835c2ee59b0389fd8732 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:41:16 +0000 Subject: [PATCH 05/12] New auxiliary functions for handlers --- handler/cookies.go | 72 +++++++++++++++++++++++++++++++++++ handler/generatenewsession.go | 21 ++++++++++ 2 files changed, 93 insertions(+) create mode 100644 handler/cookies.go create mode 100644 handler/generatenewsession.go diff --git a/handler/cookies.go b/handler/cookies.go new file mode 100644 index 0000000..5da6680 --- /dev/null +++ b/handler/cookies.go @@ -0,0 +1,72 @@ +package handler + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/collabyt/Backend/database" + "github.com/collabyt/Backend/model" + "golang.org/x/crypto/bcrypt" +) + +func hasCookie(cook *http.Cookie) ([]byte, error) { + s, err := model.GetSessionBySessionID(database.DB, cook.Value) + if err != nil { + return []byte{}, err + } + var es model.Session + if s == es { + cook.MaxAge = -1 + } + playlist, err := model.GetPlaylistByPublicID(database.DB, cook.Name) + if err != nil { + return []byte{}, err + } + jp, _ := json.Marshal(playlist) + return jp, nil +} + +func noCookie(a model.Auth) (http.Cookie, int, error) { + if a.PublicID == "" { + return http.Cookie{}, + http.StatusUnauthorized, + fmt.Errorf("Invalid Public ID") + } + ps, err := model.GetPlaylistByPublicID(database.DB, a.PublicID) + if err != nil { + return http.Cookie{}, + http.StatusUnauthorized, err + } + err = bcrypt.CompareHashAndPassword( + []byte(ps.Passphrase), + []byte(a.Passphrase), + ) + if err != nil { + return http.Cookie{}, + http.StatusUnauthorized, + fmt.Errorf("Wrong password, access denied") + } + s, err := generateNewSession(12, ps.ID) + if err != nil { + return http.Cookie{}, + http.StatusInternalServerError, + fmt.Errorf("Something wrong happened") + } + + err = model.CreateSession(database.DB, s) + if err != nil { + return http.Cookie{}, + http.StatusInternalServerError, + fmt.Errorf("Could not create session in the database") + } + nc := http.Cookie{ + Name: a.PublicID, + Value: s.SessionID, + Expires: time.Now().Add(time.Hour * 360), + } + return nc, + http.StatusOK, + nil +} diff --git a/handler/generatenewsession.go b/handler/generatenewsession.go new file mode 100644 index 0000000..65d5e23 --- /dev/null +++ b/handler/generatenewsession.go @@ -0,0 +1,21 @@ +package handler + +import ( + "crypto/rand" + "encoding/base64" + + "github.com/collabyt/Backend/model" +) + +func generateNewSession(size int, id int) (model.Session, error) { + randomBytes := make([]byte, size) + _, err := rand.Read(randomBytes) + var es model.Session + if err != nil { + return es, err + } + return model.Session{ + PlaylistID: id, + SessionID: base64.URLEncoding.EncodeToString(randomBytes), + }, nil +} From 13bc06a568048a7debb6929800c65316497cd044 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:42:09 +0000 Subject: [PATCH 06/12] Removed internal helper functions to specific files in the handler package --- handler/requestaccesstoplaylist.go | 78 ------------------------------ 1 file changed, 78 deletions(-) diff --git a/handler/requestaccesstoplaylist.go b/handler/requestaccesstoplaylist.go index 78eafff..c21b025 100644 --- a/handler/requestaccesstoplaylist.go +++ b/handler/requestaccesstoplaylist.go @@ -1,16 +1,11 @@ package handler import ( - "crypto/rand" - "encoding/base64" "encoding/json" - "fmt" "net/http" - "time" "github.com/collabyt/Backend/database" "github.com/collabyt/Backend/model" - "golang.org/x/crypto/bcrypt" ) // RequestAccessToPlaylist authorize or deny access to a given playlist. @@ -46,76 +41,3 @@ func RequestAccessToPlaylist(w http.ResponseWriter, r *http.Request) { } w.Write(jsonPlaylist) } - -func hasCookie(cook *http.Cookie) ([]byte, error) { - s, err := model.GetSessionBySessionID(database.DB, cook.Value) - if err != nil { - return []byte{}, err - } - var es model.Session - if s == es { - cook.MaxAge = -1 - } - playlist, err := model.GetPlaylistByPublicID(database.DB, cook.Name) - if err != nil { - return []byte{}, err - } - jp, _ := json.Marshal(playlist) - return jp, nil -} - -func noCookie(a model.Auth) (http.Cookie, int, error) { - if a.PublicID == "" { - return http.Cookie{}, - http.StatusUnauthorized, - fmt.Errorf("Invalid Public ID") - } - ps, err := model.GetPlaylistByPublicID(database.DB, a.PublicID) - if err != nil { - return http.Cookie{}, - http.StatusUnauthorized, err - } - err = bcrypt.CompareHashAndPassword( - []byte(ps.Passphrase), - []byte(a.Passphrase), - ) - if err != nil { - return http.Cookie{}, - http.StatusUnauthorized, - fmt.Errorf("Wrong password, access denied") - } - s, err := generateNewSession(12, ps.ID) - if err != nil { - return http.Cookie{}, - http.StatusInternalServerError, - fmt.Errorf("Something wrong happened") - } - - err = model.CreateSession(database.DB, s) - if err != nil { - return http.Cookie{}, - http.StatusInternalServerError, - fmt.Errorf("Could not create session in the database") - } - nc := http.Cookie{ - Name: a.PublicID, - Value: s.SessionID, - Expires: time.Now().Add(time.Hour * 360), - } - return nc, - http.StatusOK, - nil -} - -func generateNewSession(size int, id int) (model.Session, error) { - randomBytes := make([]byte, size) - _, err := rand.Read(randomBytes) - var es model.Session - if err != nil { - return es, err - } - return model.Session{ - PlaylistID: id, - SessionID: base64.URLEncoding.EncodeToString(randomBytes), - }, nil -} From 84769da1945746ec0f212be6299e81cc801e62c6 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:42:33 +0000 Subject: [PATCH 07/12] Changed comments + added new parameters to initialize limiter package in the application --- main.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index bd57d9e..040486b 100644 --- a/main.go +++ b/main.go @@ -19,23 +19,25 @@ func main() { r := mux.NewRouter() // Playlist operations - r.HandleFunc("/api/v1/playlists", handler.CreatePlaylist).Methods("POST") // DONE - r.HandleFunc("/api/v1/playlists/{PublicID}", handler.GetPlaylistByPublicID).Methods("GET") // DONE - r.HandleFunc("/api/v1/playlists", handler.GetPublicPlaylists).Methods("GET") // DONE + r.HandleFunc("/api/v1/playlists", handler.CreatePlaylist).Methods("POST") + r.HandleFunc("/api/v1/playlists/{PublicID}", handler.GetPlaylistByPublicID).Methods("GET") + r.HandleFunc("/api/v1/playlists", handler.GetPublicPlaylists).Methods("GET") // Private Playlist operations - r.HandleFunc("/api/v1/auth/{PublicID}", handler.RequestAccessToPlaylist).Methods("POST") // DONE - r.HandleFunc("/api/v1/exit/{PublicID}", handler.DeauthorizeToPlaylist).Methods("GET") // DONE + r.HandleFunc("/api/v1/auth/{PublicID}", handler.RequestAccessToPlaylist).Methods("POST") + r.HandleFunc("/api/v1/exit/{PublicID}", handler.DeauthorizeToPlaylist).Methods("GET") // Keyword operations - r.HandleFunc("/api/v1/keywords", handler.CreateKeyword).Methods("POST") // DONE - r.HandleFunc("/api/v1/keywords/", handler.GetKeywords).Methods("GET") // DONE + r.HandleFunc("/api/v1/keywords", handler.CreateKeyword).Methods("POST") + r.HandleFunc("/api/v1/keywords", handler.GetKeywords).Methods("GET") // Video operations - r.HandleFunc("/api/v1/playlists/{PublicID}/videos", handler.CreateVideoInPlaylist).Methods("POST") // DONE - r.HandleFunc("/api/v1/playlists/{PublicID}/videos/{VideoID}", handler.DeleteVideo).Methods("DELETE") // DONE + r.HandleFunc("/api/v1/playlists/{PublicID}/videos", handler.CreateVideoInPlaylist).Methods("POST") + r.HandleFunc("/api/v1/playlists/{PublicID}/videos/{VideoID}", handler.DeleteVideo).Methods("DELETE") fs := http.FileServer(http.Dir("./static")) http.Handle("/", fs) - visitors := limiter.NewCatalog(256) - go limiter.CleanupVisitors(visitors) + visitors := limiter.NewCatalog(256, 5, 10) // TODO: Change to environment variabes + timeToDumpVisitor := 60 * time.Minute // TODO: Change to environment variabe + go limiter.CleanupVisitors(visitors, timeToDumpVisitor) + server := http.Server{ Addr: ":8080", Handler: limiter.Limit(visitors, r), From edb1100a498240a013cfc282d4482f912cffd10b Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:42:42 +0000 Subject: [PATCH 08/12] added todo's --- database/config.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/database/config.go b/database/config.go index a5784af..4dd3baa 100644 --- a/database/config.go +++ b/database/config.go @@ -1,12 +1,12 @@ package database var ( - dbHost = "localhost" - dbUser = "postgres" - dbPassword = "postgres" - dbDatabase = "collabyt" - dbPort = "5432" + dbHost = "localhost" // TODO: Change to environment variabe + dbUser = "postgres" // TODO: Change to environment variabe + dbPassword = "postgres" // TODO: Change to environment variabe + dbDatabase = "collabyt" // TODO: Change to environment variabe + dbPort = "5432" // TODO: Change to environment variabe - dbSsl = "disable" - dbSource = "postgres" + dbSsl = "disable" // TODO: Change to environment variabe + dbSource = "postgres" // TODO: Change to environment variabe ) From 00f58a075acc11dd4bf3552f053c23b1d4905694 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Tue, 10 Nov 2020 22:43:36 +0000 Subject: [PATCH 09/12] Reduced size of errors to tidy up code. --- handler/createkeyword.go | 7 ++----- handler/createvideoinplaylist.go | 10 ++-------- handler/deletevideo.go | 22 +++++++++------------- handler/fetchplaylist.go | 2 +- handler/getkeywords.go | 5 +---- handler/getplaylistbypublicid.go | 5 +---- handler/getpublicplaylists.go | 5 +---- 7 files changed, 17 insertions(+), 39 deletions(-) diff --git a/handler/createkeyword.go b/handler/createkeyword.go index 9c89d61..56a02bf 100644 --- a/handler/createkeyword.go +++ b/handler/createkeyword.go @@ -19,15 +19,12 @@ func CreateKeyword(w http.ResponseWriter, r *http.Request) { return } if len(word.Word) < 2 { - WriteErrorReply( - w, - http.StatusBadRequest, - ) + WriteErrorReply(w, http.StatusBadRequest) return } word, err = model.CreateKeyword(database.DB, word.Word) if err != nil { - WriteErrorReply(w, http.StatusBadRequest) + WriteErrorReply(w, http.StatusConflict) return } jsonResponse, _ := json.Marshal(word) diff --git a/handler/createvideoinplaylist.go b/handler/createvideoinplaylist.go index bce5f30..94d22df 100644 --- a/handler/createvideoinplaylist.go +++ b/handler/createvideoinplaylist.go @@ -29,10 +29,7 @@ func CreateVideoInPlaylist(w http.ResponseWriter, r *http.Request) { return } if !ok { - WriteErrorReply( - w, - http.StatusInternalServerError, - ) + WriteErrorReply(w, http.StatusInternalServerError) return } } @@ -45,10 +42,7 @@ func CreateVideoInPlaylist(w http.ResponseWriter, r *http.Request) { video.PlaylistID = playlist.ID video, ok := model.CreateVideoInPlaylist(database.DB, video) if !ok { - WriteErrorReply( - w, - http.StatusInternalServerError, - ) + WriteErrorReply(w, http.StatusInternalServerError) return } np, err := model.GetPlaylistByPublicID(database.DB, playlist.PublicID) diff --git a/handler/deletevideo.go b/handler/deletevideo.go index a1ae32f..e076eda 100644 --- a/handler/deletevideo.go +++ b/handler/deletevideo.go @@ -19,10 +19,7 @@ func DeleteVideo(w http.ResponseWriter, r *http.Request) { } playlist, err := model.GetPlaylistByPublicID(database.DB, publicID) if err != nil { - WriteErrorReply( - w, - http.StatusNotFound, - ) + WriteErrorReply(w, http.StatusNotFound) return } videoID, err := fetchVars(r, "VideoID") @@ -33,20 +30,19 @@ func DeleteVideo(w http.ResponseWriter, r *http.Request) { v.PlaylistID = playlist.ID v.ID, err = strconv.Atoi(videoID) if err != nil { - WriteErrorReply( - w, - http.StatusBadRequest, - ) + WriteErrorReply(w, http.StatusBadRequest) return } ok := model.DeleteVideo(database.DB, v) if !ok { - WriteErrorReply( - w, - http.StatusInternalServerError, - ) + WriteErrorReply(w, http.StatusInternalServerError) return } - http.Redirect(w, r, fmt.Sprintf("/api/v1/playlists/%s", playlist.PublicID), http.StatusSeeOther) + http.Redirect( + w, + r, + fmt.Sprintf("/api/v1/playlists/%s", playlist.PublicID), + http.StatusSeeOther, + ) return } diff --git a/handler/fetchplaylist.go b/handler/fetchplaylist.go index 0743c56..57640b1 100644 --- a/handler/fetchplaylist.go +++ b/handler/fetchplaylist.go @@ -10,7 +10,7 @@ import ( func fetchPlaylist(db *sql.DB, publicID string) (model.Playlist, error) { ps, err := model.GetPlaylistByPublicID(db, publicID) if err != nil { - return model.Playlist{}, fmt.Errorf("Could not find a playlist with that ID") + return model.Playlist{}, fmt.Errorf("Could not find playlist") } return ps, nil } diff --git a/handler/getkeywords.go b/handler/getkeywords.go index 810d1b4..49bfe90 100644 --- a/handler/getkeywords.go +++ b/handler/getkeywords.go @@ -14,10 +14,7 @@ func GetKeywords(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") addressParams := r.URL.Query() if len(addressParams["likewise"]) < 1 || len(addressParams["likewise"][0]) < 2 { - WriteErrorReply( - w, - http.StatusBadRequest, - ) + WriteErrorReply(w, http.StatusBadRequest) return } wordList, err := model.GetKeywordsByPartialWord(database.DB, addressParams["likewise"][0]) diff --git a/handler/getplaylistbypublicid.go b/handler/getplaylistbypublicid.go index 57d550d..440d0a3 100644 --- a/handler/getplaylistbypublicid.go +++ b/handler/getplaylistbypublicid.go @@ -17,10 +17,7 @@ func GetPlaylistByPublicID(w http.ResponseWriter, r *http.Request) { } playlist, err := model.GetPlaylistByPublicID(database.DB, publicID) if !playlist.IsPublic { - WriteErrorReply( - w, - http.StatusForbidden, - ) + WriteErrorReply(w, http.StatusForbidden) return } if err != nil { diff --git a/handler/getpublicplaylists.go b/handler/getpublicplaylists.go index 6e0a0cc..8d3d32f 100644 --- a/handler/getpublicplaylists.go +++ b/handler/getpublicplaylists.go @@ -22,10 +22,7 @@ func GetPublicPlaylists(w http.ResponseWriter, r *http.Request) { } } if limit > 25 { - WriteErrorReply( - w, - http.StatusBadRequest, - ) + WriteErrorReply(w, http.StatusBadRequest) return } offsetSlc, ok := r.URL.Query()["offset"] From 0301c2f81afa5d0b928909c2aa5a4a14239f89c0 Mon Sep 17 00:00:00 2001 From: "Gustavo H. M. Silva" Date: Thu, 12 Nov 2020 23:17:56 +0000 Subject: [PATCH 10/12] Implemented a REDIS based traffic limiter. --- cache/config.go | 8 ++ cache/connect.go | 30 ++++++++ database/connect.go | 6 +- go.mod | 6 +- go.sum | 115 ++++++++++++++++++++++++++++- handler/cookies.go | 8 +- handler/createkeyword.go | 2 +- handler/createplaylist.go | 2 +- handler/createvideoinplaylist.go | 8 +- handler/deauthorizetoplaylist.go | 2 +- handler/deletevideo.go | 4 +- handler/getkeywords.go | 2 +- handler/getplaylistbypublicid.go | 2 +- handler/getpublicplaylists.go | 2 +- handler/requestaccesstoplaylist.go | 2 +- limiter/config.go | 5 ++ limiter/limiter.go | 55 +++++++++----- main.go | 9 +-- 18 files changed, 220 insertions(+), 48 deletions(-) create mode 100644 cache/config.go create mode 100644 cache/connect.go create mode 100644 limiter/config.go diff --git a/cache/config.go b/cache/config.go new file mode 100644 index 0000000..8c57922 --- /dev/null +++ b/cache/config.go @@ -0,0 +1,8 @@ +package cache + +var ( + cacheHost = "localhost" + cachePort = "6379" + cachePassword = "" + cacheDB = 0 +) diff --git a/cache/connect.go b/cache/connect.go new file mode 100644 index 0000000..3e9c978 --- /dev/null +++ b/cache/connect.go @@ -0,0 +1,30 @@ +package cache + +import ( + "context" + "fmt" + + "github.com/go-redis/redis/v8" +) + +var ( + // Cache is the REDIS document database + Cache *redis.Client +) + +// Connect will initiate a connection with the 0 DB on the REDIS server +func Connect() { + rOptions := &redis.Options{ + Addr: fmt.Sprintf("%s:%s", cacheHost, cachePort), + Password: cachePassword, + DB: 0, + } + rClient := redis.NewClient(rOptions) + ctx := context.Background() + _, err := rClient.Ping(ctx).Result() + if err != nil { + panic(err) + } + _ = rClient.FlushDB(ctx).Err() + Cache = rClient +} diff --git a/database/connect.go b/database/connect.go index d349e49..7a50060 100644 --- a/database/connect.go +++ b/database/connect.go @@ -9,8 +9,8 @@ import ( ) var ( - // DB is the Database connection pool - DB *sql.DB + // Dd is the Database connection pool + Db *sql.DB ) // Connect opens a connection to the postgres database using the environment @@ -33,5 +33,5 @@ func Connect() { if err != nil { panic(err) } - DB = db + Db = db } diff --git a/go.mod b/go.mod index 0af83a8..6939ead 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,12 @@ module github.com/collabyt/Backend go 1.14 require ( + github.com/go-redis/redis/v8 v8.0.0 + github.com/go-redis/redis_rate/v9 v9.0.2 + github.com/google/go-cmp v0.5.2 // indirect github.com/gorilla/mux v1.7.4 github.com/lib/pq v1.3.0 + github.com/onsi/ginkgo v1.14.2 // indirect + github.com/onsi/gomega v1.10.3 // indirect golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de - golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e ) diff --git a/go.sum b/go.sum index aefe264..c05a431 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,124 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-redis/redis/v8 v8.0.0 h1:PC0VsF9sFFd2sko5bu30aEFc8F1TKl6n65o0b8FnCIE= +github.com/go-redis/redis/v8 v8.0.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo= +github.com/go-redis/redis_rate/v9 v9.0.2 h1:Uwj0zTwwODzmL5E3UeN78jnyFxN41itYgkkCfqNYc7w= +github.com/go-redis/redis_rate/v9 v9.0.2/go.mod h1:Vrl8qDpRb7bHcLyr7OXVtKKww4bD8dLL9gmUdA6XClg= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.11.0 h1:IN2tzQa9Gc4ZVKnTaMbPVcHjvzOdg5n9QfnmlqiET7E= +go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925 h1:5XVKs2rlCg8EFyRcvO8/XFwYxh1oKJO1Q3X5vttIf9c= +golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/cookies.go b/handler/cookies.go index 5da6680..94524bc 100644 --- a/handler/cookies.go +++ b/handler/cookies.go @@ -12,7 +12,7 @@ import ( ) func hasCookie(cook *http.Cookie) ([]byte, error) { - s, err := model.GetSessionBySessionID(database.DB, cook.Value) + s, err := model.GetSessionBySessionID(database.Db, cook.Value) if err != nil { return []byte{}, err } @@ -20,7 +20,7 @@ func hasCookie(cook *http.Cookie) ([]byte, error) { if s == es { cook.MaxAge = -1 } - playlist, err := model.GetPlaylistByPublicID(database.DB, cook.Name) + playlist, err := model.GetPlaylistByPublicID(database.Db, cook.Name) if err != nil { return []byte{}, err } @@ -34,7 +34,7 @@ func noCookie(a model.Auth) (http.Cookie, int, error) { http.StatusUnauthorized, fmt.Errorf("Invalid Public ID") } - ps, err := model.GetPlaylistByPublicID(database.DB, a.PublicID) + ps, err := model.GetPlaylistByPublicID(database.Db, a.PublicID) if err != nil { return http.Cookie{}, http.StatusUnauthorized, err @@ -55,7 +55,7 @@ func noCookie(a model.Auth) (http.Cookie, int, error) { fmt.Errorf("Something wrong happened") } - err = model.CreateSession(database.DB, s) + err = model.CreateSession(database.Db, s) if err != nil { return http.Cookie{}, http.StatusInternalServerError, diff --git a/handler/createkeyword.go b/handler/createkeyword.go index 56a02bf..9a0fbf2 100644 --- a/handler/createkeyword.go +++ b/handler/createkeyword.go @@ -22,7 +22,7 @@ func CreateKeyword(w http.ResponseWriter, r *http.Request) { WriteErrorReply(w, http.StatusBadRequest) return } - word, err = model.CreateKeyword(database.DB, word.Word) + word, err = model.CreateKeyword(database.Db, word.Word) if err != nil { WriteErrorReply(w, http.StatusConflict) return diff --git a/handler/createplaylist.go b/handler/createplaylist.go index 43a9113..7667b1d 100644 --- a/handler/createplaylist.go +++ b/handler/createplaylist.go @@ -19,7 +19,7 @@ func CreatePlaylist(w http.ResponseWriter, r *http.Request) { WriteErrorReply(w, http.StatusBadRequest) return } - playlist, err = model.CreatePlaylist(database.DB, playlist) + playlist, err = model.CreatePlaylist(database.Db, playlist) if err != nil { WriteErrorReply(w, http.StatusBadRequest) return diff --git a/handler/createvideoinplaylist.go b/handler/createvideoinplaylist.go index 94d22df..af0c996 100644 --- a/handler/createvideoinplaylist.go +++ b/handler/createvideoinplaylist.go @@ -17,13 +17,13 @@ func CreateVideoInPlaylist(w http.ResponseWriter, r *http.Request) { WriteErrorReply(w, http.StatusBadRequest) return } - playlist, err := fetchPlaylist(database.DB, publicID) + playlist, err := fetchPlaylist(database.Db, publicID) if err != nil { WriteErrorReply(w, http.StatusNotFound) return } if !playlist.IsPublic { - ok, err := validateSession(database.DB, r, playlist) + ok, err := validateSession(database.Db, r, playlist) if err != nil { WriteErrorReply(w, http.StatusForbidden) return @@ -40,12 +40,12 @@ func CreateVideoInPlaylist(w http.ResponseWriter, r *http.Request) { return } video.PlaylistID = playlist.ID - video, ok := model.CreateVideoInPlaylist(database.DB, video) + video, ok := model.CreateVideoInPlaylist(database.Db, video) if !ok { WriteErrorReply(w, http.StatusInternalServerError) return } - np, err := model.GetPlaylistByPublicID(database.DB, playlist.PublicID) + np, err := model.GetPlaylistByPublicID(database.Db, playlist.PublicID) np.Passphrase = "" jsonResponse, _ := json.Marshal(np) w.Write(jsonResponse) diff --git a/handler/deauthorizetoplaylist.go b/handler/deauthorizetoplaylist.go index edad009..7a57886 100644 --- a/handler/deauthorizetoplaylist.go +++ b/handler/deauthorizetoplaylist.go @@ -21,7 +21,7 @@ func DeauthorizeToPlaylist(w http.ResponseWriter, r *http.Request) { } cook.MaxAge = -1 http.SetCookie(w, cook) - err = model.DeleteSessionBySessionID(database.DB, cook.Value) + err = model.DeleteSessionBySessionID(database.Db, cook.Value) if err != nil { WriteErrorReply(w, http.StatusInternalServerError) return diff --git a/handler/deletevideo.go b/handler/deletevideo.go index e076eda..6462679 100644 --- a/handler/deletevideo.go +++ b/handler/deletevideo.go @@ -17,7 +17,7 @@ func DeleteVideo(w http.ResponseWriter, r *http.Request) { if err != nil { WriteErrorReply(w, http.StatusBadRequest) } - playlist, err := model.GetPlaylistByPublicID(database.DB, publicID) + playlist, err := model.GetPlaylistByPublicID(database.Db, publicID) if err != nil { WriteErrorReply(w, http.StatusNotFound) return @@ -33,7 +33,7 @@ func DeleteVideo(w http.ResponseWriter, r *http.Request) { WriteErrorReply(w, http.StatusBadRequest) return } - ok := model.DeleteVideo(database.DB, v) + ok := model.DeleteVideo(database.Db, v) if !ok { WriteErrorReply(w, http.StatusInternalServerError) return diff --git a/handler/getkeywords.go b/handler/getkeywords.go index 49bfe90..a1a250a 100644 --- a/handler/getkeywords.go +++ b/handler/getkeywords.go @@ -17,7 +17,7 @@ func GetKeywords(w http.ResponseWriter, r *http.Request) { WriteErrorReply(w, http.StatusBadRequest) return } - wordList, err := model.GetKeywordsByPartialWord(database.DB, addressParams["likewise"][0]) + wordList, err := model.GetKeywordsByPartialWord(database.Db, addressParams["likewise"][0]) if err != nil { WriteErrorReply(w, http.StatusBadRequest) return diff --git a/handler/getplaylistbypublicid.go b/handler/getplaylistbypublicid.go index 440d0a3..cdcbc5e 100644 --- a/handler/getplaylistbypublicid.go +++ b/handler/getplaylistbypublicid.go @@ -15,7 +15,7 @@ func GetPlaylistByPublicID(w http.ResponseWriter, r *http.Request) { if err != nil { WriteErrorReply(w, http.StatusBadRequest) } - playlist, err := model.GetPlaylistByPublicID(database.DB, publicID) + playlist, err := model.GetPlaylistByPublicID(database.Db, publicID) if !playlist.IsPublic { WriteErrorReply(w, http.StatusForbidden) return diff --git a/handler/getpublicplaylists.go b/handler/getpublicplaylists.go index 8d3d32f..610435c 100644 --- a/handler/getpublicplaylists.go +++ b/handler/getpublicplaylists.go @@ -34,7 +34,7 @@ func GetPublicPlaylists(w http.ResponseWriter, r *http.Request) { offset = 0 } } - ps, err := model.GetPublicPlaylistsByLimitAndOffset(database.DB, limit, offset) + ps, err := model.GetPublicPlaylistsByLimitAndOffset(database.Db, limit, offset) if err != nil { WriteErrorReply(w, http.StatusInternalServerError) return diff --git a/handler/requestaccesstoplaylist.go b/handler/requestaccesstoplaylist.go index c21b025..11f706a 100644 --- a/handler/requestaccesstoplaylist.go +++ b/handler/requestaccesstoplaylist.go @@ -29,7 +29,7 @@ func RequestAccessToPlaylist(w http.ResponseWriter, r *http.Request) { return } http.SetCookie(w, &newCookie) - playlist, _ := model.GetPlaylistByPublicID(database.DB, newCookie.Name) + playlist, _ := model.GetPlaylistByPublicID(database.Db, newCookie.Name) playlist.Passphrase = "" jasonPlaylist, _ := json.Marshal(playlist) w.Write(jasonPlaylist) diff --git a/limiter/config.go b/limiter/config.go new file mode 100644 index 0000000..b805317 --- /dev/null +++ b/limiter/config.go @@ -0,0 +1,5 @@ +package limiter + +var ( + timeToDumpVisitor = 60 // TODO: Change to environment variabe +) diff --git a/limiter/limiter.go b/limiter/limiter.go index c1bb2f0..7f67f24 100644 --- a/limiter/limiter.go +++ b/limiter/limiter.go @@ -1,20 +1,48 @@ package limiter import ( + "fmt" "net" "net/http" - "sync" - "time" - - "golang.org/x/time/rate" + "github.com/collabyt/Backend/cache" "github.com/collabyt/Backend/handler" + "github.com/go-redis/redis/v8" + "github.com/go-redis/redis_rate/v9" ) +// Limit cap the amount of requests possible for a single IP in a given period +// of time +func Limit(rClient *redis.Client, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + handler.WriteErrorReply(w, http.StatusInternalServerError) + return + } + ctx := r.Context() + limiter := redis_rate.NewLimiter(cache.Cache) + rRet, err := limiter.Allow(ctx, ip, redis_rate.PerMinute(timeToDumpVisitor)) + if err != nil { + handler.WriteErrorReply(w, http.StatusInternalServerError) + return + + } + if rRet.Allowed == 0 { + handler.WriteErrorReply(w, http.StatusTooManyRequests) + return + } + fmt.Println("allowed", rRet.Allowed, "remaining", rRet.Remaining) + next.ServeHTTP(w, r) + }) +} + +/* // Visitor is a instance of a user accessing the API type Visitor struct { - limiter *rate.Limiter - lastSeen time.Time + limiter *rate.Limiter + expiration } // Catalog represent a list of visitor and a Mutex @@ -69,17 +97,4 @@ func Limit(cat *Catalog, next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } - -// CleanupVisitors remove unused IP's from the visitors -func CleanupVisitors(cat *Catalog, limit time.Duration) { - for { - time.Sleep(time.Minute) - cat.mu.Lock() - for ip, v := range cat.visitors { - if time.Since(v.lastSeen) > limit { - delete(cat.visitors, ip) - } - } - cat.mu.Unlock() - } -} +*/ diff --git a/main.go b/main.go index 040486b..900464c 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "time" + "github.com/collabyt/Backend/cache" "github.com/collabyt/Backend/database" "github.com/collabyt/Backend/handler" "github.com/collabyt/Backend/limiter" @@ -14,6 +15,8 @@ import ( func main() { // Database connection pool database.Connect() + // Document database connection client + cache.Connect() // API routes r := mux.NewRouter() @@ -34,13 +37,9 @@ func main() { fs := http.FileServer(http.Dir("./static")) http.Handle("/", fs) - visitors := limiter.NewCatalog(256, 5, 10) // TODO: Change to environment variabes - timeToDumpVisitor := 60 * time.Minute // TODO: Change to environment variabe - go limiter.CleanupVisitors(visitors, timeToDumpVisitor) - server := http.Server{ Addr: ":8080", - Handler: limiter.Limit(visitors, r), + Handler: limiter.Limit(cache.Cache, r), IdleTimeout: 120 * time.Second, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, From 60afb53724641eacc6679be1a4a3e2da54fec612 Mon Sep 17 00:00:00 2001 From: John Lennon Date: Tue, 17 Nov 2020 21:19:11 +0000 Subject: [PATCH 11/12] Removed the legacy code inside a comment for the limiter! --- limiter/limiter.go | 61 ---------------------------------------------- 1 file changed, 61 deletions(-) diff --git a/limiter/limiter.go b/limiter/limiter.go index 7f67f24..a0f3e32 100644 --- a/limiter/limiter.go +++ b/limiter/limiter.go @@ -37,64 +37,3 @@ func Limit(rClient *redis.Client, next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } - -/* -// Visitor is a instance of a user accessing the API -type Visitor struct { - limiter *rate.Limiter - expiration -} - -// Catalog represent a list of visitor and a Mutex -type Catalog struct { - visitors map[string]*Visitor - mu sync.RWMutex - rate rate.Limit - maxBurst int -} - -// NewCatalog initialize the visitors map inside the type Catalog -func NewCatalog(size, r, maxBurst int) *Catalog { - var cat Catalog - cat.visitors = make(map[string]*Visitor, size) - cat.rate = rate.Limit(r) - cat.maxBurst = maxBurst - return &cat -} - -func getVisitor(cat *Catalog, ip string) *rate.Limiter { - cat.mu.RLock() //read - v, exist := cat.visitors[ip] - cat.mu.RUnlock() - if !exist { - limiter := rate.NewLimiter(cat.rate, cat.maxBurst) - cat.mu.Lock() //read - cat.visitors[ip] = &Visitor{limiter, time.Now()} - cat.mu.Unlock() - return limiter - } - cat.mu.Lock() //write - v.lastSeen = time.Now() - cat.mu.Unlock() - return v.limiter -} - -// Limit cap the amount of requests possible for a single IP in a given period -// of time -func Limit(cat *Catalog, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - handler.WriteErrorReply(w, http.StatusInternalServerError) - return - } - limiter := getVisitor(cat, ip) - if !limiter.Allow() { - handler.WriteErrorReply(w, http.StatusTooManyRequests) - return - } - next.ServeHTTP(w, r) - }) -} -*/ From f981e6f9bd1c3b38c2cfe464df9422a547a98732 Mon Sep 17 00:00:00 2001 From: John Lennon Date: Tue, 17 Nov 2020 21:58:17 +0000 Subject: [PATCH 12/12] Adapted application for usage in docker with system parameters. --- Dockerfile | 25 +++++++++++++++++-------- cache/config.go | 9 ++++++--- cache/connect.go | 14 +++++++++++++- database/config.go | 16 +++++++++------- limiter/config.go | 5 ----- limiter/limiter.go | 2 +- main.go | 15 +++++---------- startup/startup.go | 37 +++++++++++++++++++++++++++++++++++++ 8 files changed, 88 insertions(+), 35 deletions(-) delete mode 100644 limiter/config.go create mode 100644 startup/startup.go diff --git a/Dockerfile b/Dockerfile index 568d87b..ecf0e4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,14 +9,23 @@ COPY go.sum . RUN go mod download COPY . . -ENV PORT 8080 -ENV DBHOST 0.0.0.0 -ENV DBUSER postgres -ENV DBPASSWORD postgres -ENV DBDATABASE collabyt -ENV DBPORT 5432 -ENV DBSSL disable -ENV DBSOURCE postgres +ENV APP_PORT 8080 +ENV APP_IDLE_TIMEOUT 120 +ENV APP_READ_TIMEOUT 5 +ENV APP_WRITE_TIMEOUT 5 + +ENV DB_HOST localhost +ENV DB_USER postgres +ENV DB_PASSWORD postgres +ENV DB_DATABASE collabyt +ENV DB_PORT 5432 +ENV DB_SSL disable +ENV DB_SOURCE postgres + +ENV CACHE_TTL 60 +ENV CACHE_HOST localhost +ENV CACHE_PORT 6379 +ENV CACHE_PASSWORD "" RUN go build CMD ["./Backend"] \ No newline at end of file diff --git a/cache/config.go b/cache/config.go index 8c57922..497dde7 100644 --- a/cache/config.go +++ b/cache/config.go @@ -1,8 +1,11 @@ package cache +import "os" + var ( - cacheHost = "localhost" - cachePort = "6379" - cachePassword = "" + cacheHost = os.Getenv("CACHE_HOST") + cachePort = os.Getenv("CACHE_PORT") + cachePassword = os.Getenv("CACHE_PASSWORD") cacheDB = 0 + CacheTTL = 0 ) diff --git a/cache/connect.go b/cache/connect.go index 3e9c978..6cb0447 100644 --- a/cache/connect.go +++ b/cache/connect.go @@ -3,6 +3,8 @@ package cache import ( "context" "fmt" + "os" + "strconv" "github.com/go-redis/redis/v8" ) @@ -14,6 +16,11 @@ var ( // Connect will initiate a connection with the 0 DB on the REDIS server func Connect() { + ttl, err := loadTTL() + if err != nil { + panic(err) + } + CacheTTL = ttl rOptions := &redis.Options{ Addr: fmt.Sprintf("%s:%s", cacheHost, cachePort), Password: cachePassword, @@ -21,10 +28,15 @@ func Connect() { } rClient := redis.NewClient(rOptions) ctx := context.Background() - _, err := rClient.Ping(ctx).Result() + _, err = rClient.Ping(ctx).Result() if err != nil { panic(err) } _ = rClient.FlushDB(ctx).Err() Cache = rClient } + +func loadTTL() (int, error) { + ttl, err := strconv.Atoi(os.Getenv("CACHE_TTL")) + return ttl, err +} diff --git a/database/config.go b/database/config.go index 4dd3baa..9210220 100644 --- a/database/config.go +++ b/database/config.go @@ -1,12 +1,14 @@ package database +import "os" + var ( - dbHost = "localhost" // TODO: Change to environment variabe - dbUser = "postgres" // TODO: Change to environment variabe - dbPassword = "postgres" // TODO: Change to environment variabe - dbDatabase = "collabyt" // TODO: Change to environment variabe - dbPort = "5432" // TODO: Change to environment variabe + dbHost = os.Getenv("DB_HOST") + dbUser = os.Getenv("DB_USER") + dbPassword = os.Getenv("DB_PASSWORD") + dbDatabase = os.Getenv("DB_DATABASE") + dbPort = os.Getenv("DB_PORT") - dbSsl = "disable" // TODO: Change to environment variabe - dbSource = "postgres" // TODO: Change to environment variabe + dbSsl = os.Getenv("DB_SSL") + dbSource = os.Getenv("DB_SOURCE") ) diff --git a/limiter/config.go b/limiter/config.go deleted file mode 100644 index b805317..0000000 --- a/limiter/config.go +++ /dev/null @@ -1,5 +0,0 @@ -package limiter - -var ( - timeToDumpVisitor = 60 // TODO: Change to environment variabe -) diff --git a/limiter/limiter.go b/limiter/limiter.go index a0f3e32..2ffe03a 100644 --- a/limiter/limiter.go +++ b/limiter/limiter.go @@ -23,7 +23,7 @@ func Limit(rClient *redis.Client, next http.Handler) http.Handler { } ctx := r.Context() limiter := redis_rate.NewLimiter(cache.Cache) - rRet, err := limiter.Allow(ctx, ip, redis_rate.PerMinute(timeToDumpVisitor)) + rRet, err := limiter.Allow(ctx, ip, redis_rate.PerMinute(cache.CacheTTL)) if err != nil { handler.WriteErrorReply(w, http.StatusInternalServerError) return diff --git a/main.go b/main.go index 900464c..667d6d3 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,10 @@ package main import ( "net/http" - "time" - "github.com/collabyt/Backend/cache" "github.com/collabyt/Backend/database" "github.com/collabyt/Backend/handler" - "github.com/collabyt/Backend/limiter" + "github.com/collabyt/Backend/startup" "github.com/gorilla/mux" ) @@ -37,14 +35,11 @@ func main() { fs := http.FileServer(http.Dir("./static")) http.Handle("/", fs) - server := http.Server{ - Addr: ":8080", - Handler: limiter.Limit(cache.Cache, r), - IdleTimeout: 120 * time.Second, - ReadTimeout: 5 * time.Second, - WriteTimeout: 5 * time.Second, + server, err := startup.SetupServer(r) + if err != nil { + panic(err) } - err := server.ListenAndServe() + err = server.ListenAndServe() if err != nil { panic(err) } diff --git a/startup/startup.go b/startup/startup.go new file mode 100644 index 0000000..e9c11ff --- /dev/null +++ b/startup/startup.go @@ -0,0 +1,37 @@ +package startup + +import ( + "fmt" + "net/http" + "os" + "strconv" + "time" + + "github.com/collabyt/Backend/cache" + "github.com/collabyt/Backend/limiter" + "github.com/gorilla/mux" +) + +func SetupServer(r *mux.Router) (http.Server, error) { + address := os.Getenv("APP_PORT") + handler := limiter.Limit(cache.Cache, r) + idleTimeout, err := strconv.Atoi(os.Getenv("APP_IDLE_TIMEOUT")) + if err != nil { + return http.Server{}, fmt.Errorf("server stup faile: impossible to get %s from system's parameters", "APP_IDLE_TIMEOUT") + } + readTimeout, err := strconv.Atoi(os.Getenv("APP_READ_TIMEOUT")) + if err != nil { + return http.Server{}, fmt.Errorf("server stup faile: impossible to get %s from system's parameters", "APP_READ_TIMEOUT") + } + writeTimeout, err := strconv.Atoi(os.Getenv("APP_WRITE_TIMEOUT")) + if err != nil { + return http.Server{}, fmt.Errorf("server stup faile: impossible to get %s from system's parameters", "APP_WRITE_TIMEOUT") + } + return http.Server{ + Addr: address, + Handler: handler, + IdleTimeout: time.Duration(idleTimeout), + ReadTimeout: time.Duration(readTimeout), + WriteTimeout: time.Duration(writeTimeout), + }, err +}