Skip to content

Commit

Permalink
feat(service): add Nonced middleware and add IsNonced
Browse files Browse the repository at this point in the history
The Nonced middleware wraps a handler into NoncedHandlerFunc
requiring any request to have a valid nonce or any condition that
makes the service IsNonced false.

The IsNonced method added to the service is a way to unblock
requests from a handler or function to be allowed to proceed
without a nonce validation. This method was added to allow a
handler to have a request that will provide a new nonce.

IsNonced can be used for requests where nonce is not necessary
after a validation.

Refs: #2
  • Loading branch information
piraz committed Jul 21, 2024
1 parent d516189 commit 7176cc6
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 114 deletions.
35 changes: 19 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# Candango Go Peasant

Peasant is a protocol abstraction of how to control agents that need to
communicate with a central entity or entities.

We define agents as peasants and central entities (bases) as bastions.

This project won't define the implementation, security level neither levels of
redundancies but instead a minimal contract of what should be implemented.

A bastion/peasant relationship could be defined as stateful or not. If stateful
it is necessary to implement a session control in the bastion where peasants
need to perform knocks (as knock at the door) for permission or a valid session.
In a stateless case we just ignore any knock implementation.

What must be implemented in the protocol are nonce generation, consumption and
validation on both sides and a directory list of available resources offered by
a bastion for peasants to consume.
Peasant Protocol: A Contract for Controlling Agents

The Peasant protocol is a high-level abstraction designed to facilitate
communication between agents (peasants) and central entities (bastions). It
does not impose specific implementation details, security requirements, or
redundancy levels, but instead establishes a minimal contract for what must be
implemented.

In this protocol, agents are referred to as peasants, while central entities
are called bastions. The relationship between a bastion and peasant can be
either stateful or stateless. In a stateful scenario, bastions must implement
a session control mechanism, requiring peasants to perform "knocks" (similar
to knocking on a door) to request permission or establish a valid session. In a
stateless scenario, the concept of knocking is ignored.

The Peasant protocol mandates the implementation of nonce generation,
consumption and validation on both the peasant and bastion sides.
Additionally, bastions must provide a directory list of available resources
that peasants can consume.

## Support

Expand Down
17 changes: 17 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package peasant

import (
"net/http"
)

// Nonced is a middleware that verifies the presence of a valid nonce in a
// request.
// If the nonce is not provided or is invalid, it prevents the request from
// proceeding.
func Nonced(next http.Handler, s NonceService) http.Handler {
return http.HandlerFunc(NoncedHandlerFunc(s,
func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
}),
)
}
79 changes: 79 additions & 0 deletions middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package peasant

import (
"net/http"
"testing"
"time"

"github.com/candango/gopeasant/testrunner"
"github.com/stretchr/testify/assert"
)

func NewNoncedServeMux(t *testing.T) http.Handler {
s := &InMemoryNonceService{
nonceMap: make(map[string]*interface{}),
t: t,
}
nonced := &NoncedHandler{
service: s,
}
h := http.NewServeMux()
h.HandleFunc("/new-nonce", nonced.getNonce)
h.HandleFunc("/do-nonced-something", nonced.doNoncedFunc)
return Nonced(h, s)
}

func TestNoncedServer(t *testing.T) {
handler := NewNoncedServeMux(t)
runner := testrunner.NewHttpTestRunner(t).WithHandler(handler)

t.Run("Retrieve a new nonce", func(t *testing.T) {
t.Run("Request OK", func(t *testing.T) {
res, err := runner.WithPath("/new-nonce").Head()
if err != nil {
t.Error(err)
}

assert.Equal(t, "200 OK", res.Status)
assert.Equal(t, http.NoBody, res.Body)
assert.Equal(t, 32, len(res.Header.Get("nonce")))
})
})

t.Run("Run a nonced function", func(t *testing.T) {
t.Run("Request OK", func(t *testing.T) {
res, err := runner.WithPath("/new-nonce").Head()
if err != nil {
t.Error(err)
}
nonce := res.Header.Get("nonce")

res, err = runner.WithPath(
"/do-nonced-something").WithHeader("nonce", nonce).Get()
if err != nil {
t.Error(err)
}

assert.Equal(t, "200 OK", res.Status)
assert.Equal(t, "Func done with nonce "+nonce,
testrunner.BodyAsString(t, res))
assert.Equal(t, 32, len(res.Header.Get("nonce")))
})
t.Run("Expired nonce", func(t *testing.T) {
res, err := runner.WithPath("/new-nonce").Head()
if err != nil {
t.Error(err)
}
nonce := res.Header.Get("nonce")

time.Sleep(250 * time.Millisecond)
res, err = runner.WithPath(
"/do-nonced-something").WithHeader("nonce", nonce).Get()
if err != nil {
t.Error(err)
}

assert.Equal(t, "403 Forbidden", res.Status)
})
})
}
101 changes: 47 additions & 54 deletions service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package peasant

