140 lines
3 KiB
Go
140 lines
3 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Visit struct {
|
||
|
|
RemoteAddr string
|
||
|
|
Location string
|
||
|
|
}
|
||
|
|
|
||
|
|
type GeolocationResponse struct {
|
||
|
|
Country string `json:"country"`
|
||
|
|
City string `json:"city"`
|
||
|
|
RegionName string `json:"regionName"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type VisitHandler struct {
|
||
|
|
geolocationApiBaseUrl string
|
||
|
|
visitsCache map[string]int64
|
||
|
|
pastRequests []int64
|
||
|
|
rateLimitAmount int
|
||
|
|
rateLimitDurationSeconds int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (v *VisitHandler) buildUrl(ipAddress string) string {
|
||
|
|
return fmt.Sprintf("%s/json/%s?fields=country,regionName,city", v.geolocationApiBaseUrl, ipAddress)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (v *VisitHandler) limitRate() bool {
|
||
|
|
now := time.Now()
|
||
|
|
|
||
|
|
idxToRemove := make([]int, 0)
|
||
|
|
for idx, ts := range v.pastRequests {
|
||
|
|
if now.Sub(time.Unix(ts, 0)).Seconds() >= float64(v.rateLimitDurationSeconds) {
|
||
|
|
idxToRemove = append(idxToRemove, idx)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for i := len(idxToRemove) - 1; i >= 0; i-- {
|
||
|
|
idx := idxToRemove[i]
|
||
|
|
v.pastRequests = append(v.pastRequests[:idx], v.pastRequests[idx+1:]...)
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(v.pastRequests) == 0 {
|
||
|
|
v.pastRequests = append(v.pastRequests, now.Unix())
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
retVal := false
|
||
|
|
|
||
|
|
if len(v.pastRequests) < v.rateLimitAmount {
|
||
|
|
v.pastRequests = append(v.pastRequests, now.Unix())
|
||
|
|
retVal = true
|
||
|
|
}
|
||
|
|
|
||
|
|
return retVal
|
||
|
|
}
|
||
|
|
|
||
|
|
func (v *VisitHandler) getLocation(ipAddress string) *GeolocationResponse {
|
||
|
|
if !v.limitRate() {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, err := http.Get(v.buildUrl(ipAddress))
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
body, err := io.ReadAll(resp.Body)
|
||
|
|
if err != nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
var geoResponse GeolocationResponse
|
||
|
|
err = json.Unmarshal(body, &geoResponse)
|
||
|
|
return &geoResponse
|
||
|
|
}
|
||
|
|
|
||
|
|
func (v *VisitHandler) createVisit(ipAddress string) *Visit {
|
||
|
|
loc := v.getLocation(ipAddress)
|
||
|
|
if loc == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
locationParts := []string{}
|
||
|
|
if loc.City != "" {
|
||
|
|
locationParts = append(locationParts, loc.City)
|
||
|
|
}
|
||
|
|
if loc.RegionName != "" {
|
||
|
|
locationParts = append(locationParts, loc.RegionName)
|
||
|
|
}
|
||
|
|
if loc.Country != "" {
|
||
|
|
locationParts = append(locationParts, loc.Country)
|
||
|
|
}
|
||
|
|
|
||
|
|
return &Visit{
|
||
|
|
RemoteAddr: ipAddress,
|
||
|
|
Location: strings.Join(locationParts, ", "),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (v *VisitHandler) HandleVisit(remoteAddr string) *Visit {
|
||
|
|
if v.visitsCache == nil {
|
||
|
|
v.visitsCache = make(map[string]int64)
|
||
|
|
}
|
||
|
|
|
||
|
|
if v.visitsCache[remoteAddr] == 0 || time.Since(time.Unix(v.visitsCache[remoteAddr], 0)) > time.Duration(v.rateLimitDurationSeconds) {
|
||
|
|
visit := v.createVisit(remoteAddr)
|
||
|
|
if visit == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
v.visitsCache[visit.RemoteAddr] = time.Now().Unix()
|
||
|
|
return visit
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type VisitHandlerArgs struct {
|
||
|
|
baseUrl string
|
||
|
|
rateLimitAmount int
|
||
|
|
rateLimitDurationSeconds int
|
||
|
|
}
|
||
|
|
|
||
|
|
func SetupVisitHandler(args *VisitHandlerArgs) *VisitHandler {
|
||
|
|
return &VisitHandler{
|
||
|
|
geolocationApiBaseUrl: args.baseUrl,
|
||
|
|
visitsCache: make(map[string]int64),
|
||
|
|
pastRequests: make([]int64, 0),
|
||
|
|
rateLimitAmount: args.rateLimitAmount,
|
||
|
|
rateLimitDurationSeconds: args.rateLimitDurationSeconds,
|
||
|
|
}
|
||
|
|
}
|