Add login rate limiting
This commit is contained in:
		| @@ -1,9 +1,5 @@ | |||||||
| package v1 | package v1 | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"time" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| 	upSince = time.Now() | 	go removeUseless() | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										61
									
								
								app/v1/login_attempts.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								app/v1/login_attempts.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | package v1 | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type failedAttempt struct { | ||||||
|  | 	attempt time.Time | ||||||
|  | 	ID      int | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var failedAttempts []failedAttempt | ||||||
|  | var failedAttemptsMutex = new(sync.RWMutex) | ||||||
|  |  | ||||||
|  | // removeUseless removes the expired attempts in failedAttempts | ||||||
|  | func removeUseless() { | ||||||
|  | 	for { | ||||||
|  | 		failedAttemptsMutex.RLock() | ||||||
|  | 		var localCopy = make([]failedAttempt, len(failedAttempts)) | ||||||
|  | 		copy(localCopy, failedAttempts) | ||||||
|  | 		failedAttemptsMutex.RUnlock() | ||||||
|  | 		var newStartFrom int | ||||||
|  | 		for k, v := range localCopy { | ||||||
|  | 			if time.Since(v.attempt) > time.Minute*10 { | ||||||
|  | 				newStartFrom = k + 1 | ||||||
|  | 			} else { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		copySl := localCopy[newStartFrom:] | ||||||
|  | 		failedAttemptsMutex.Lock() | ||||||
|  | 		failedAttempts = make([]failedAttempt, len(copySl)) | ||||||
|  | 		for i, v := range copySl { | ||||||
|  | 			failedAttempts[i] = v | ||||||
|  | 		} | ||||||
|  | 		failedAttemptsMutex.Unlock() | ||||||
|  | 		time.Sleep(time.Minute * 10) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func addFailedAttempt(uid int) { | ||||||
|  | 	failedAttemptsMutex.Lock() | ||||||
|  | 	failedAttempts = append(failedAttempts, failedAttempt{ | ||||||
|  | 		attempt: time.Now(), | ||||||
|  | 		ID:      uid, | ||||||
|  | 	}) | ||||||
|  | 	failedAttemptsMutex.Unlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func nFailedAttempts(uid int) int { | ||||||
|  | 	var count int | ||||||
|  | 	failedAttemptsMutex.RLock() | ||||||
|  | 	for _, i := range failedAttempts { | ||||||
|  | 		if i.ID == uid && time.Since(i.attempt) < time.Minute*10 { | ||||||
|  | 			count++ | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	failedAttemptsMutex.RUnlock() | ||||||
|  | 	return count | ||||||
|  | } | ||||||
| @@ -50,7 +50,7 @@ func MetaKillGET(md common.MethodData) (r common.Response) { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
|  |  | ||||||
| var upSince time.Time | var upSince = time.Now() | ||||||
|  |  | ||||||
| // MetaUpSinceGET retrieves the moment the API application was started. | // MetaUpSinceGET retrieves the moment the API application was started. | ||||||
| // Mainly used to get if the API was restarted. | // Mainly used to get if the API was restarted. | ||||||
|   | |||||||
| @@ -77,6 +77,12 @@ func TokenNewPOST(md common.MethodData) (r common.Response) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if nFailedAttempts(ret.ID) > 20 { | ||||||
|  | 		r.Code = 429 | ||||||
|  | 		r.Message = "You've made too many login attempts. Try again later." | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if pwVersion == 1 { | 	if pwVersion == 1 { | ||||||
| 		r.Code = 418 // Teapots! | 		r.Code = 418 // Teapots! | ||||||
| 		r.Message = "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website." | 		r.Message = "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website." | ||||||
| @@ -84,6 +90,7 @@ func TokenNewPOST(md common.MethodData) (r common.Response) { | |||||||
| 	} | 	} | ||||||
| 	if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil { | 	if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil { | ||||||
| 		if err == bcrypt.ErrMismatchedHashAndPassword { | 		if err == bcrypt.ErrMismatchedHashAndPassword { | ||||||
|  | 			go addFailedAttempt(ret.ID) | ||||||
| 			r.Code = 403 | 			r.Code = 403 | ||||||
| 			r.Message = "That password doesn't match!" | 			r.Message = "That password doesn't match!" | ||||||
| 			return | 			return | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user