import "net/http"
import (
"net/http"
)

// NonceService defines methods for managing nonces in HTTP requests.
// It provides functionality for blocking, clearing, consuming, getting,
Expand All @@ -19,85 +21,76 @@ type NonceService interface {
// Consume processes the nonce associated with the specified key and
// returns a boolean indicating whether the nonce was successfully
// consumed, along with any error encountered.
// If the specified key is not present in the map, the method will return
// false without performing any action.
Consume(http.ResponseWriter, *http.Request) (bool, error)
// If nonce connot be consumed header sould be set with the respective http
// error code.
Consume(http.ResponseWriter, *http.Request) error

// GetNonce generates a new nonce, and stores it for a future validation.
// It returns the nonce as a string and an error if any occurred during
// the nonce generation or header update.
GetNonce(*http.Request) (string, error)

// Provided verifies the presence of a valid nonce in the specified HTTP
// IsNonced return if the request should be nonced or not.
IsNonced(*http.Request) bool

// IsProvided verifies the presence of a valid nonce in the specified HTTP
// request.
//
// If the nonce is not provided or is invalid, it sets the response HTTP
// status to "Unauthorized", "Forbidden", or another appropriate status
// based on the specific conditions and checks performed within the method.
// It returns a boolean indicating whether the nonce was provided and
// valid, along with any error encountered.
Provided(http.ResponseWriter, *http.Request) (bool, error)
// If nonce is not provided header sould be set with the respective http
// error code.
IsProvided(http.ResponseWriter, *http.Request) error
}

func NoncedHandlerFunc(s NonceService,
f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(res http.ResponseWriter, req *http.Request) {
ok, err := s.Provided(res, req)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
type WrappedWriter struct {
http.ResponseWriter
StatusCode int
}

func (w *WrappedWriter) WriteHeader(c int) {
w.ResponseWriter.WriteHeader(c)
w.StatusCode = c
}

func NoncedHandlerFunc(
s NonceService, f func(http.ResponseWriter, *http.Request),
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !s.IsNonced(r) {
f(w, r)
return
}
if !ok {
return
wrapped := &WrappedWriter{
ResponseWriter: w,
StatusCode: http.StatusOK,
}
ok, err = s.Consume(res, req)
err := s.IsProvided(wrapped, r)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
wrapped.WriteHeader(http.StatusInternalServerError)
return
}
if !ok {
if wrapped.StatusCode >= 300 {
return
}
nonce, err := s.GetNonce(req)
err = s.Consume(wrapped, r)
if err != nil {
res.WriteHeader(http.StatusInternalServerError)
wrapped.WriteHeader(http.StatusInternalServerError)
return
}
res.Header().Add("nonce", nonce)
f(res, req)
}
}

// Nonced processes the provided HTTP request to verify and consume a
// valid nonce. It returns an error if any occurred during the process.
func Nonced(res http.ResponseWriter, req *http.Request,
service NonceService) (err error) {
ok, err := service.Provided(res, req)
if err != nil {
return err
}
if !ok {
err = service.Block(res, req)
if err != nil {
return err
if wrapped.StatusCode >= 300 {
return
}
return nil
}
nonce, err := service.GetNonce(req)
if err != nil {
return err
}

ok, err = service.Consume(res, req)
if err != nil {
return err
}
if ok {
err = service.Clear(nonce)
nonce, err := s.GetNonce(r)
if err != nil {
return err
wrapped.WriteHeader(http.StatusInternalServerError)
return
}
if wrapped.StatusCode >= 300 {
return
}
wrapped.Header().Add("nonce", nonce)
f(wrapped, r)
}

return nil
}
Loading

0 comments on commit 7176cc6

Please sign in to comment.