In this tutorial we will create a small web server and utilize BadActor to protect the /login endpoint.
First setup your application’s structure and main.go file.
$ go get "github.com/jaredfolkins/badactor"
$ go get "github.com/codegangsta/negroni"
$ go get "github.com/julienschmidt/httprouter"
$ mkdir $GOPATH/badactorTest;
$ cd $GOPATH/badactorTest;
$ vi main.go
Create the basic boilerplate main function, import the following packages, and create a variable for BadActor’s singlton Studio Struct.
Note, BadActor uses the Observer pattern with the Studio as the Subject. The Studio contains all of the publicly exposed APIs and is the only way of utilizing BadActor while limiting lock contention and avoiding deadlock.
package main
import (
"log"
"net"
"net/http"
"time"
"github.com/codegangsta/negroni"
"github.com/jaredfolkins/badactor"
"github.com/julienschmidt/httprouter"
)
var st *badactor.Studio
func main() {
// create new Studio
st = badactor.NewStudio()
}
Next we need to define a Rule. Please notice the Name field, which is how we will reference the Rule going forward. We safely pass the Rule to the Studio using the AddRule() method. If we wanted to we could add multiple Rules, but for this tutorial we will just use one.
// create and add rule
ru := &badactor.Rule{
Name: "Login",
Message: "You have failed to login too many times",
StrikeLimit: 10,
ExpireBase: time.Second * 15,
Sentence: time.Minute * 1,
}
// add the rule to the stack
err := st.AddRule(ru)
if err != nil {
panic(err)
}
After all the rules are created and added, we call the CreateDirectors() method. We pass this method a int32 value indicating the Capacity of our Studio.
// creates the Directors who act as the Buckets in our sharding cache
err := st.CreateDirectors(256)
if err != nil {
log.Fatal(err)
}
Your last step for initializing BadActor is to setup your polling duration and begin running the Reaper.
//poll duration
dur := time.Minute * time.Duration(60)
// Start the reaper
st.StartReaper()
Next we will create our HttpRouter and our Negroni middleware handler.
// router
router := httprouter.New()
// middleware
n := negroni.Classic()
To keep things clean, lets create a LoginHandler function.
What this very naive example LoginHandler does is -
- It stores the username and password into two variables
- Next it snags the requestor’s IP address which we will use as the Actor’s Name (aka the Key)
- Then it validates the username and password
- If the username and password is correct, it redirects sending a StatusOK code
- If the username and password is incorrect, it increments the Infraction for the Actor based on the “Login” rule we create, then it prints the Strikes (just for clarity) and redirects send a StatusUnathuorized code
// this is a naive login function for example purposes
func LoginHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var err error
// get the username and password
un := r.FormValue("username")
pw := r.FormValue("password")
// snag the IP for use as the actor's name
an, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
panic(err)
}
// mock authentication, not safe, don't do this in a production env
if un == "example_user" && pw == "example_pass" {
http.Redirect(w, r, "", http.StatusOK)
return
}
// auth fails, increment infraction
err = st.Infraction(an, "Login")
if err != nil {
log.Printf("[%v] has err %v", an, err)
}
// this will display the Strike count
i, err := st.Strikes(an, "Login")
log.Printf("%v has %v strikes, %v\n", an, i , err)
http.Redirect(w, r, "", http.StatusUnauthorized)
return
}
We now need to implement some middleware for Negroni to utilize. The middleware does the following.
- It snags the requestor’s IP address for use as the Actor’s name.
- It checks to see if the Actor is jailed.
- If the Actor is jailed, throw an error and send the StatusUnathorized code
- If the Actor is NOT jailed, continue processing the request as normal
type BadActorMiddleware struct {
negroni.Handler
}
func NewBadActorMiddleware() *BadActorMiddleware {
return &BadActorMiddleware{}
}
func (bam *BadActorMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// snag the IP for use as the actor's name
an, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
panic(err)
}
// if the Actor is jailed, send them StatusUnauthorized
if st.IsJailed(an) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// call the next middleware in the chain
next(w, r)
}
Finally run your main.go application.
➜ badactorTest git:(master) ✗ go run main.go
Then run the following from the command line, at least 10 times, to simulate someone trying to login with the wrong credentials.
$ curl --data "username=badusername&password=badpassword" http://localhost:9999/login
You’ll recieve the following output.
➜ badactorTest git:(master) ✗ go run main.go
[negroni] listening on :9999
2015/01/10 18:22:48 [::1] has 1 Strikes <nil>
[negroni] Completed 401 Unauthorized in 89.348µs
[negroni] Started POST /login
2015/01/10 18:22:49 [::1] has 2 Strikes <nil>
[negroni] Completed 401 Unauthorized in 81.692µs
[negroni] Started POST /login
2015/01/10 18:22:49 [::1] has 3 Strikes <nil>
[negroni] Completed 401 Unauthorized in 65.213µs
[negroni] Started POST /login
2015/01/10 18:22:50 [::1] has 4 Strikes <nil>
[negroni] Completed 401 Unauthorized in 86µs
[negroni] Started POST /login
2015/01/10 18:22:50 [::1] has 5 Strikes <nil>
[negroni] Completed 401 Unauthorized in 85.37µs
[negroni] Started POST /login
2015/01/10 18:22:51 [::1] has 6 Strikes <nil>
[negroni] Completed 401 Unauthorized in 81.869µs
[negroni] Started POST /login
2015/01/10 18:22:51 [::1] has 7 Strikes <nil>
[negroni] Completed 401 Unauthorized in 105.735µs
[negroni] Started POST /login
2015/01/10 18:22:52 [::1] has 8 Strikes <nil>
[negroni] Completed 401 Unauthorized in 118.018µs
[negroni] Started POST /login
2015/01/10 18:22:52 [::1] has 9 Strikes <nil>
[negroni] Completed 401 Unauthorized in 71.321µs
[negroni] Started POST /login
2015/01/10 18:22:53 [::1] has 0 Strikes director.Strikes() failed, Infraction does not exists
[negroni] Completed 401 Unauthorized in 130.181µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 29.554µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 90.571µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 23.424µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 24.39µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 24.329µs
[negroni] Started POST /login
[negroni] Completed 404 Not Found in 23.048µs
By continuing to try and Login you keep increasing the Infraction count, once it goes past 10 you break the threshold, as defined by our rule, and the Actor is jailed.
The sentence time is 1 minute.
During that period the middleware intercepts the requestor, sees that they are Jailed, and returns a 404 Not Found.
The code is on github.