@@ -15,6 +15,7 @@ import (
1515 "path/filepath"
1616 "strconv"
1717 "strings"
18+ "sync"
1819 "syscall"
1920 "time"
2021
@@ -28,6 +29,12 @@ type app struct {
2829 sessionSecret string
2930 events chan map [string ]any
3031 docker * dockerreader.Reader
32+ loginMu sync.Mutex
33+ loginAttempts map [string ]attempt
34+ }
35+ type attempt struct {
36+ count int
37+ since time.Time
3138}
3239
3340func main () {
@@ -45,7 +52,7 @@ func main() {
4552 panic (err )
4653 }
4754 dr , _ := dockerreader .NewReader ()
48- a := & app {db : db , sessionSecret : getenv ("STACKHOST_SESSION_SECRET" , "development-only-change-me" ), events : make (chan map [string ]any , 32 ), docker : dr }
55+ a := & app {db : db , sessionSecret : getenv ("STACKHOST_SESSION_SECRET" , "development-only-change-me" ), events : make (chan map [string ]any , 32 ), docker : dr , loginAttempts : make ( map [ string ] attempt ) }
4956 s := & http.Server {Addr : addr , Handler : a .routes (), ReadHeaderTimeout : 10 * time .Second }
5057 ctx , stop := signal .NotifyContext (context .Background (), os .Interrupt , syscall .SIGTERM )
5158 defer stop ()
@@ -99,6 +106,17 @@ func security(next http.Handler) http.Handler {
99106 w .Header ().Set ("X-Content-Type-Options" , "nosniff" )
100107 w .Header ().Set ("X-Frame-Options" , "DENY" )
101108 w .Header ().Set ("Referrer-Policy" , "strict-origin-when-cross-origin" )
109+ w .Header ().Set ("X-XSS-Protection" , "0" )
110+ w .Header ().Set ("Permissions-Policy" , "camera=(), microphone=(), geolocation=()" )
111+ if origin := os .Getenv ("STACKHOST_ALLOWED_ORIGIN" ); origin != "" {
112+ w .Header ().Set ("Access-Control-Allow-Origin" , origin )
113+ w .Header ().Set ("Access-Control-Allow-Credentials" , "true" )
114+ w .Header ().Set ("Access-Control-Allow-Headers" , "Content-Type" )
115+ if r .Method == http .MethodOptions {
116+ w .WriteHeader (http .StatusNoContent )
117+ return
118+ }
119+ }
102120 next .ServeHTTP (w , r )
103121 })
104122}
@@ -157,6 +175,10 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
157175 jsonError (w , 422 , "validation_failed" , "Revise os campos informados." )
158176 return
159177 }
178+ if ! a .loginAllowed (r ) {
179+ jsonError (w , 429 , "too_many_attempts" , "Tente novamente em alguns instantes." )
180+ return
181+ }
160182 var id int64
161183 var hash string
162184 err := a .db .QueryRow ("SELECT id,password_hash FROM users WHERE email=?" , strings .ToLower (strings .TrimSpace (in .Email ))).Scan (& id , & hash )
@@ -169,6 +191,19 @@ func (a *app) login(w http.ResponseWriter, r *http.Request) {
169191 a .createSession (w , r , id )
170192 json .NewEncoder (w ).Encode (map [string ]bool {"ok" : true })
171193}
194+ func (a * app ) loginAllowed (r * http.Request ) bool {
195+ ip := strings .Split (r .RemoteAddr , ":" )[0 ]
196+ now := time .Now ()
197+ a .loginMu .Lock ()
198+ defer a .loginMu .Unlock ()
199+ v := a .loginAttempts [ip ]
200+ if now .Sub (v .since ) > time .Minute {
201+ v = attempt {since : now }
202+ }
203+ v .count ++
204+ a .loginAttempts [ip ] = v
205+ return v .count <= 10
206+ }
172207func (a * app ) createSession (w http.ResponseWriter , r * http.Request , id int64 ) {
173208 seed := make ([]byte , 32 )
174209 if _ , err := rand .Read (seed ); err != nil {
0 commit comments