From 3c1ba07755a39e43de03fa8b31454b5b51ab715c Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 6 Sep 2016 15:39:47 +0200 Subject: [PATCH 001/127] Added testing of issuer claim methods --- jwt/claims_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/jwt/claims_test.go b/jwt/claims_test.go index c36fd9a..e978650 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -230,6 +230,26 @@ func TestClaimsIssuedAt(t *testing.T) { assert.False(ok) } +// TestClaimsIssuer checks the setting, getting, and +// deleting of the issuer at claim. +func TestClaimsIssuer(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing claim \"iss\"") + issuer := "foo" + claims := jwt.NewClaims() + iss, ok := claims.Issuer() + assert.False(ok) + none := claims.SetIssuer(issuer) + assert.Equal(none, "") + iss, ok = claims.Issuer() + assert.Equal(iss, issuer) + assert.True(ok) + old := claims.DeleteIssuer() + assert.Equal(old, iss) + iss, ok = claims.Issuer() + assert.False(ok) +} + // TestClaimsNotBefore checks the setting, getting, and // deleting of the not before claim. func TestClaimsNotBefore(t *testing.T) { From db6ccb0746280ce5cedd948226df07bd86913e71 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 6 Sep 2016 17:38:42 +0200 Subject: [PATCH 002/127] Added test for identifier claim --- jwt/claims_test.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/jwt/claims_test.go b/jwt/claims_test.go index e978650..d6fc96a 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -210,6 +210,26 @@ func TestClaimsExpiration(t *testing.T) { assert.False(ok) } +// TestClaimsIdentifier checks the setting, getting, and +// deleting of the identifier claim. +func TestClaimsIdentifier(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing claim \"jti\"") + identifier := "foo" + claims := jwt.NewClaims() + jti, ok := claims.Identifier() + assert.False(ok) + none := claims.SetIdentifier(identifier) + assert.Equal(none, "") + jti, ok = claims.Identifier() + assert.Equal(jti, identifier) + assert.True(ok) + old := claims.DeleteIdentifier() + assert.Equal(old, jti) + jti, ok = claims.Identifier() + assert.False(ok) +} + // TestClaimsIssuedAt checks the setting, getting, and // deleting of the issued at claim. func TestClaimsIssuedAt(t *testing.T) { @@ -231,7 +251,7 @@ func TestClaimsIssuedAt(t *testing.T) { } // TestClaimsIssuer checks the setting, getting, and -// deleting of the issuer at claim. +// deleting of the issuer claim. func TestClaimsIssuer(t *testing.T) { assert := audit.NewTestingAssertion(t, true) assert.Logf("testing claim \"iss\"") From 0c7d78a91c9c31c4d04945e5d266ce5d5e487ab3 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 6 Sep 2016 20:56:53 +0200 Subject: [PATCH 003/127] Added tests for audience and subject --- jwt/claims_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/jwt/claims_test.go b/jwt/claims_test.go index d6fc96a..8b4ba04 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -190,6 +190,26 @@ func TestClaimsTime(t *testing.T) { assert.False(ok) } +// TestClaimsAudience checks the setting, getting, and +// deleting of the audience claim. +func TestClaimsAudience(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing claim \"aud\"") + audience := []string{"foo", "bar", "baz"} + claims := jwt.NewClaims() + aud, ok := claims.Audience() + assert.False(ok) + none := claims.SetAudience(audience...) + assert.Equal(none, "") + aud, ok = claims.Audience() + assert.Equal(aud, audience) + assert.True(ok) + old := claims.DeleteAudience() + assert.Equal(old, aud) + aud, ok = claims.Audience() + assert.False(ok) +} + // TestClaimsExpiration checks the setting, getting, and // deleting of the expiration claim. func TestClaimsExpiration(t *testing.T) { @@ -290,6 +310,26 @@ func TestClaimsNotBefore(t *testing.T) { assert.False(ok) } +// TestClaimsSubject checks the setting, getting, and +// deleting of the subject claim. +func TestClaimsSubject(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing claim \"sub\"") + subject := "foo" + claims := jwt.NewClaims() + sub, ok := claims.Subject() + assert.False(ok) + none := claims.SetSubject(subject) + assert.Equal(none, "") + sub, ok = claims.Subject() + assert.Equal(sub, subject) + assert.True(ok) + old := claims.DeleteSubject() + assert.Equal(old, sub) + sub, ok = claims.Subject() + assert.False(ok) +} + // TestClaimsValidity checks the validation of the not before // and the expiring time. func TestClaimsValidity(t *testing.T) { From 41e105b03245620b5595d40c3aa18afc9c2d0789 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 6 Sep 2016 19:30:27 +0000 Subject: [PATCH 004/127] Fixed newly added tests --- README.md | 2 +- jwt/claims_test.go | 2 +- jwt/doc.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 274e288..ad98a38 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-05 +Version 2.0.0 beta 2016-09-06 ## Packages diff --git a/jwt/claims_test.go b/jwt/claims_test.go index 8b4ba04..e294808 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -200,7 +200,7 @@ func TestClaimsAudience(t *testing.T) { aud, ok := claims.Audience() assert.False(ok) none := claims.SetAudience(audience...) - assert.Equal(none, "") + assert.Length(none, 0) aud, ok = claims.Audience() assert.Equal(aud, audience) assert.True(ok) diff --git a/jwt/doc.go b/jwt/doc.go index 2697728..9e93b0d 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-05") + return version.New(2, 0, 0, "beta", "2016-09-06") } // EOF From e054cdce11c5deb410a5bce8643bde75cd0d0834 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 6 Sep 2016 20:14:02 +0000 Subject: [PATCH 005/127] Started implementation of token cache The cache shall help to only unmarshall the payload once. --- jwt/cache.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 jwt/cache.go diff --git a/jwt/cache.go b/jwt/cache.go new file mode 100644 index 0000000..b5feffb --- /dev/null +++ b/jwt/cache.go @@ -0,0 +1,56 @@ +// Tideland Go REST Server Library - JSON Web Token - Cache +// +// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package jwt + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "sync" + "time" +) + +//-------------------- +// CACHE +//-------------------- + +// Cache provides a caching for tokens so that these +// don't have to be decoded or verified multiple times. +type Cache interface { + // Get tries to retrieve a token from the cache. + Get(token string) (JWT, bool) + + // Put adds a token to the cache. + Put(jwt JWT) +} + +// cacheEntry manages a token and its access time. +type cacheEntry struct { + jwt JWT + accessed time.Time +} + +// cache implements Cache. +type cache struct { + mutex sync.RWMutex + entries map[string]*cacheEntry +} + +func (c *cache) Get(token string) (JWT, bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + entry, ok := c.entries[token] + if !ok { + return nil, false + } + // TODO Check claims and their validity. + return entry.jwt, true +} + +// EOF From 2a72fbb988ab97339bd04b752b1457babbe7b3eb Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 7 Sep 2016 21:35:44 +0200 Subject: [PATCH 006/127] Only added one comment and empty function --- jwt/cache.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jwt/cache.go b/jwt/cache.go index b5feffb..9eb3bc8 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -42,6 +42,7 @@ type cache struct { entries map[string]*cacheEntry } +// Get implements the Cache interface. func (c *cache) Get(token string) (JWT, bool) { c.mutex.RLock() defer c.mutex.RUnlock() @@ -53,4 +54,11 @@ func (c *cache) Get(token string) (JWT, bool) { return entry.jwt, true } +// Put implements the Cache interface. +func (c *cache) Put(jwt JWT) { + c.mutex.Lock() + defer c.mutex.Unlock() + delete(c.entries, jwt.Token()) +} + // EOF From 9915362adc68ee9fb170f0b0d1999692b6d7de05 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 7 Sep 2016 22:46:26 +0200 Subject: [PATCH 007/127] Changed from generic payloads to claims Not yet complete testing, may not compile. --- jwt/doc.go | 2 +- jwt/jwt.go | 48 +++++++++++----------- jwt/jwt_test.go | 103 +++++++++++++++++++++--------------------------- 3 files changed, 69 insertions(+), 84 deletions(-) diff --git a/jwt/doc.go b/jwt/doc.go index 9e93b0d..7aacaf7 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-06") + return version.New(2, 0, 0, "beta", "2016-09-07") } // EOF diff --git a/jwt/jwt.go b/jwt/jwt.go index 02319d2..a9fd951 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -32,9 +32,8 @@ type JWT interface { // Stringer provides the String() method. fmt.Stringer - // Payload returns the payload of the token, - // normally claims. - Payload() interface{} + // Claims returns the claims payload of the token. + Claims() Claims // Key return the key of the token only when // it is a result of encoding or verification. @@ -51,17 +50,17 @@ type jwtHeader struct { } type jwt struct { - payload interface{} + claims Claims key Key algorithm Algorithm token string } -// Encodes creates a JSON Web Token for the given payload +// Encodes creates a JSON Web Token for the given claims // based on key and algorithm. -func Encode(payload interface{}, key Key, algorithm Algorithm) (JWT, error) { +func Encode(claims Claims, key Key, algorithm Algorithm) (JWT, error) { jwt := &jwt{ - payload: payload, + claims: claims, key: key, algorithm: algorithm, } @@ -69,11 +68,11 @@ func Encode(payload interface{}, key Key, algorithm Algorithm) (JWT, error) { if err != nil { return nil, errors.Annotate(err, ErrCannotEncode, errorMessages, "header") } - payloadPart, err := marshallAndEncode(payload) + claimsPart, err := marshallAndEncode(claims) if err != nil { - return nil, errors.Annotate(err, ErrCannotEncode, errorMessages, "payload") + return nil, errors.Annotate(err, ErrCannotEncode, errorMessages, "claims") } - dataParts := headerPart + "." + payloadPart + dataParts := headerPart + "." + claimsPart signaturePart, err := signAndEncode([]byte(dataParts), key, algorithm) if err != nil { return nil, errors.Annotate(err, ErrCannotEncode, errorMessages, "signature") @@ -82,9 +81,8 @@ func Encode(payload interface{}, key Key, algorithm Algorithm) (JWT, error) { return jwt, nil } -// Decode creates a token out of a string without verification. The passed -// payload is used for the unmarshalling of the payload part. -func Decode(token string, payload interface{}) (JWT, error) { +// Decode creates a token out of a string without verification. +func Decode(token string) (JWT, error) { parts := strings.Split(token, ".") if len(parts) != 3 { return nil, errors.New(ErrCannotDecode, errorMessages, "parts") @@ -94,20 +92,21 @@ func Decode(token string, payload interface{}) (JWT, error) { if err != nil { return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "header") } - err = decodeAndUnmarshall(parts[1], payload) + var claims Claims + err = decodeAndUnmarshall(parts[1], claims) if err != nil { - return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "payload") + return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "claims") } return &jwt{ - payload: payload, + claims: claims, algorithm: Algorithm(header.Algorithm), token: token, }, nil } // Verify creates a token out of a string and varifies it against -// the passed key. Like in Decode() the payload is used for unmarshalling. -func Verify(token string, payload interface{}, key Key) (JWT, error) { +// the passed key. +func Verify(token string, key Key) (JWT, error) { parts := strings.Split(token, ".") if len(parts) != 3 { return nil, errors.New(ErrCannotVerify, errorMessages, "parts") @@ -121,21 +120,22 @@ func Verify(token string, payload interface{}, key Key) (JWT, error) { if err != nil { return nil, errors.Annotate(err, ErrCannotVerify, errorMessages, "signature") } - err = decodeAndUnmarshall(parts[1], payload) + var claims Claims + err = decodeAndUnmarshall(parts[1], claims) if err != nil { - return nil, errors.Annotate(err, ErrCannotVerify, errorMessages, "payload") + return nil, errors.Annotate(err, ErrCannotVerify, errorMessages, "claims") } return &jwt{ - payload: payload, + claims: claims, key: key, algorithm: Algorithm(header.Algorithm), token: token, }, nil } -// Payload implements the JWT interface. -func (jwt *jwt) Payload() interface{} { - return jwt.payload +// Claims implements the JWT interface. +func (jwt *jwt) Claims() Claims { + return jwt.claims } // Key implements the JWT interface. diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index 2e470ac..911b84b 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -32,12 +32,6 @@ import ( //-------------------- var ( - payload = testPayload{ - Sub: "1234567890", - Name: "John Doe", - Admin: true, - } - esTests = []jwt.Algorithm{jwt.ES256, jwt.ES384, jwt.ES512} hsTests = []jwt.Algorithm{jwt.HS256, jwt.HS384, jwt.HS512} psTests = []jwt.Algorithm{jwt.PS256, jwt.PS384, jwt.PS512} @@ -50,22 +44,20 @@ func TestESAlgorithms(t *testing.T) { assert := audit.NewTestingAssertion(t, true) privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) assert.Nil(err) + claims := initClaims() for _, test := range esTests { assert.Logf("testing algorithm %q", test) // Encode. - jwtEncode, err := jwt.Encode(payload, privateKey, test) + jwtEncode, err := jwt.Encode(claims, privateKey, test) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) // Verify. - var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, privateKey.Public()) + jwtVerify, err := jwt.Verify(jwtEncode.String(), privateKey.Public()) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } } @@ -74,22 +66,20 @@ func TestESAlgorithms(t *testing.T) { func TestHSAlgorithms(t *testing.T) { assert := audit.NewTestingAssertion(t, true) key := []byte("secret") + claims := initClaims() for _, test := range hsTests { assert.Logf("testing algorithm %q", test) // Encode. - jwtEncode, err := jwt.Encode(payload, key, test) + jwtEncode, err := jwt.Encode(claims, key, test) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) // Verify. - var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, key) + jwtVerify, err := jwt.Verify(jwtEncode.String(), key) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } } @@ -99,6 +89,7 @@ func TestPSAlgorithms(t *testing.T) { assert := audit.NewTestingAssertion(t, true) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) assert.Nil(err) + claims := initClaims() for _, test := range psTests { assert.Logf("testing algorithm %q", test) // Encode. @@ -107,14 +98,11 @@ func TestPSAlgorithms(t *testing.T) { parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) // Verify. - var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, privateKey.Public()) + jwtVerify, err := jwt.Verify(jwtEncode.String(), privateKey.Public()) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } } @@ -124,22 +112,21 @@ func TestRSAlgorithms(t *testing.T) { assert := audit.NewTestingAssertion(t, true) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) assert.Nil(err) + claims := initClaims() for _, test := range rsTests { assert.Logf("testing algorithm %q", test) // Encode. - jwtEncode, err := jwt.Encode(payload, privateKey, test) + jwtEncode, err := jwt.Encode(claims, privateKey, test) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) // Verify. var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, privateKey.Public()) + jwtVerify, err := jwt.Verify(jwtEncode.String(), privateKey.Public()) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } } @@ -149,20 +136,18 @@ func TestNoneAlgorithm(t *testing.T) { assert := audit.NewTestingAssertion(t, true) assert.Logf("testing algorithm \"none\"") // Encode. - jwtEncode, err := jwt.Encode(payload, "", jwt.NONE) + claims := initClaims() + jwtEncode, err := jwt.Encode(claims, "", jwt.NONE) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) assert.Equal(parts[2], "") // Verify. - var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, "") + jwtVerify, err := jwt.Verify(jwtEncode.String(), "") assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } // TestNotMatchingAlgorithm @@ -176,6 +161,7 @@ func TestNotMatchingAlgorithm(t *testing.T) { rsPublicKey := rsPrivateKey.Public() assert.Nil(err) noneKey := "" + claims := initClaims() errorMatch := ".* combination of algorithm .* and key type .*" tests := []struct { description string @@ -199,14 +185,13 @@ func TestNotMatchingAlgorithm(t *testing.T) { for _, test := range tests { assert.Logf("testing %q algorithm key type mismatch", test.description) for _, key := range test.encodeKeys { - _, err = jwt.Encode(payload, key, test.algorithm) + _, err = jwt.Encode(claims, key, test.algorithm) assert.ErrorMatch(err, errorMatch) } jwtEncode, err := jwt.Encode(payload, test.key, test.algorithm) assert.Nil(err) for _, key := range test.verifyKeys { - var verifyPayload testPayload - _, err = jwt.Verify(jwtEncode.String(), &verifyPayload, key) + _, err = jwt.Verify(jwtEncode.String(), key) assert.ErrorMatch(err, errorMatch) } } @@ -243,18 +228,16 @@ func TestESTools(t *testing.T) { publicKeyOut, err := jwt.ReadECPublicKey(buf) assert.Nil(err) // And as a last step check if they are correctly usable. - jwtEncode, err := jwt.Encode(payload, privateKeyOut, jwt.ES512) + claims := initClaims() + jwtEncode, err := jwt.Encode(claims, privateKeyOut, jwt.ES512) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) - var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, publicKeyOut) + jwtVerify, err := jwt.Verify(jwtEncode.String(), publicKeyOut) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } // TestRSTools tests the tools for the reading of PEM encoded @@ -287,7 +270,8 @@ func TestRSTools(t *testing.T) { publicKeyOut, err := jwt.ReadRSAPublicKey(buf) assert.Nil(err) // And as a last step check if they are correctly usable. - jwtEncode, err := jwt.Encode(payload, privateKeyOut, jwt.RS512) + claims := initClaims() + jwtEncode, err := jwt.Encode(claims, privateKeyOut, jwt.RS512) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) @@ -296,9 +280,7 @@ func TestRSTools(t *testing.T) { assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) - assert.Equal(payload.Sub, verifyPayload.Sub) - assert.Equal(payload.Name, verifyPayload.Name) - assert.Equal(payload.Admin, verifyPayload.Admin) + testClaims(assert, jwtVerify.Claims()) } // TestDecode tests the decoding without verifying the signature. @@ -306,36 +288,39 @@ func TestDecode(t *testing.T) { assert := audit.NewTestingAssertion(t, true) privateKey, err := rsa.GenerateKey(rand.Reader, 2048) assert.Nil(err) + claims := initClaims() assert.Logf("testing decoding without verifying") // Encode. - jwtEncode, err := jwt.Encode(payload, privateKey, jwt.RS512) + jwtEncode, err := jwt.Encode(claims, privateKey, jwt.RS512) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) // Decode. - var decodePayload testPayload - jwtDecode, err := jwt.Decode(jwtEncode.String(), &decodePayload) + jwtDecode, err := jwt.Decode(jwtEncode.String()) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtDecode.Algorithm()) key, err := jwtDecode.Key() assert.Nil(key) assert.ErrorMatch(err, ".*no key available, only after encoding or verifying.*") assert.Equal(jwtEncode.String(), jwtDecode.String()) - assert.Equal(payload.Sub, decodePayload.Sub) - assert.Equal(payload.Name, decodePayload.Name) - assert.Equal(payload.Admin, decodePayload.Admin) + testClaims(assert, jwtVerify.Claims()) } //-------------------- // HELPERS //-------------------- -// testPayload is used as payload instead of claims -// for stable mashalling. -type testPayload struct { - Sub string `json:"sub"` - Name string `json:"name"` - Admin bool `json:"admin"` +// initClaims creates test claims. +func initClaims() Claims { + claims := NewClaims() + claims.SetSubject("1234567890") + claims.Set("name", "John Doe") + claims.Set("admin" true) + return claims +} + +// testClaims checks the passed claims. +func testClaims(assert audit.Assertion, claims Claims) { } // EOF From fa3c15d99488ab73543ce0a3b609a20fa44959c2 Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 8 Sep 2016 11:39:44 +0200 Subject: [PATCH 008/127] Finished the payload migration to Claims and added bool handling to Claims --- README.md | 2 +- jwt/cache.go | 2 +- jwt/claims.go | 19 +++++++++++++++++++ jwt/claims_test.go | 28 ++++++++++++++++++++++++++++ jwt/doc.go | 2 +- jwt/jwt.go | 4 ++-- jwt/jwt_test.go | 27 +++++++++++++++++---------- 7 files changed, 69 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ad98a38..d7c6013 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-06 +Version 2.0.0 beta 2016-09-08 ## Packages diff --git a/jwt/cache.go b/jwt/cache.go index 9eb3bc8..e696410 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -58,7 +58,7 @@ func (c *cache) Get(token string) (JWT, bool) { func (c *cache) Put(jwt JWT) { c.mutex.Lock() defer c.mutex.Unlock() - delete(c.entries, jwt.Token()) + delete(c.entries, jwt.String()) } // EOF diff --git a/jwt/claims.go b/jwt/claims.go index 9a4ec03..a9f60af 100644 --- a/jwt/claims.go +++ b/jwt/claims.go @@ -63,6 +63,25 @@ func (c Claims) GetString(key string) (string, bool) { return fmt.Sprintf("%v", value), true } +// GetBool retrieves a bool value. It also accepts the +// strings "1", "t", "T", "TRUE", "true", "True", "0", +// "f", "F", "FALSE", "false", and "False". +func (c Claims) GetBool(key string) (bool, bool) { + value, ok := c.Get(key) + if !ok { + return false, false + } + if b, ok := value.(bool); ok { + return b, true + } + if str, ok := value.(string); ok { + if b, err := strconv.ParseBool(str); err == nil { + return b, true + } + } + return false, false +} + // GetInt retrieves an integer value. func (c Claims) GetInt(key string) (int, bool) { value, ok := c.Get(key) diff --git a/jwt/claims_test.go b/jwt/claims_test.go index e294808..a355931 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -117,6 +117,34 @@ func TestClaimsString(t *testing.T) { assert.True(ok) } +// TestClaimsBool tests the bool operations +// on claims. +func TestClaimsBool(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing claims bool handling") + claims := jwt.NewClaims() + claims.Set("foo", true) + claims.Set("bar", false) + claims.Set("baz", "T") + claims.Set("bingo", "0") + claims.Set("yadda", "nope") + foo, ok := claims.GetBool("foo") + assert.True(foo) + assert.True(ok) + bar, ok := claims.GetBool("bar") + assert.False(bar) + assert.True(ok) + baz, ok := claims.GetBool("baz") + assert.True(baz) + assert.True(ok) + bingo, ok := claims.GetBool("bingo") + assert.False(bingo) + assert.True(ok) + yadda, ok := claims.GetBool("yadda") + assert.False(yadda) + assert.False(ok) +} + // TestClaimsInt tests the int operations // on claims. func TestClaimsInt(t *testing.T) { diff --git a/jwt/doc.go b/jwt/doc.go index 7aacaf7..b66af17 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-07") + return version.New(2, 0, 0, "beta", "2016-09-08") } // EOF diff --git a/jwt/jwt.go b/jwt/jwt.go index a9fd951..0a4d516 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -93,7 +93,7 @@ func Decode(token string) (JWT, error) { return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "header") } var claims Claims - err = decodeAndUnmarshall(parts[1], claims) + err = decodeAndUnmarshall(parts[1], &claims) if err != nil { return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "claims") } @@ -121,7 +121,7 @@ func Verify(token string, key Key) (JWT, error) { return nil, errors.Annotate(err, ErrCannotVerify, errorMessages, "signature") } var claims Claims - err = decodeAndUnmarshall(parts[1], claims) + err = decodeAndUnmarshall(parts[1], &claims) if err != nil { return nil, errors.Annotate(err, ErrCannotVerify, errorMessages, "claims") } diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index 911b84b..9bda8ad 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -93,7 +93,7 @@ func TestPSAlgorithms(t *testing.T) { for _, test := range psTests { assert.Logf("testing algorithm %q", test) // Encode. - jwtEncode, err := jwt.Encode(payload, privateKey, test) + jwtEncode, err := jwt.Encode(claims, privateKey, test) assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) @@ -121,7 +121,6 @@ func TestRSAlgorithms(t *testing.T) { parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) // Verify. - var verifyPayload testPayload jwtVerify, err := jwt.Verify(jwtEncode.String(), privateKey.Public()) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) @@ -188,7 +187,7 @@ func TestNotMatchingAlgorithm(t *testing.T) { _, err = jwt.Encode(claims, key, test.algorithm) assert.ErrorMatch(err, errorMatch) } - jwtEncode, err := jwt.Encode(payload, test.key, test.algorithm) + jwtEncode, err := jwt.Encode(claims, test.key, test.algorithm) assert.Nil(err) for _, key := range test.verifyKeys { _, err = jwt.Verify(jwtEncode.String(), key) @@ -275,8 +274,7 @@ func TestRSTools(t *testing.T) { assert.Nil(err) parts := strings.Split(jwtEncode.String(), ".") assert.Length(parts, 3) - var verifyPayload testPayload - jwtVerify, err := jwt.Verify(jwtEncode.String(), &verifyPayload, publicKeyOut) + jwtVerify, err := jwt.Verify(jwtEncode.String(), publicKeyOut) assert.Nil(err) assert.Equal(jwtEncode.Algorithm(), jwtVerify.Algorithm()) assert.Equal(jwtEncode.String(), jwtVerify.String()) @@ -303,7 +301,7 @@ func TestDecode(t *testing.T) { assert.Nil(key) assert.ErrorMatch(err, ".*no key available, only after encoding or verifying.*") assert.Equal(jwtEncode.String(), jwtDecode.String()) - testClaims(assert, jwtVerify.Claims()) + testClaims(assert, jwtDecode.Claims()) } //-------------------- @@ -311,16 +309,25 @@ func TestDecode(t *testing.T) { //-------------------- // initClaims creates test claims. -func initClaims() Claims { - claims := NewClaims() +func initClaims() jwt.Claims { + claims := jwt.NewClaims() claims.SetSubject("1234567890") claims.Set("name", "John Doe") - claims.Set("admin" true) + claims.Set("admin", true) return claims } // testClaims checks the passed claims. -func testClaims(assert audit.Assertion, claims Claims) { +func testClaims(assert audit.Assertion, claims jwt.Claims) { + sub, ok := claims.Subject() + assert.True(ok) + assert.Equal(sub, "1234567890") + name, ok := claims.GetString("name") + assert.True(ok) + assert.Equal(name, "John Doe") + admin, ok := claims.GetBool("admin") + assert.True(ok) + assert.True(admin) } // EOF From e6572028f73e895d0df7cf3f6f2a0d890b63f4da Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 8 Sep 2016 12:09:25 +0200 Subject: [PATCH 009/127] Added convenience method IsValid to JWT --- jwt/jwt.go | 10 ++++++++++ jwt/jwt_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/jwt/jwt.go b/jwt/jwt.go index 0a4d516..4549a9d 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -16,6 +16,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/tideland/golib/errors" ) @@ -42,6 +43,10 @@ type JWT interface { // Algorithm returns the algorithm of the token // after encoding, decoding, or verification. Algorithm() Algorithm + + // IsValid is a convenience method checking the + // registered claims if the token is valid. + IsValid(leeway time.Duration) bool } type jwtHeader struct { @@ -151,6 +156,11 @@ func (jwt *jwt) Algorithm() Algorithm { return jwt.algorithm } +// IsValid implements the JWT interface. +func (jwt *jwt) IsValid(leeway time.Duration) bool { + return jwt.claims.IsValid(leeway) +} + // String implements the Stringer interface. func (jwt *jwt) String() string { return jwt.token diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index 9bda8ad..f2e00f5 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -21,6 +21,7 @@ import ( "encoding/pem" "strings" "testing" + "time" "github.com/tideland/golib/audit" @@ -304,6 +305,53 @@ func TestDecode(t *testing.T) { testClaims(assert, jwtDecode.Claims()) } +// TestIsValid checks the time validation of a token. +func TestIsValid(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing time validation") + now := time.Now() + leeway := time.Minute + key := []byte("secret") + // Create token with no times set, encode, decode, validate ok. + claims := jwt.NewClaims() + jwtEncode, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + jwtDecode, err := jwt.Decode(jwtEncode.String()) + assert.Nil(err) + ok := jwtDecode.IsValid(leeway) + assert.True(ok) + // Now a token with a long timespan, still valid. + claims = jwt.NewClaims() + claims.SetNotBefore(now.Add(-time.Hour)) + claims.SetExpiration(now.Add(time.Hour)) + jwtEncode, err = jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + jwtDecode, err = jwt.Decode(jwtEncode.String()) + assert.Nil(err) + ok = jwtDecode.IsValid(leeway) + assert.True(ok) + // Now a token with a long timespan in the past, not valid. + claims = jwt.NewClaims() + claims.SetNotBefore(now.Add(-2 * time.Hour)) + claims.SetExpiration(now.Add(-time.Hour)) + jwtEncode, err = jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + jwtDecode, err = jwt.Decode(jwtEncode.String()) + assert.Nil(err) + ok = jwtDecode.IsValid(leeway) + assert.False(ok) + // And at last a token with a long timespan in the future, not valid. + claims = jwt.NewClaims() + claims.SetNotBefore(now.Add(time.Hour)) + claims.SetExpiration(now.Add(2 * time.Hour)) + jwtEncode, err = jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + jwtDecode, err = jwt.Decode(jwtEncode.String()) + assert.Nil(err) + ok = jwtDecode.IsValid(leeway) + assert.False(ok) +} + //-------------------- // HELPERS //-------------------- From 16a881e6d372a2b2eb92ddf4c5fe909a7a49c82d Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 8 Sep 2016 13:20:27 +0200 Subject: [PATCH 010/127] Further cache implementation So far now background cleanup. --- jwt/cache.go | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/jwt/cache.go b/jwt/cache.go index e696410..dad7512 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -38,27 +38,50 @@ type cacheEntry struct { // cache implements Cache. type cache struct { - mutex sync.RWMutex + mutex sync.Mutex + cleanup time.Duration + leeway time.Duration entries map[string]*cacheEntry } +// NewCache creates a new JWT caching. It takes two +// durations. The first one is the time a token hasn't +// been used anymore before it is cleaned up. The second +// one is the leeway taken for token time validations. +func NewCache(cleanup, leeway time.Duration) Cache { + c := &cache{ + cleanup: cleanup, + leeway: leeway, + entries: map[string]*cacheEntry{}, + } + // TODO Start cleanup goroutine. + return c +} + // Get implements the Cache interface. func (c *cache) Get(token string) (JWT, bool) { - c.mutex.RLock() - defer c.mutex.RUnlock() + c.mutex.Lock() + defer c.mutex.Unlock() entry, ok := c.entries[token] if !ok { return nil, false } - // TODO Check claims and their validity. - return entry.jwt, true + if entry.jwt.IsValid(c.leeway) { + entry.accessed = time.Now() + return entry.jwt, true + } + // Remove invalid token. + delete(c.entries, token) + return nil, false } // Put implements the Cache interface. func (c *cache) Put(jwt JWT) { c.mutex.Lock() defer c.mutex.Unlock() - delete(c.entries, jwt.String()) + if jwt.IsValid(c.leeway) { + c.entries[jwt.String()] = &cacheEntry{jwt, time.Now()} + } } // EOF From c4297d650597c2fcccc748fdd2cbe563fd2ec1de Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 8 Sep 2016 14:42:10 +0200 Subject: [PATCH 011/127] Added cleanup to cache --- jwt/cache.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/jwt/cache.go b/jwt/cache.go index dad7512..d507536 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -14,6 +14,8 @@ package jwt import ( "sync" "time" + + "github.com/tideland/golib/loop" ) //-------------------- @@ -28,6 +30,9 @@ type Cache interface { // Put adds a token to the cache. Put(jwt JWT) + + // Stop tells the cache to end working. + Stop() error } // cacheEntry manages a token and its access time. @@ -39,22 +44,23 @@ type cacheEntry struct { // cache implements Cache. type cache struct { mutex sync.Mutex - cleanup time.Duration + ttl time.Duration leeway time.Duration entries map[string]*cacheEntry + loop loop.Loop } // NewCache creates a new JWT caching. It takes two // durations. The first one is the time a token hasn't // been used anymore before it is cleaned up. The second // one is the leeway taken for token time validations. -func NewCache(cleanup, leeway time.Duration) Cache { +func NewCache(ttl, leeway time.Duration) Cache { c := &cache{ - cleanup: cleanup, + ttl: ttl, leeway: leeway, entries: map[string]*cacheEntry{}, } - // TODO Start cleanup goroutine. + c.loop = loop.Go(c.backendLoop, "jwt", "cache") return c } @@ -84,4 +90,45 @@ func (c *cache) Put(jwt JWT) { } } +// Stop implements the Cache interface. +func (c *cache) Stop() error { + return c.loop.Stop() +} + +// backendLoop runs a cleaning session every five minutes. +func (c *cache) backendLoop(l loop.Loop) error { + defer func() { + // Some cleanup after stop or error. + c.ttl = 0 + c.leeway = 0 + c.entries = nil + }() + ticker := time.NewTicker(5 * time.Minute) + for { + select { + case <-l.ShallStop(): + return nil + case <-ticker.C: + c.cleanup() + } + } +} + +// cleanup checks for invalid or unused tokens. +func (c *cache) cleanup() { + c.mutex.Lock() + defer c.mutex.Unlock() + valids := map[string]*cacheEntry{} + now := time.Now() + for token, entry := range c.entries { + if entry.jwt.IsValid(c.leeway) { + if entry.accessed.Add(c.ttl).Before(now) { + // Everything fine. + valids[token] = entry + } + } + } + c.entries = valids +} + // EOF From 06cfd32a29a486b846e5ea9226340dfe0300482e Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 8 Sep 2016 21:27:30 +0200 Subject: [PATCH 012/127] Added first cache test Started to test the JWT cache. --- jwt/cache.go | 6 +++++- jwt/cache_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++ jwt/claims_test.go | 26 ++++++++++++++++++++++++ jwt/export_test.go | 34 ++++++++++++++++++++++++++++++++ jwt/jwt_test.go | 26 ------------------------ 5 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 jwt/cache_test.go create mode 100644 jwt/export_test.go diff --git a/jwt/cache.go b/jwt/cache.go index d507536..96ce8b9 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -22,6 +22,10 @@ import ( // CACHE //-------------------- +// cleanupInterval defines the timespan between +// two cleanup runs. +var cleanupInterval = 5 * time.Minute + // Cache provides a caching for tokens so that these // don't have to be decoded or verified multiple times. type Cache interface { @@ -103,7 +107,7 @@ func (c *cache) backendLoop(l loop.Loop) error { c.leeway = 0 c.entries = nil }() - ticker := time.NewTicker(5 * time.Minute) + ticker := time.NewTicker(cleanupInterval) for { select { case <-l.ShallStop(): diff --git a/jwt/cache_test.go b/jwt/cache_test.go new file mode 100644 index 0000000..6d79208 --- /dev/null +++ b/jwt/cache_test.go @@ -0,0 +1,49 @@ +// Tideland Go REST Server Library - JSON Web Token - Unit Tests +// +// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package jwt_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "testing" + "time" + + "github.com/tideland/golib/audit" + + "github.com/tideland/gorest/jwt" +) + +//-------------------- +// TESTS +//-------------------- + +// TestCachePutGet tests the putting and getting of tokens +// to the cache. +func TestCachePutGet(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing cache put and get") + cache := jwt.NewCache(time.Minute, time.Minute) + key := []byte("secret") + claims := init.Claims() + jwtIn, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + cache.Put(jwt) + token := jwtIn.String() + jwtOut, ok := cache.Get(token) + assert.True(ok) + assert.Equal(jwtIn, jwtOut) + jwtOut, ok = cache.Get("is.not.there") + assert.False(ok) + assert.Nil(jwtOut) + err := cache.Stop() + assert.Nil(err) +} + +// EOF \ No newline at end of file diff --git a/jwt/claims_test.go b/jwt/claims_test.go index a355931..14ef56d 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -404,4 +404,30 @@ func TestClaimsValidity(t *testing.T) { assert.False(valid) } +//-------------------- +// HELPERS +//-------------------- + +// initClaims creates test claims. +func initClaims() jwt.Claims { + claims := jwt.NewClaims() + claims.SetSubject("1234567890") + claims.Set("name", "John Doe") + claims.Set("admin", true) + return claims +} + +// testClaims checks the passed claims. +func testClaims(assert audit.Assertion, claims jwt.Claims) { + sub, ok := claims.Subject() + assert.True(ok) + assert.Equal(sub, "1234567890") + name, ok := claims.GetString("name") + assert.True(ok) + assert.Equal(name, "John Doe") + admin, ok := claims.GetBool("admin") + assert.True(ok) + assert.True(admin) +} + // EOF diff --git a/jwt/export_test.go b/jwt/export_test.go new file mode 100644 index 0000000..e015a3a --- /dev/null +++ b/jwt/export_test.go @@ -0,0 +1,34 @@ +// Tideland Go REST Server Library - JSON Web Token - Unit Tests +// +// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package jwt + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "time" +) + +//-------------------- +// HELPERS +//-------------------- + +// SetCleanupInterval allows to configure a shorter cleanup +// interval for tests. The returned function resets the +// current interval when called, e.g. with defer. +func SetCleanupInterval(interval time.Duration) func() { + currentInterval := cleanupInterval + reset := func() { + cleanupInterval = currentInterval + } + cleanupInterval = interval + return reset +} + +// EOF diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index f2e00f5..0eb78ee 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -352,30 +352,4 @@ func TestIsValid(t *testing.T) { assert.False(ok) } -//-------------------- -// HELPERS -//-------------------- - -// initClaims creates test claims. -func initClaims() jwt.Claims { - claims := jwt.NewClaims() - claims.SetSubject("1234567890") - claims.Set("name", "John Doe") - claims.Set("admin", true) - return claims -} - -// testClaims checks the passed claims. -func testClaims(assert audit.Assertion, claims jwt.Claims) { - sub, ok := claims.Subject() - assert.True(ok) - assert.Equal(sub, "1234567890") - name, ok := claims.GetString("name") - assert.True(ok) - assert.Equal(name, "John Doe") - admin, ok := claims.GetBool("admin") - assert.True(ok) - assert.True(admin) -} - // EOF From bd776413f9551dd18904c0d058c7155d55c6af6a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 8 Sep 2016 19:35:23 +0000 Subject: [PATCH 013/127] Finished first cache test --- jwt/cache_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jwt/cache_test.go b/jwt/cache_test.go index 6d79208..0cdbde9 100644 --- a/jwt/cache_test.go +++ b/jwt/cache_test.go @@ -31,10 +31,10 @@ func TestCachePutGet(t *testing.T) { assert.Logf("testing cache put and get") cache := jwt.NewCache(time.Minute, time.Minute) key := []byte("secret") - claims := init.Claims() + claims := initClaims() jwtIn, err := jwt.Encode(claims, key, jwt.HS512) assert.Nil(err) - cache.Put(jwt) + cache.Put(jwtIn) token := jwtIn.String() jwtOut, ok := cache.Get(token) assert.True(ok) @@ -42,8 +42,8 @@ func TestCachePutGet(t *testing.T) { jwtOut, ok = cache.Get("is.not.there") assert.False(ok) assert.Nil(jwtOut) - err := cache.Stop() + err = cache.Stop() assert.Nil(err) } -// EOF \ No newline at end of file +// EOF From 10732e7db97e6c7c72be401bfdb956583a319a05 Mon Sep 17 00:00:00 2001 From: themue Date: Fri, 9 Sep 2016 13:58:33 +0200 Subject: [PATCH 014/127] Added test for cache cleanup --- jwt/cache_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/jwt/cache_test.go b/jwt/cache_test.go index 0cdbde9..929f736 100644 --- a/jwt/cache_test.go +++ b/jwt/cache_test.go @@ -46,4 +46,27 @@ func TestCachePutGet(t *testing.T) { assert.Nil(err) } +// TestCacheCleanup tests the cleanup of the JWT cache. +func TestCacheCleanup(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing cache cleanup") + reset := jwt.SetCleanupInterval(time.Second) + defer reset() + cache := jwt.NewCache(time.Second, time.Second) + key := []byte("secret") + claims := initClaims() + jwtIn, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + cache.Put(jwtIn) + token := jwtIn.String() + jwtOut, ok := cache.Get(token) + assert.True(ok) + assert.Equal(jwtIn, jwtOut) + // Now wait a bit an try again. + time.Sleep(5 * time.Second) + jwtOut, ok = cache.Get(token) + assert.False(ok) + assert.Nil(jwtOut) +} + // EOF From 59a63ad826e8411a0e33d57063b165954a3501ae Mon Sep 17 00:00:00 2001 From: themue Date: Fri, 9 Sep 2016 14:38:29 +0200 Subject: [PATCH 015/127] Extended JWT cache test Now one test for access based cleanup and one for invalid tokens. --- README.md | 2 +- jwt/cache.go | 2 +- jwt/cache_test.go | 43 ++++++++++++++++++++++++++++++++++++++++--- jwt/doc.go | 2 +- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d7c6013..57ba081 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-08 +Version 2.0.0 beta 2016-09-09 ## Packages diff --git a/jwt/cache.go b/jwt/cache.go index 96ce8b9..9d0aeec 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -126,7 +126,7 @@ func (c *cache) cleanup() { now := time.Now() for token, entry := range c.entries { if entry.jwt.IsValid(c.leeway) { - if entry.accessed.Add(c.ttl).Before(now) { + if entry.accessed.Add(c.ttl).After(now) { // Everything fine. valids[token] = entry } diff --git a/jwt/cache_test.go b/jwt/cache_test.go index 929f736..f9ba963 100644 --- a/jwt/cache_test.go +++ b/jwt/cache_test.go @@ -46,10 +46,11 @@ func TestCachePutGet(t *testing.T) { assert.Nil(err) } -// TestCacheCleanup tests the cleanup of the JWT cache. -func TestCacheCleanup(t *testing.T) { +// TestCacheAccessCleanup tests the access based cleanup +// of the JWT cache. +func TestCacheAccessCleanup(t *testing.T) { assert := audit.NewTestingAssertion(t, true) - assert.Logf("testing cache cleanup") + assert.Logf("testing cache access based cleanup") reset := jwt.SetCleanupInterval(time.Second) defer reset() cache := jwt.NewCache(time.Second, time.Second) @@ -69,4 +70,40 @@ func TestCacheCleanup(t *testing.T) { assert.Nil(jwtOut) } +// TestCacheValidityCleanup tests the validity based cleanup +// of the JWT cache. +func TestCacheValidityCleanup(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing cache validity based cleanup") + reset := jwt.SetCleanupInterval(time.Second) + defer reset() + cache := jwt.NewCache(time.Minute, time.Second) + key := []byte("secret") + now := time.Now() + nbf := now.Add(-2 * time.Second) + exp := now.Add(2 * time.Second) + claims := initClaims() + claims.SetNotBefore(nbf) + claims.SetExpiration(exp) + jwtIn, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + cache.Put(jwtIn) + token := jwtIn.String() + jwtOut, ok := cache.Get(token) + assert.True(ok) + assert.Equal(jwtIn, jwtOut) + // Now access until it is invalid and not + // available anymore. + var i int + for i = 0; i < 5; i++ { + time.Sleep(time.Second) + jwtOut, ok = cache.Get(token) + if !ok { + break + } + assert.Equal(jwtIn, jwtOut) + } + assert.True(i > 1 && i < 4) +} + // EOF diff --git a/jwt/doc.go b/jwt/doc.go index b66af17..bc9b2c2 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-08") + return version.New(2, 0, 0, "beta", "2016-09-09") } // EOF From cef1765a5783ca99794d51fc2f44b467eec26318 Mon Sep 17 00:00:00 2001 From: themue Date: Fri, 9 Sep 2016 15:42:53 +0200 Subject: [PATCH 016/127] Started with convenience functions They will help to retrieve tokens from requests and jobs and also to add them into requests. --- jwt/header.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 jwt/header.go diff --git a/jwt/header.go b/jwt/header.go new file mode 100644 index 0000000..699d8ae --- /dev/null +++ b/jwt/header.go @@ -0,0 +1,45 @@ +// Tideland Go REST Server Library - JSON Web Token - Header +// +// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package jwt + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "net/http" + "strings" + + "github.com/tideland/gorest/rest" +) + +//-------------------- +// REQUEST HANDLING +//-------------------- + +// TokenFromJob retrieves a possible JWT from the request +// inside a REST job. The JWT is only decoded. +func TokenFromJob(job rest.Job) (JWT, error) { + return TokenFromRequest(job.Request()) +} + +// TokenFromRequest retrieves a possible JWT from a +// HTTP request. The JWT is only decoded. +func TokenFromRequest(req *http.Request) (JWT, error) { + authorization := req.Header.Get("Authorization") + if authorization == "" { + return nil, nil + } + fields := strings.Fields(authorization) + if len(fields) != 2 || fields[0] != "Bearer" { + return nil, nil + } + return Decode(fields[1]) +} + +// EOF From 2d63e8d9a624232559c8a23e4722d9be82673af0 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 9 Sep 2016 22:06:07 +0200 Subject: [PATCH 017/127] Extended JWT header convenience functions Now also can retrieve verified tokens. --- jwt/header.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/jwt/header.go b/jwt/header.go index 699d8ae..f508c2a 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -42,4 +42,25 @@ func TokenFromRequest(req *http.Request) (JWT, error) { return Decode(fields[1]) } +// VerifiedTokenFromJob retrieves a possible JWT from +// the request inside a REST job. The JWT is verified. +func VerifiedTokenFromJob(job rest.Job, key Key) (JWT, error) { + return VerifiedTokenFromRequest(job.Request(), key) +} + +// VerifiedTokenFromRequest retrieves a possible JWT from a +// HTTP request. The JWT is verified. +func VerifiedTokenFromRequest(req *http.Request, key Key) (JWT, error) { + authorization := req.Header.Get("Authorization") + if authorization == "" { + return nil, nil + } + fields := strings.Fields(authorization) + if len(fields) != 2 || fields[0] != "Bearer" { + return nil, nil + } + return Verify(fields[1], key) +} + + // EOF From a8c056cf05ead092f0aad39f28fca24554c53b88 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 9 Sep 2016 22:24:30 +0200 Subject: [PATCH 018/127] Now can add token as header to request --- jwt/header.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jwt/header.go b/jwt/header.go index f508c2a..3254643 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -62,5 +62,11 @@ func VerifiedTokenFromRequest(req *http.Request, key Key) (JWT, error) { return Verify(fields[1], key) } +// AddTokenToRequest adds a token as header to a request for +// usage by a client. +func AddTokenToRequest(req *http.Request, jwt JWT) *http.Request { + req.Header.Add("Authorization", "Bearer " + jwt.String()) + return req +} // EOF From 7f372a4468a5d6459aeea52e21945a6ac0c45dbf Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 10 Sep 2016 15:29:06 +0200 Subject: [PATCH 019/127] Added first test for the JWT header functions Right now only decoding, no verification. --- README.md | 8 ++- jwt/header.go | 20 ++++---- jwt/header_test.go | 113 +++++++++++++++++++++++++++++++++++++++++ jwt/jwt.go | 2 +- restaudit/doc.go | 2 +- restaudit/restaudit.go | 5 ++ 6 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 jwt/header_test.go diff --git a/README.md b/README.md index 57ba081..1c090be 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-09 +Version 2.0.0 beta 2016-09-10 ## Packages @@ -35,6 +35,12 @@ JWT package for secure authentication and information exchange like claims. [![GoDoc](https://godoc.org/github.com/tideland/gorest/jwt?status.svg)](https://godoc.org/github.com/tideland/gorest/jwt) +#### REST Audit + +Helpers for the unit tests of the Go REST Server Library. + +[![GoDoc](https://godoc.org/github.com/tideland/gorest/restaudit?status.svg)](https://godoc.org/github.com/tideland/gorest/restaudit) + ## Contributors - Frank Mueller (https://github.com/themue / https://github.com/tideland) diff --git a/jwt/header.go b/jwt/header.go index 3254643..0835d85 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -22,15 +22,15 @@ import ( // REQUEST HANDLING //-------------------- -// TokenFromJob retrieves a possible JWT from the request +// DecodeTokenFromJob retrieves a possible JWT from the request // inside a REST job. The JWT is only decoded. -func TokenFromJob(job rest.Job) (JWT, error) { - return TokenFromRequest(job.Request()) +func DecodeTokenFromJob(job rest.Job) (JWT, error) { + return DecodeTokenFromRequest(job.Request()) } -// TokenFromRequest retrieves a possible JWT from a +// DecodeTokenFromRequest retrieves a possible JWT from a // HTTP request. The JWT is only decoded. -func TokenFromRequest(req *http.Request) (JWT, error) { +func DecodeTokenFromRequest(req *http.Request) (JWT, error) { authorization := req.Header.Get("Authorization") if authorization == "" { return nil, nil @@ -42,15 +42,15 @@ func TokenFromRequest(req *http.Request) (JWT, error) { return Decode(fields[1]) } -// VerifiedTokenFromJob retrieves a possible JWT from +// VerifyTokenFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. -func VerifiedTokenFromJob(job rest.Job, key Key) (JWT, error) { - return VerifiedTokenFromRequest(job.Request(), key) +func VerifyTokenFromJob(job rest.Job, key Key) (JWT, error) { + return VerifyTokenFromJob(job.Request(), key) } -// VerifiedTokenFromRequest retrieves a possible JWT from a +// VerifyTokenFromRequest retrieves a possible JWT from a // HTTP request. The JWT is verified. -func VerifiedTokenFromRequest(req *http.Request, key Key) (JWT, error) { +func VerifyTokenFromRequest(req *http.Request, key Key) (JWT, error) { authorization := req.Header.Get("Authorization") if authorization == "" { return nil, nil diff --git a/jwt/header_test.go b/jwt/header_test.go new file mode 100644 index 0000000..f27c2ef --- /dev/null +++ b/jwt/header_test.go @@ -0,0 +1,113 @@ +// Tideland Go REST Server Library - JSON Web Token - Unit Tests +// +// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package jwt_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/tideland/golib/audit" + + "github.com/tideland/gorest/jwt" + "github.com/tideland/gorest/rest" + "github.com/tideland/gorest/restaudit" +) + +//-------------------- +// TESTS +//-------------------- + +// TestDecodeRequest tests the decoding of a token +// in a handler. +func TestDecodeRequest(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing decoding a token") + key := []byte("secret") + claimsIn := initClaims() + jwt, err := jwt.Encode(claimsIn, key, jwt.HS512) + assert.Nil(err) + // Setup the test server. + mux := rest.NewMultiplexer() + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err := mux.Register("test", "jwt", NewTestHandler("jwt", assert)) + assert.Nil(err) + // Perform test request. + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/test/jwt/1234567890", + Header: restaudit.KeyValues{"Accept": "application/json"}, + ProcessRequest: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwt) + }, + }) + var claimsOut jwt.Claims + err = json.Unmarshal(resp.Body, &claimsOut) + assert.Nil(err) + assert.Equal(claimsOut, claimsIn) +} + +//-------------------- +// HANDLER +//-------------------- + +// testHandler is used in the test scenarios. +type testHandler struct { + id string + assert audit.Assertion + key jwt.Key +} + +func NewTestHandler(id string, assert audit.Assertion, key jwt.Key) rest.ResourceHandler { + return &TestHandler{id, assert, key} +} + +func (th *TestHandler) ID() string { + return th.id +} + +func (th *TestHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +func (th *TestHandler) Get(job rest.Job) (bool, error) { + if th.Key == nil { + return th.testDecode(job) + } else { + return th.testVerify(job) + } +} + +func (th *TestHandler) testDecode(job rest.Job) (bool, error) { + jwt, err := jwt.DecodeTokenFromJob(job) + th.assert.Nil(err) + th.assert.True(jwt.IsValid(time.Minute)) + subject, ok := jwt.Subject() + th.assert.True(ok) + th.assert.Equal(subject, job.ResourceID()) + job.JSON(true).Write(jwt.Claims()) + return true, nil +} + +func (th *TestHandler) testVerify(job rest.Job) (bool, error) { + jwt, err := jwt.VerifyTokenFromJob(job, th.key) + th.assert.Nil(err) + th.assert.True(jwt.IsValid(time.Minute)) + subject, ok := jwt.Subject() + th.assert.True(ok) + th.assert.Equal(subject, job.ResourceID()) + job.JSON(true).Write(jwt.Claims()) + return true, nil +} + +// EOF \ No newline at end of file diff --git a/jwt/jwt.go b/jwt/jwt.go index 4549a9d..a161a78 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -100,7 +100,7 @@ func Decode(token string) (JWT, error) { var claims Claims err = decodeAndUnmarshall(parts[1], &claims) if err != nil { - return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "claims") + return nil, errors.Annotat(err, ErrCannotDecode, errorMessages, "claims") } return &jwt{ claims: claims, diff --git a/restaudit/doc.go b/restaudit/doc.go index 1e11779..8d4dee5 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-08-23") + return version.New(2, 0, 0, "beta", "2016-09-10") } // EOF diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 44312b7..ec5e55f 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -37,6 +37,7 @@ type Request struct { Header KeyValues Cookies KeyValues Body []byte + RequestProcessor func(req *http.Request) *http.Request } // Response wraps all infos of a test response. @@ -103,6 +104,10 @@ func (ts *testServer) DoRequest(req *Request) *Response { } httpReq.AddCookie(cookie) } + // Check if request shall be processed before performed. + if req.RequestProcessor != nil { + httpReq = req.RequestProcessor(httpReq) + } // Now do it. resp, err := c.Do(httpReq) ts.assert.Nil(err, "cannot perform test request") From a8a159a993d39539e8de0565570276eb8c1c519a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 10 Sep 2016 14:40:01 +0000 Subject: [PATCH 020/127] Test for header function is now working --- jwt/doc.go | 2 +- jwt/header.go | 6 +++--- jwt/header_test.go | 43 +++++++++++++++++++++--------------------- jwt/jwt.go | 2 +- restaudit/restaudit.go | 10 +++++----- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/jwt/doc.go b/jwt/doc.go index bc9b2c2..820993d 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-09") + return version.New(2, 0, 0, "beta", "2016-09-10") } // EOF diff --git a/jwt/header.go b/jwt/header.go index 0835d85..34b2cd6 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -42,10 +42,10 @@ func DecodeTokenFromRequest(req *http.Request) (JWT, error) { return Decode(fields[1]) } -// VerifyTokenFromJob retrieves a possible JWT from +// VerifyTokenFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. func VerifyTokenFromJob(job rest.Job, key Key) (JWT, error) { - return VerifyTokenFromJob(job.Request(), key) + return VerifyTokenFromRequest(job.Request(), key) } // VerifyTokenFromRequest retrieves a possible JWT from a @@ -65,7 +65,7 @@ func VerifyTokenFromRequest(req *http.Request, key Key) (JWT, error) { // AddTokenToRequest adds a token as header to a request for // usage by a client. func AddTokenToRequest(req *http.Request, jwt JWT) *http.Request { - req.Header.Add("Authorization", "Bearer " + jwt.String()) + req.Header.Add("Authorization", "Bearer "+jwt.String()) return req } diff --git a/jwt/header_test.go b/jwt/header_test.go index f27c2ef..97ac856 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -15,6 +15,7 @@ import ( "encoding/json" "net/http" "testing" + "time" "github.com/tideland/golib/audit" @@ -34,21 +35,21 @@ func TestDecodeRequest(t *testing.T) { assert.Logf("testing decoding a token") key := []byte("secret") claimsIn := initClaims() - jwt, err := jwt.Encode(claimsIn, key, jwt.HS512) + jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) assert.Nil(err) // Setup the test server. mux := rest.NewMultiplexer() ts := restaudit.StartServer(mux, assert) defer ts.Close() - err := mux.Register("test", "jwt", NewTestHandler("jwt", assert)) + err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil)) assert.Nil(err) // Perform test request. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", Path: "/test/jwt/1234567890", Header: restaudit.KeyValues{"Accept": "application/json"}, - ProcessRequest: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwt) + RequestProcessor: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) }, }) var claimsOut jwt.Claims @@ -65,49 +66,49 @@ func TestDecodeRequest(t *testing.T) { type testHandler struct { id string assert audit.Assertion - key jwt.Key + key jwt.Key } func NewTestHandler(id string, assert audit.Assertion, key jwt.Key) rest.ResourceHandler { - return &TestHandler{id, assert, key} + return &testHandler{id, assert, key} } -func (th *TestHandler) ID() string { +func (th *testHandler) ID() string { return th.id } -func (th *TestHandler) Init(env rest.Environment, domain, resource string) error { +func (th *testHandler) Init(env rest.Environment, domain, resource string) error { return nil } -func (th *TestHandler) Get(job rest.Job) (bool, error) { - if th.Key == nil { +func (th *testHandler) Get(job rest.Job) (bool, error) { + if th.key == nil { return th.testDecode(job) } else { return th.testVerify(job) } } -func (th *TestHandler) testDecode(job rest.Job) (bool, error) { - jwt, err := jwt.DecodeTokenFromJob(job) +func (th *testHandler) testDecode(job rest.Job) (bool, error) { + jwtOut, err := jwt.DecodeTokenFromJob(job) th.assert.Nil(err) - th.assert.True(jwt.IsValid(time.Minute)) - subject, ok := jwt.Subject() + th.assert.True(jwtOut.IsValid(time.Minute)) + subject, ok := jwtOut.Claims().Subject() th.assert.True(ok) th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(jwt.Claims()) + job.JSON(true).Write(jwtOut.Claims()) return true, nil } -func (th *TestHandler) testVerify(job rest.Job) (bool, error) { - jwt, err := jwt.VerifyTokenFromJob(job, th.key) +func (th *testHandler) testVerify(job rest.Job) (bool, error) { + jwtOut, err := jwt.VerifyTokenFromJob(job, th.key) th.assert.Nil(err) - th.assert.True(jwt.IsValid(time.Minute)) - subject, ok := jwt.Subject() + th.assert.True(jwtOut.IsValid(time.Minute)) + subject, ok := jwtOut.Claims().Subject() th.assert.True(ok) th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(jwt.Claims()) + job.JSON(true).Write(jwtOut.Claims()) return true, nil } -// EOF \ No newline at end of file +// EOF diff --git a/jwt/jwt.go b/jwt/jwt.go index a161a78..4549a9d 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -100,7 +100,7 @@ func Decode(token string) (JWT, error) { var claims Claims err = decodeAndUnmarshall(parts[1], &claims) if err != nil { - return nil, errors.Annotat(err, ErrCannotDecode, errorMessages, "claims") + return nil, errors.Annotate(err, ErrCannotDecode, errorMessages, "claims") } return &jwt{ claims: claims, diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index ec5e55f..3ac1191 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -32,11 +32,11 @@ type KeyValues map[string]string // Request wraps all infos for a test request. type Request struct { - Method string - Path string - Header KeyValues - Cookies KeyValues - Body []byte + Method string + Path string + Header KeyValues + Cookies KeyValues + Body []byte RequestProcessor func(req *http.Request) *http.Request } From dd98cc3925e55ca22c1f6d18e791da96464bb9ef Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 10 Sep 2016 14:45:02 +0000 Subject: [PATCH 021/127] Add verified header token access --- jwt/header_test.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/jwt/header_test.go b/jwt/header_test.go index 97ac856..b988948 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -32,7 +32,7 @@ import ( // in a handler. func TestDecodeRequest(t *testing.T) { assert := audit.NewTestingAssertion(t, true) - assert.Logf("testing decoding a token") + assert.Logf("testing decode a request token") key := []byte("secret") claimsIn := initClaims() jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) @@ -58,6 +58,36 @@ func TestDecodeRequest(t *testing.T) { assert.Equal(claimsOut, claimsIn) } +// TestVerifyRequest tests the verification of a token +// in a handler. +func TestVerifyRequest(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing verify a request token") + key := []byte("secret") + claimsIn := initClaims() + jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) + assert.Nil(err) + // Setup the test server. + mux := rest.NewMultiplexer() + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key)) + assert.Nil(err) + // Perform test request. + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/test/jwt/1234567890", + Header: restaudit.KeyValues{"Accept": "application/json"}, + RequestProcessor: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) + }, + }) + var claimsOut jwt.Claims + err = json.Unmarshal(resp.Body, &claimsOut) + assert.Nil(err) + assert.Equal(claimsOut, claimsIn) +} + //-------------------- // HANDLER //-------------------- From 26ed5781f47598156bc8854864b25afd39cbb265 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 11 Sep 2016 16:56:51 +0200 Subject: [PATCH 022/127] Started implementing better cache control Now also controling the maximum size. Also the cleanup interval can now be directly controlled by the user. --- jwt/cache.go | 43 ++++++++++++++++++++++++++++++------------- jwt/cache_test.go | 10 +++------- jwt/export_test.go | 34 ---------------------------------- 3 files changed, 33 insertions(+), 54 deletions(-) delete mode 100644 jwt/export_test.go diff --git a/jwt/cache.go b/jwt/cache.go index 9d0aeec..1abab20 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -22,10 +22,6 @@ import ( // CACHE //-------------------- -// cleanupInterval defines the timespan between -// two cleanup runs. -var cleanupInterval = 5 * time.Minute - // Cache provides a caching for tokens so that these // don't have to be decoded or verified multiple times. type Cache interface { @@ -35,6 +31,10 @@ type Cache interface { // Put adds a token to the cache. Put(jwt JWT) + // Cleanup manually tells the cache to cleanup. + // Setting force to true empties it totally. + Cleanup(force bool) + // Stop tells the cache to end working. Stop() error } @@ -48,9 +48,12 @@ type cacheEntry struct { // cache implements Cache. type cache struct { mutex sync.Mutex + entries map[string]*cacheEntry ttl time.Duration leeway time.Duration - entries map[string]*cacheEntry + interval time.Duration + maxSize int + cleanupc chan bool loop loop.Loop } @@ -58,11 +61,14 @@ type cache struct { // durations. The first one is the time a token hasn't // been used anymore before it is cleaned up. The second // one is the leeway taken for token time validations. -func NewCache(ttl, leeway time.Duration) Cache { +func NewCache(ttl, leeway, interval time.Duration, maxSize int) Cache { c := &cache{ + entries: map[string]*cacheEntry{}, ttl: ttl, leeway: leeway, - entries: map[string]*cacheEntry{}, + interval: interval, + maxSize: maxSize, + cleanupc: make(chan bool, 1), } c.loop = loop.Go(c.backendLoop, "jwt", "cache") return c @@ -94,6 +100,11 @@ func (c *cache) Put(jwt JWT) { } } +// Cleanup implements the Cache interface. +func (c *cache) Cleanup(force bool) { + c.cleanupc <- force +} + // Stop implements the Cache interface. func (c *cache) Stop() error { return c.loop.Stop() @@ -102,27 +113,33 @@ func (c *cache) Stop() error { // backendLoop runs a cleaning session every five minutes. func (c *cache) backendLoop(l loop.Loop) error { defer func() { - // Some cleanup after stop or error. - c.ttl = 0 - c.leeway = 0 + // Cleanup entries map after stop or error. c.entries = nil }() - ticker := time.NewTicker(cleanupInterval) + ticker := time.NewTicker(c.interval) for { select { case <-l.ShallStop(): return nil + case force := <-c.cleanupc: + c.cleanup(force) case <-ticker.C: - c.cleanup() + c.cleanup(false) } } } // cleanup checks for invalid or unused tokens. -func (c *cache) cleanup() { +func (c *cache) cleanup(force bool) { c.mutex.Lock() defer c.mutex.Unlock() valids := map[string]*cacheEntry{} + if force { + // Forced cleanup removes all entries. + c.entries = valids + return + } + // Check for valid and accessed entries. now := time.Now() for token, entry := range c.entries { if entry.jwt.IsValid(c.leeway) { diff --git a/jwt/cache_test.go b/jwt/cache_test.go index f9ba963..e44d817 100644 --- a/jwt/cache_test.go +++ b/jwt/cache_test.go @@ -29,7 +29,7 @@ import ( func TestCachePutGet(t *testing.T) { assert := audit.NewTestingAssertion(t, true) assert.Logf("testing cache put and get") - cache := jwt.NewCache(time.Minute, time.Minute) + cache := jwt.NewCache(time.Minute, time.Minute, time.Minute, 10) key := []byte("secret") claims := initClaims() jwtIn, err := jwt.Encode(claims, key, jwt.HS512) @@ -51,9 +51,7 @@ func TestCachePutGet(t *testing.T) { func TestCacheAccessCleanup(t *testing.T) { assert := audit.NewTestingAssertion(t, true) assert.Logf("testing cache access based cleanup") - reset := jwt.SetCleanupInterval(time.Second) - defer reset() - cache := jwt.NewCache(time.Second, time.Second) + cache := jwt.NewCache(time.Second, time.Second, time.Second, 10) key := []byte("secret") claims := initClaims() jwtIn, err := jwt.Encode(claims, key, jwt.HS512) @@ -75,9 +73,7 @@ func TestCacheAccessCleanup(t *testing.T) { func TestCacheValidityCleanup(t *testing.T) { assert := audit.NewTestingAssertion(t, true) assert.Logf("testing cache validity based cleanup") - reset := jwt.SetCleanupInterval(time.Second) - defer reset() - cache := jwt.NewCache(time.Minute, time.Second) + cache := jwt.NewCache(time.Minute, time.Second, time.Second, 10) key := []byte("secret") now := time.Now() nbf := now.Add(-2 * time.Second) diff --git a/jwt/export_test.go b/jwt/export_test.go deleted file mode 100644 index e015a3a..0000000 --- a/jwt/export_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Tideland Go REST Server Library - JSON Web Token - Unit Tests -// -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany -// -// All rights reserved. Use of this source code is governed -// by the new BSD license. - -package jwt - -//-------------------- -// IMPORTS -//-------------------- - -import ( - "time" -) - -//-------------------- -// HELPERS -//-------------------- - -// SetCleanupInterval allows to configure a shorter cleanup -// interval for tests. The returned function resets the -// current interval when called, e.g. with defer. -func SetCleanupInterval(interval time.Duration) func() { - currentInterval := cleanupInterval - reset := func() { - cleanupInterval = currentInterval - } - cleanupInterval = interval - return reset -} - -// EOF From aa94aa211941928ce6c29200400de9fd7ef3e583 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 11 Sep 2016 18:46:23 +0000 Subject: [PATCH 023/127] JWT cache now can dynalically calculate ttl on high load --- README.md | 2 +- jwt/cache.go | 69 ++++++++++++++++++++++++++-------------------------- jwt/doc.go | 2 +- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 1c090be..e718e4a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-10 +Version 2.0.0 beta 2016-09-11 ## Packages diff --git a/jwt/cache.go b/jwt/cache.go index 1abab20..643ac7f 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -32,8 +32,7 @@ type Cache interface { Put(jwt JWT) // Cleanup manually tells the cache to cleanup. - // Setting force to true empties it totally. - Cleanup(force bool) + Cleanup() // Stop tells the cache to end working. Stop() error @@ -47,28 +46,31 @@ type cacheEntry struct { // cache implements Cache. type cache struct { - mutex sync.Mutex - entries map[string]*cacheEntry - ttl time.Duration - leeway time.Duration - interval time.Duration - maxSize int - cleanupc chan bool - loop loop.Loop + mutex sync.Mutex + entries map[string]*cacheEntry + ttl time.Duration + leeway time.Duration + interval time.Duration + maxEntries int + cleanupc chan time.Duration + loop loop.Loop } -// NewCache creates a new JWT caching. It takes two -// durations. The first one is the time a token hasn't -// been used anymore before it is cleaned up. The second -// one is the leeway taken for token time validations. -func NewCache(ttl, leeway, interval time.Duration, maxSize int) Cache { +// NewCache creates a new JWT caching. The ttl value controls +// the time a cached token may be unused before cleanup. The +// leeway is used for the time validation of the token itself. +// The duration of the interval controls how often the background +// cleanup is running. Final configuration parameter is the maximum +// number of entries inside the cache. If these grow too fast the +// ttl will be temporarilly reduced for cleanup. +func NewCache(ttl, leeway, interval time.Duration, maxEntries int) Cache { c := &cache{ - entries: map[string]*cacheEntry{}, - ttl: ttl, - leeway: leeway, - interval: interval, - maxSize: maxSize, - cleanupc: make(chan bool, 1), + entries: map[string]*cacheEntry{}, + ttl: ttl, + leeway: leeway, + interval: interval, + maxEntries: maxEntries, + cleanupc: make(chan time.Duration, 5), } c.loop = loop.Go(c.backendLoop, "jwt", "cache") return c @@ -97,12 +99,17 @@ func (c *cache) Put(jwt JWT) { defer c.mutex.Unlock() if jwt.IsValid(c.leeway) { c.entries[jwt.String()] = &cacheEntry{jwt, time.Now()} + lenEntries := len(c.entries) + if lenEntries > c.maxEntries { + ttl := int64(c.ttl) / int64(lenEntries) * int64(c.maxEntries) + c.cleanupc <- time.Duration(ttl) + } } } // Cleanup implements the Cache interface. -func (c *cache) Cleanup(force bool) { - c.cleanupc <- force +func (c *cache) Cleanup() { + c.cleanupc <- c.ttl } // Stop implements the Cache interface. @@ -121,29 +128,23 @@ func (c *cache) backendLoop(l loop.Loop) error { select { case <-l.ShallStop(): return nil - case force := <-c.cleanupc: - c.cleanup(force) + case ttl := <-c.cleanupc: + c.cleanup(ttl) case <-ticker.C: - c.cleanup(false) + c.cleanup(c.ttl) } } } // cleanup checks for invalid or unused tokens. -func (c *cache) cleanup(force bool) { +func (c *cache) cleanup(ttl time.Duration) { c.mutex.Lock() defer c.mutex.Unlock() valids := map[string]*cacheEntry{} - if force { - // Forced cleanup removes all entries. - c.entries = valids - return - } - // Check for valid and accessed entries. now := time.Now() for token, entry := range c.entries { if entry.jwt.IsValid(c.leeway) { - if entry.accessed.Add(c.ttl).After(now) { + if entry.accessed.Add(ttl).After(now) { // Everything fine. valids[token] = entry } diff --git a/jwt/doc.go b/jwt/doc.go index 820993d..c5f109c 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-10") + return version.New(2, 0, 0, "beta", "2016-09-11") } // EOF From 75371be112239624bc3509a95bd420eeb20f52e5 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 11 Sep 2016 20:04:04 +0000 Subject: [PATCH 024/127] Tested JWT cache load handling --- jwt/cache.go | 5 +++-- jwt/cache_test.go | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/jwt/cache.go b/jwt/cache.go index 643ac7f..2ec224e 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -29,7 +29,7 @@ type Cache interface { Get(token string) (JWT, bool) // Put adds a token to the cache. - Put(jwt JWT) + Put(jwt JWT) int // Cleanup manually tells the cache to cleanup. Cleanup() @@ -94,7 +94,7 @@ func (c *cache) Get(token string) (JWT, bool) { } // Put implements the Cache interface. -func (c *cache) Put(jwt JWT) { +func (c *cache) Put(jwt JWT) int { c.mutex.Lock() defer c.mutex.Unlock() if jwt.IsValid(c.leeway) { @@ -105,6 +105,7 @@ func (c *cache) Put(jwt JWT) { c.cleanupc <- time.Duration(ttl) } } + return len(c.entries) } // Cleanup implements the Cache interface. diff --git a/jwt/cache_test.go b/jwt/cache_test.go index e44d817..c3b5435 100644 --- a/jwt/cache_test.go +++ b/jwt/cache_test.go @@ -12,6 +12,7 @@ package jwt_test //-------------------- import ( + "fmt" "testing" "time" @@ -102,4 +103,24 @@ func TestCacheValidityCleanup(t *testing.T) { assert.True(i > 1 && i < 4) } +// TestCacheLoad tests the cache load based cleanup. +func TestCacheLoad(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing cache load based cleanup") + cacheTime := 100 * time.Millisecond + cache := jwt.NewCache(2*cacheTime, cacheTime, cacheTime, 4) + claims := initClaims() + // Now fill the cache and check that it doesn't + // grow too high. + var i int + for i = 0; i < 10; i++ { + time.Sleep(50 * time.Millisecond) + key := []byte(fmt.Sprintf("secret-%d", i)) + jwtIn, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + size := cache.Put(jwtIn) + assert.True(size < 6) + } +} + // EOF From 5d8995dcc926c031f37ddfbc2dc96fa8f8f0da00 Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 12 Sep 2016 10:15:12 +0200 Subject: [PATCH 025/127] Renamed header based JWT decode and verify functions --- README.md | 2 +- jwt/doc.go | 2 +- jwt/header.go | 20 ++++++++++---------- jwt/header_test.go | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e718e4a..d52b9a2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-11 +Version 2.0.0 beta 2016-09-12 ## Packages diff --git a/jwt/doc.go b/jwt/doc.go index c5f109c..95e1ced 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-11") + return version.New(2, 0, 0, "beta", "2016-09-12") } // EOF diff --git a/jwt/header.go b/jwt/header.go index 34b2cd6..05e9ee2 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -22,15 +22,15 @@ import ( // REQUEST HANDLING //-------------------- -// DecodeTokenFromJob retrieves a possible JWT from the request +// DecodeFromJob retrieves a possible JWT from the request // inside a REST job. The JWT is only decoded. -func DecodeTokenFromJob(job rest.Job) (JWT, error) { - return DecodeTokenFromRequest(job.Request()) +func DecodeFromJob(job rest.Job) (JWT, error) { + return DecodeFromRequest(job.Request()) } -// DecodeTokenFromRequest retrieves a possible JWT from a +// DecodeFromRequest retrieves a possible JWT from a // HTTP request. The JWT is only decoded. -func DecodeTokenFromRequest(req *http.Request) (JWT, error) { +func DecodeFromRequest(req *http.Request) (JWT, error) { authorization := req.Header.Get("Authorization") if authorization == "" { return nil, nil @@ -42,15 +42,15 @@ func DecodeTokenFromRequest(req *http.Request) (JWT, error) { return Decode(fields[1]) } -// VerifyTokenFromJob retrieves a possible JWT from +// VerifyFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. -func VerifyTokenFromJob(job rest.Job, key Key) (JWT, error) { - return VerifyTokenFromRequest(job.Request(), key) +func VerifyFromJob(job rest.Job, key Key) (JWT, error) { + return VerifyFromRequest(job.Request(), key) } -// VerifyTokenFromRequest retrieves a possible JWT from a +// VerifyFromRequest retrieves a possible JWT from a // HTTP request. The JWT is verified. -func VerifyTokenFromRequest(req *http.Request, key Key) (JWT, error) { +func VerifyFromRequest(req *http.Request, key Key) (JWT, error) { authorization := req.Header.Get("Authorization") if authorization == "" { return nil, nil diff --git a/jwt/header_test.go b/jwt/header_test.go index b988948..e14b6ee 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -120,7 +120,7 @@ func (th *testHandler) Get(job rest.Job) (bool, error) { } func (th *testHandler) testDecode(job rest.Job) (bool, error) { - jwtOut, err := jwt.DecodeTokenFromJob(job) + jwtOut, err := jwt.DecodeFromJob(job) th.assert.Nil(err) th.assert.True(jwtOut.IsValid(time.Minute)) subject, ok := jwtOut.Claims().Subject() @@ -131,7 +131,7 @@ func (th *testHandler) testDecode(job rest.Job) (bool, error) { } func (th *testHandler) testVerify(job rest.Job) (bool, error) { - jwtOut, err := jwt.VerifyTokenFromJob(job, th.key) + jwtOut, err := jwt.VerifyFromJob(job, th.key) th.assert.Nil(err) th.assert.True(jwtOut.IsValid(time.Minute)) subject, ok := jwtOut.Claims().Subject() From d2c54e4cb2cb0f86a3620f9a6ead68629fc193d8 Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 12 Sep 2016 13:40:17 +0200 Subject: [PATCH 026/127] Added GetMarshalled() to claims It allows to unmarshal nested claim value structures into passed Go structures. --- jwt/claims.go | 20 ++++++++++++++++++++ jwt/claims_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/jwt/claims.go b/jwt/claims.go index a9f60af..fb2de23 100644 --- a/jwt/claims.go +++ b/jwt/claims.go @@ -148,6 +148,26 @@ func (c Claims) GetTime(key string) (time.Time, bool) { } } +// GetMarshalled unmarshalls the JSON value of the key and stores +// it in the value pointed to by v. +func (c Claims) GetMarshalled(key string, v interface{}) (bool, error) { + value, ok := c.Get(key) + if !ok { + return false, nil + } + // Need to go the way via JSON again due to the generic + // map of strings to interfaces. + marshalled, err := json.Marshal(value) + if err != nil { + return false, err + } + err = json.Unmarshal(marshalled, v) + if err != nil { + return false, err + } + return true, nil +} + // Set sets a value in the claims. It returns a potential // old value. func (c Claims) Set(key string, value interface{}) interface{} { diff --git a/jwt/claims_test.go b/jwt/claims_test.go index 14ef56d..d3a2a6e 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -218,6 +218,45 @@ func TestClaimsTime(t *testing.T) { assert.False(ok) } +// nestedValue is used as a structured value of a claim. +type nestedValue struct { + Name string + Value int +} + +// TestClaimsMarshalledValue tests the marshalling and +// unmarshalling of structures as values. +func TestClaimsMarshalledValue(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing claims deep value unmarshalling") + baz := []*nestedValue{ + {"one", 1}, + {"two", 2}, + {"three", 3}, + } + claims := jwt.NewClaims() + claims.Set("foo", "bar") + claims.Set("baz", baz) + // Now marshal and unmarshal the claims. + jsonValue, err := json.Marshal(claims) + assert.NotNil(jsonValue) + assert.Nil(err) + var unmarshalled jwt.Claims + err = json.Unmarshal(jsonValue, &unmarshalled) + assert.Nil(err) + assert.Length(unmarshalled, 2) + foo, ok := claims.Get("foo") + assert.Equal(foo, "bar") + assert.True(ok) + var unmarshalledBaz []*nestedValue + ok, err = claims.GetMarshalled("baz", &unmarshalledBaz) + assert.True(ok) + assert.Nil(err) + assert.Length(unmarshalledBaz, 3) + assert.Equal(unmarshalledBaz[0].Name, "one") + assert.Equal(unmarshalledBaz[2].Value, 3) +} + // TestClaimsAudience checks the setting, getting, and // deleting of the audience claim. func TestClaimsAudience(t *testing.T) { From e7f7e99549aa3b5eeb918dcaa2c11d60eed0d05d Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 12 Sep 2016 16:18:18 +0200 Subject: [PATCH 027/127] Added some documentation about handler deployment. --- rest/doc.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/rest/doc.go b/rest/doc.go index fc1e5fa..65cda77 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -20,7 +20,7 @@ // boolean is more interesting. Registering a handler is based on a // domain and a resource. The URL // -// // +// // // // leads to a handler, or even better, to a list of handlers. All // are used as long as the returned boolean value is true. E.g. the @@ -28,9 +28,22 @@ // authorization, and the third one does the business. Additionally // the URL // -// /// +// /// // // provides the resource identifier via Job.ResourceID(). +// +// The handlers then are deployed to the Multiplexer which implements +// the Handler interface of the net/http package. So the typical order +// is +// +// mux := rest.NewMultiplexer() +// mux.Register("domain", "resource-type-a", NewTypeAHandler("foo")) +// mux.Register("domain", "resource-type-b", NewTypeBHandler("bar")) +// mux.Register("admin", "user", NewUserManagementHandler()) +// http.ListenAndServe(":8000", mux) +// +// Additionally further handlers can be registered or running once +// removed during runtime. package rest //-------------------- @@ -47,7 +60,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-02") + return version.New(2, 0, 0, "beta", "2016-09-12") } // EOF From 5bba51e84381cdc40e82222f5678f97a7a78aa0c Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 12 Sep 2016 19:54:56 +0000 Subject: [PATCH 028/127] Added more documentation --- rest/doc.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/rest/doc.go b/rest/doc.go index 65cda77..369d364 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -15,6 +15,37 @@ // according methods get a Job as argument. It provides convenient // helpers for the processing of the job. // +// type myHandler struct { +// id string +// } +// +// func NewMyHandler(id string) rest.ResourceHandler { +// return &myHandler{id} +// } +// +// func (h *myHandler) ID() string { +// return h.id +// } +// +// func (h *myHandler) Init(env rest.Environment, domain, resource string) error { +// // Nothing to do in this example. +// return nil +// } +// +// // Get handles reading of resources, here simplified w/o +// // error handling. +// func (h *myHandler) Get(job rest.Job) (bool, error) { +// id := job.ResourceID() +// if id == "" { +// all := model.GetAllData() +// job.JSON(true).Write(all) +// return true, nil +// } +// one := model.GetOneData(id) +// job.JSON(true).Write(one) +// return true, nil +// } +// // The processing methods return two values: a boolean and an error. // The latter is pretty clear, it signals a job processing error. The // boolean is more interesting. Registering a handler is based on a From 1c78de423e6ca98b7a21742a0085d2806bc9a781 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 13 Sep 2016 17:38:38 +0200 Subject: [PATCH 029/127] Started adding a JWT authorization handler --- README.md | 2 +- handlers/doc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d52b9a2..c0f79ea 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-12 +Version 2.0.0 beta 2016-09-13 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 4549642..a428066 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-08-23") + return version.New(2, 0, 0, "beta", "2016-09-13") } // EOF From 5886c212db1a4b875328c3229d7ed773dd2ad66b Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 14 Sep 2016 11:46:34 +0200 Subject: [PATCH 030/127] Added first version of JWT auth handler Also changed JWT job handling to integrate caching. Tests are yet missing. --- README.md | 2 +- handlers/jwtauth.go | 115 ++++++++++++++++++++++++++++++++++++++++++++ jwt/doc.go | 2 +- jwt/header.go | 85 +++++++++++++++++++++----------- 4 files changed, 173 insertions(+), 31 deletions(-) create mode 100644 handlers/jwtauth.go diff --git a/README.md b/README.md index c0f79ea..b9d7652 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-13 +Version 2.0.0 beta 2016-09-14 ## Packages diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go new file mode 100644 index 0000000..9260bb2 --- /dev/null +++ b/handlers/jwtauth.go @@ -0,0 +1,115 @@ +// Tideland Go REST Server Library - Handlers - JWT Authorization +// +// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package handlers + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "time" + + "github.com/tideland/gorest/jwt" + "github.com/tideland/gorest/rest" +) + +//-------------------- +// JWT AUTHORIZATION HANDLER +//-------------------- + +// GatekeeperFunc has to be defined by the handler user to check if the +// claims contain the required authorization information. In case it doesn't +// the function has to return false. +type GatekeeperFunc func(job rest.Job, claims jwt.Claims) (bool, error) + +// jwtAuthorizationHandler checks for a valid token and then runs +// a gatekeeper function. +type jwtAuthorizationHandler struct { + id string + key jwt.Key + gatekeeper GatekeeperFunc +} + +// NewJWTAuthorizationHandler creates a handler checking for a valid JSON +// Web Token in each request. In case the request has one the configured +// gatekeeper function will be called with job and claims for further +// validation. +func NewjwtAuthorizationHandler(id string, key jwt.Key, gf GatekeeperFunc) rest.ResourceHandler { + return &jwtAuthorizationHandler{id, key, gf} +} + +// ID is specified on the ResourceHandler interface. +func (h *jwtAuthorizationHandler) ID() string { + return h.id +} + +// Init is specified on the ResourceHandler interface. +func (h *jwtAuthorizationHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +// Get is specified on the GetResourceHandler interface. +func (h *jwtAuthorizationHandler) Get(job rest.Job) (bool, error) { + return h.check(job) +} + +// Head is specified on the HeadResourceHandler interface. +func (h *jwtAuthorizationHandler) Head(job rest.Job) (bool, error) { + return h.check(job) +} + +// Put is specified on the PutResourceHandler interface. +func (h *jwtAuthorizationHandler) Put(job rest.Job) (bool, error) { + return h.check(job) +} + +// Post is specified on the PostResourceHandler interface. +func (h *jwtAuthorizationHandler) Post(job rest.Job) (bool, error) { + return h.check(job) +} + +// Patch is specified on the PatchResourceHandler interface. +func (h *jwtAuthorizationHandler) Patch(job rest.Job) (bool, error) { + return h.check(job) +} + +// Delete is specified on the DeleteResourceHandler interface. +func (h *jwtAuthorizationHandler) Delete(job rest.Job) (bool, error) { + return h.check(job) +} + +// Options is specified on the OptionsResourceHandler interface. +func (h *jwtAuthorizationHandler) Options(job rest.Job) (bool, error) { + return h.check(job) +} + +// check is used by all methods to check the token. +func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { + token, err := jwt.VerifyFromJob(job, h.key) + if err != nil { + return false, h.deny(job, err.Error()) + } + if !token.IsValid(time.Minute) { + // TODO Configurable leeway. + return false, h.deny(job, "invalid JSON web token") + } + return h.gatekeeper(job, token.Claims()) +} + +// deny sends a negative feedback to the caller. +func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { + var f rest.Formatter + if job.AcceptsContentType(rest.ContentTypeJSON) { + f = job.JSON(true) + } else { + f = job.XML() + } + return rest.NegativeFeedback(f, msg) +} + +// EOF diff --git a/jwt/doc.go b/jwt/doc.go index 95e1ced..af9ea3d 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-12") + return version.New(2, 0, 0, "beta", "2016-09-14") } // EOF diff --git a/jwt/header.go b/jwt/header.go index 05e9ee2..ed671ca 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -19,47 +19,33 @@ import ( ) //-------------------- -// REQUEST HANDLING +// JOB AND REQUEST HANDLING //-------------------- -// DecodeFromJob retrieves a possible JWT from the request -// inside a REST job. The JWT is only decoded. +// DecodeFromJob retrieves a possible JWT from +// the request inside a REST job. The JWT is only decoded. func DecodeFromJob(job rest.Job) (JWT, error) { - return DecodeFromRequest(job.Request()) + return retrieveFromJob(job, nil, nil) } -// DecodeFromRequest retrieves a possible JWT from a -// HTTP request. The JWT is only decoded. -func DecodeFromRequest(req *http.Request) (JWT, error) { - authorization := req.Header.Get("Authorization") - if authorization == "" { - return nil, nil - } - fields := strings.Fields(authorization) - if len(fields) != 2 || fields[0] != "Bearer" { - return nil, nil - } - return Decode(fields[1]) +// DecodeCachedFromJob retrieves a possible JWT from the request +// inside a REST job and checks if it already is cached. The JWT is +// only decoded. In case of no error the token is added to the cache. +func DecodeCachedFromJob(job rest.Job, cache Cache) (JWT, error) { + return retrieveFromJob(job, cache, nil) } // VerifyFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. func VerifyFromJob(job rest.Job, key Key) (JWT, error) { - return VerifyFromRequest(job.Request(), key) + return retrieveFromJob(job, nil, key) } -// VerifyFromRequest retrieves a possible JWT from a -// HTTP request. The JWT is verified. -func VerifyFromRequest(req *http.Request, key Key) (JWT, error) { - authorization := req.Header.Get("Authorization") - if authorization == "" { - return nil, nil - } - fields := strings.Fields(authorization) - if len(fields) != 2 || fields[0] != "Bearer" { - return nil, nil - } - return Verify(fields[1], key) +// VerifyCachedFromJob retrieves a possible JWT from the request +// inside a REST job and checks if it already is cached. The JWT is +// verified. In case of no error the token is added to the cache. +func VerifyCachedFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { + return retrieveFromJob(job, cache, key) } // AddTokenToRequest adds a token as header to a request for @@ -69,4 +55,45 @@ func AddTokenToRequest(req *http.Request, jwt JWT) *http.Request { return req } +//-------------------- +// PRIVATE HELPERS +//-------------------- + +// retrieveFromJob is the generic retrieval function with possible +// caching and verifaction. +func retrieveFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { + // Retrieve token from header. + authorization := job.Request().Header.Get("Authorization") + if authorization == "" { + return nil, nil + } + fields := strings.Fields(authorization) + if len(fields) != 2 || fields[0] != "Bearer" { + return nil, nil + } + // Check cache. + if cache != nil { + jwt, ok := cache.Get(fields[1]) + if ok { + return jwt, nil + } + } + // Decode or verify. + var jwt JWT + var err error + if key == nil { + jwt, err = Decode(fields[1]) + } else { + jwt, err = Verify(fields[1], key) + } + if err != nil { + return nil, err + } + // Add to cache and return. + if cache != nil { + cache.Put(jwt) + } + return jwt, nil +} + // EOF From 4e1aea24cc56ebd828f0d492f8d047d5f2cfb517 Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 14 Sep 2016 17:46:15 +0200 Subject: [PATCH 031/127] Started testing cached JWT header access --- jwt/header_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/jwt/header_test.go b/jwt/header_test.go index e14b6ee..1592a55 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -41,7 +41,7 @@ func TestDecodeRequest(t *testing.T) { mux := rest.NewMultiplexer() ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil)) + err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) assert.Nil(err) // Perform test request. resp := ts.DoRequest(&restaudit.Request{ @@ -58,6 +58,48 @@ func TestDecodeRequest(t *testing.T) { assert.Equal(claimsOut, claimsIn) } +// TestDecodeCachedRequest tests the decoding of a token +// in a handler including usage of the cache. +func TestDecodeCachedRequest(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing decode a request token using a cache") + key := []byte("secret") + claimsIn := initClaims() + jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) + assert.Nil(err) + // Setup the test server. + mux := rest.NewMultiplexer() + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, true)) + assert.Nil(err) + // Perform first test request. + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/test/jwt/1234567890", + Header: restaudit.KeyValues{"Accept": "application/json"}, + RequestProcessor: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) + }, + }) + var claimsOut jwt.Claims + err = json.Unmarshal(resp.Body, &claimsOut) + assert.Nil(err) + assert.Equal(claimsOut, claimsIn) + // Perform second test request. + resp = ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/test/jwt/1234567890", + Header: restaudit.KeyValues{"Accept": "application/json"}, + RequestProcessor: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) + }, + }) + err = json.Unmarshal(resp.Body, &claimsOut) + assert.Nil(err) + assert.Equal(claimsOut, claimsIn) +} + // TestVerifyRequest tests the verification of a token // in a handler. func TestVerifyRequest(t *testing.T) { @@ -71,7 +113,7 @@ func TestVerifyRequest(t *testing.T) { mux := rest.NewMultiplexer() ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key)) + err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, false)) assert.Nil(err) // Perform test request. resp := ts.DoRequest(&restaudit.Request{ @@ -97,10 +139,15 @@ type testHandler struct { id string assert audit.Assertion key jwt.Key + cache jwt.Cache } -func NewTestHandler(id string, assert audit.Assertion, key jwt.Key) rest.ResourceHandler { - return &testHandler{id, assert, key} +func NewTestHandler(id string, assert audit.Assertion, key jwt.Key, useCache bool) rest.ResourceHandler { + var cache jwt.Cache + if useCache { + cache = jwt.NewCache(time.Minute, time.Minute, time.Minute, 10) + } + return &testHandler{id, assert, key, cache} } func (th *testHandler) ID() string { @@ -120,7 +167,13 @@ func (th *testHandler) Get(job rest.Job) (bool, error) { } func (th *testHandler) testDecode(job rest.Job) (bool, error) { - jwtOut, err := jwt.DecodeFromJob(job) + decode := func() (jwt.JWT, error) { + if th.cache == nil { + return jwt.DecodeFromJob(job) + } + return jwt.DecodeCachedFromJob(job, th.cache) + } + jwtOut, err := decode() th.assert.Nil(err) th.assert.True(jwtOut.IsValid(time.Minute)) subject, ok := jwtOut.Claims().Subject() @@ -131,7 +184,13 @@ func (th *testHandler) testDecode(job rest.Job) (bool, error) { } func (th *testHandler) testVerify(job rest.Job) (bool, error) { - jwtOut, err := jwt.VerifyFromJob(job, th.key) + verify := func() (jwt.JWT, error) { + if th.cache == nil { + return jwt.VerifyFromJob(job, th.key) + } + return jwt.VerifyCachedFromJob(job, th.cache, th.key) + } + jwtOut, err := verify() th.assert.Nil(err) th.assert.True(jwtOut.IsValid(time.Minute)) subject, ok := jwtOut.Claims().Subject() From 65f1fbc86f00ea5c87a73102748efa9e13ae18a4 Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 15 Sep 2016 11:41:35 +0200 Subject: [PATCH 032/127] Added test for verified JWT header access with cache --- README.md | 2 +- jwt/doc.go | 2 +- jwt/header_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9d7652..896d8ea 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-14 +Version 2.0.0 beta 2016-09-15 ## Packages diff --git a/jwt/doc.go b/jwt/doc.go index af9ea3d..313a03c 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-14") + return version.New(2, 0, 0, "beta", "2016-09-15") } // EOF diff --git a/jwt/header_test.go b/jwt/header_test.go index 1592a55..d4cc88d 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -130,6 +130,48 @@ func TestVerifyRequest(t *testing.T) { assert.Equal(claimsOut, claimsIn) } +// TestVerifyCachedRequest tests the verification of a token +// in a handler including usage of the cache. +func TestVerifyCachedRequest(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing verify a request token using a cache") + key := []byte("secret") + claimsIn := initClaims() + jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) + assert.Nil(err) + // Setup the test server. + mux := rest.NewMultiplexer() + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, true)) + assert.Nil(err) + // Perform first test request. + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/test/jwt/1234567890", + Header: restaudit.KeyValues{"Accept": "application/json"}, + RequestProcessor: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) + }, + }) + var claimsOut jwt.Claims + err = json.Unmarshal(resp.Body, &claimsOut) + assert.Nil(err) + assert.Equal(claimsOut, claimsIn) + // Perform second test request. + resp = ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/test/jwt/1234567890", + Header: restaudit.KeyValues{"Accept": "application/json"}, + RequestProcessor: func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) + }, + }) + err = json.Unmarshal(resp.Body, &claimsOut) + assert.Nil(err) + assert.Equal(claimsOut, claimsIn) +} + //-------------------- // HANDLER //-------------------- From 53eaef554f2c646768f3c6ae6d70c638d468f491 Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 15 Sep 2016 17:30:36 +0200 Subject: [PATCH 033/127] Started testing of the JWT authorization handler --- handlers/doc.go | 2 +- handlers/handlers_test.go | 33 +++++++++++++++ handlers/jwtauth.go | 87 +++++++++++++++++++++++++++++---------- restaudit/doc.go | 2 +- restaudit/restaudit.go | 2 + 5 files changed, 103 insertions(+), 23 deletions(-) diff --git a/handlers/doc.go b/handlers/doc.go index a428066..90c9def 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-13") + return version.New(2, 0, 0, "beta", "2016-09-15") } // EOF diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index a892a21..7973e82 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -111,4 +111,37 @@ func TestFileUploadHandler(t *testing.T) { ts.DoUpload("/test/files", "testfile", "test.txt", data) } +// TestJWTAuthorizationHandler tests the authorization process +// using JSON Web Tokens. +func TestJWTAuthorizationHandler(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + tests := []struct { + id string + config *handlers.JWTAuthorizationConfig + status int + body string + }{ + { + id: "no-jwt", + status: 401, + }, + } + // Run defined tests. + mux := rest.NewMultiplexer() + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + for i, test := range tests { + // Prepare one test. + assert.Logf("JWT test #%d: %s", i, test.id) + err := mux.Register("jwt", test.id, handlers.NewJWTAuthorizationHandler(test.id, test.config)) + assert.Nil(err) + // Make request. + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/jwt/" + test.id + "/1234567890", + }) + assert.Equal(resp.Status, test.status) + } +} + // EOF diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 9260bb2..cc27980 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -12,6 +12,7 @@ package handlers //-------------------- import ( + "net/http" "time" "github.com/tideland/gorest/jwt" @@ -22,25 +23,49 @@ import ( // JWT AUTHORIZATION HANDLER //-------------------- -// GatekeeperFunc has to be defined by the handler user to check if the -// claims contain the required authorization information. In case it doesn't -// the function has to return false. -type GatekeeperFunc func(job rest.Job, claims jwt.Claims) (bool, error) +// JWTAuthorizationConfig allows to control how the JWT authorization +// handler works. All values are optional. In this case tokens are only +// decoded without using a cache, validated for the time, and there's +// no user defined gatekeeper function running afterwards. +type JWTAuthorizationConfig struct { + Cache jwt.Cache + Key jwt.Key + Leeway time.Duration + Gatekeeper func(job rest.Job, claims jwt.Claims) (bool, error) +} // jwtAuthorizationHandler checks for a valid token and then runs // a gatekeeper function. type jwtAuthorizationHandler struct { id string + cache jwt.Cache key jwt.Key - gatekeeper GatekeeperFunc + leeway time.Duration + gatekeeper func(job rest.Job, claims jwt.Claims) (bool, error) } // NewJWTAuthorizationHandler creates a handler checking for a valid JSON -// Web Token in each request. In case the request has one the configured -// gatekeeper function will be called with job and claims for further -// validation. -func NewjwtAuthorizationHandler(id string, key jwt.Key, gf GatekeeperFunc) rest.ResourceHandler { - return &jwtAuthorizationHandler{id, key, gf} +// Web Token in each request. +func NewJWTAuthorizationHandler(id string, config *JWTAuthorizationConfig) rest.ResourceHandler { + h := &jwtAuthorizationHandler{ + id: id, + leeway: time.Minute, + } + if config != nil { + if config.Cache != nil { + h.cache = config.Cache + } + if config.Key != nil { + h.key = config.Key + } + if config.Leeway != 0 { + h.leeway = config.Leeway + } + if config.Gatekeeper != nil { + h.gatekeeper = config.Gatekeeper + } + } + return h } // ID is specified on the ResourceHandler interface. @@ -90,26 +115,46 @@ func (h *jwtAuthorizationHandler) Options(job rest.Job) (bool, error) { // check is used by all methods to check the token. func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { - token, err := jwt.VerifyFromJob(job, h.key) + var jobJWT jwt.JWT + var err error + switch { + case h.cache != nil && h.key != nil: + jobJWT, err = jwt.VerifyCachedFromJob(job, h.cache, h.key) + case h.cache != nil && h.key == nil: + jobJWT, err = jwt.DecodeCachedFromJob(job, h.cache) + case h.cache == nil && h.key != nil: + jobJWT, err = jwt.VerifyFromJob(job, h.key) + default: + jobJWT, err = jwt.DecodeFromJob(job) + } if err != nil { return false, h.deny(job, err.Error()) } - if !token.IsValid(time.Minute) { - // TODO Configurable leeway. - return false, h.deny(job, "invalid JSON web token") + if jobJWT == nil { + return false, h.deny(job, "no JSON Web Token") + } + if !jobJWT.IsValid(h.leeway) { + return false, h.deny(job, "invalid JSON Web Token") + } + if h.gatekeeper != nil { + return h.gatekeeper(job, jobJWT.Claims()) } - return h.gatekeeper(job, token.Claims()) + return true, nil } // deny sends a negative feedback to the caller. func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { - var f rest.Formatter - if job.AcceptsContentType(rest.ContentTypeJSON) { - f = job.JSON(true) - } else { - f = job.XML() + job.ResponseWriter().WriteHeader(http.StatusUnauthorized) + switch { + case job.AcceptsContentType(rest.ContentTypeJSON): + return rest.NegativeFeedback(job.JSON(true), msg) + case job.AcceptsContentType(rest.ContentTypeXML): + return rest.NegativeFeedback(job.XML(), msg) + default: + job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypePlain) + job.ResponseWriter().Write([]byte(msg)) + return nil } - return rest.NegativeFeedback(f, msg) } // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index 8d4dee5..2f754f2 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-10") + return version.New(2, 0, 0, "beta", "2016-09-15") } // EOF diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 3ac1191..3d19e8f 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -42,6 +42,7 @@ type Request struct { // Response wraps all infos of a test response. type Response struct { + Status int Header KeyValues Cookies KeyValues Body []byte @@ -149,6 +150,7 @@ func (ts *testServer) response(hr *http.Response) *Response { ts.assert.Nil(err, "cannot read response") defer hr.Body.Close() return &Response{ + Status: hr.StatusCode, Header: respHeader, Cookies: respCookies, Body: respBody, From 0c3c2b2e4dc9798d0aab81a2c3c4ba84ab5413aa Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 16 Sep 2016 20:55:18 +0000 Subject: [PATCH 034/127] Added further JWT authorization handler tests --- handlers/handlers_test.go | 46 +++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 7973e82..b06a9a2 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -19,10 +19,12 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/tideland/golib/audit" "github.com/tideland/gorest/handlers" + "github.com/tideland/gorest/jwt" "github.com/tideland/gorest/rest" "github.com/tideland/gorest/restaudit" ) @@ -115,14 +117,37 @@ func TestFileUploadHandler(t *testing.T) { // using JSON Web Tokens. func TestJWTAuthorizationHandler(t *testing.T) { assert := audit.NewTestingAssertion(t, true) + key := []byte("secret") tests := []struct { - id string - config *handlers.JWTAuthorizationConfig - status int - body string + id string + tokener func() jwt.JWT + config *handlers.JWTAuthorizationConfig + status int + body string }{ { - id: "no-jwt", + id: "no-token", + status: 401, + }, { + id: "token-no-gatekeeper", + tokener: func() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("test") + out, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + return out + }, + status: 200, + }, { + id: "token-expired", + tokener: func() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("test") + claims.SetExpiration(time.Now().Add(-time.Hour)) + out, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + return out + }, status: 401, }, } @@ -136,9 +161,16 @@ func TestJWTAuthorizationHandler(t *testing.T) { err := mux.Register("jwt", test.id, handlers.NewJWTAuthorizationHandler(test.id, test.config)) assert.Nil(err) // Make request. + var requestProcessor func(req *http.Request) *http.Request + if test.tokener != nil { + requestProcessor = func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, test.tokener()) + } + } resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/jwt/" + test.id + "/1234567890", + Method: "GET", + Path: "/jwt/" + test.id + "/1234567890", + RequestProcessor: requestProcessor, }) assert.Equal(resp.Status, test.status) } From 1a14c6ab14a4bdcf3935c9737e68edff54d6757f Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 16 Sep 2016 21:01:03 +0000 Subject: [PATCH 035/127] More testing --- README.md | 2 +- handlers/doc.go | 2 +- handlers/handlers_test.go | 15 ++++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 896d8ea..b7601dc 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-15 +Version 2.0.0 beta 2016-09-16 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 90c9def..7765b47 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-15") + return version.New(2, 0, 0, "beta", "2016-09-16") } // EOF diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index b06a9a2..9fab290 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -129,7 +129,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { id: "no-token", status: 401, }, { - id: "token-no-gatekeeper", + id: "token-decode-no-gatekeeper", tokener: func() jwt.JWT { claims := jwt.NewClaims() claims.SetSubject("test") @@ -138,6 +138,19 @@ func TestJWTAuthorizationHandler(t *testing.T) { return out }, status: 200, + }, { + id: "token-verify-no-gatekeeper", + tokener: func() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("test") + out, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + return out + }, + config: &handlers.JWTAuthorizationConfig{ + Key: key, + }, + status: 200, }, { id: "token-expired", tokener: func() jwt.JWT { From 9c8415e1ceb373e804c99d96cad588f5769c3f1e Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 17 Sep 2016 19:43:59 +0000 Subject: [PATCH 036/127] Added JWT authorization handler test using cache --- README.md | 2 +- handlers/doc.go | 2 +- handlers/handlers_test.go | 36 +++++++++++++++++++++++++++++------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b7601dc..f47f2b7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-16 +Version 2.0.0 beta 2016-09-17 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 7765b47..e4f02d4 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-16") + return version.New(2, 0, 0, "beta", "2016-09-17") } // EOF diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 9fab290..c945ddf 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -122,6 +122,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { id string tokener func() jwt.JWT config *handlers.JWTAuthorizationConfig + runs int status int body string }{ @@ -151,6 +152,21 @@ func TestJWTAuthorizationHandler(t *testing.T) { Key: key, }, status: 200, + }, { + id: "cached-token-verify-no-gatekeeper", + tokener: func() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("test") + out, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + return out + }, + config: &handlers.JWTAuthorizationConfig{ + Cache: jwt.NewCache(time.Minute, time.Minute, time.Minute, 10), + Key: key, + }, + runs: 5, + status: 200, }, { id: "token-expired", tokener: func() jwt.JWT { @@ -173,19 +189,25 @@ func TestJWTAuthorizationHandler(t *testing.T) { assert.Logf("JWT test #%d: %s", i, test.id) err := mux.Register("jwt", test.id, handlers.NewJWTAuthorizationHandler(test.id, test.config)) assert.Nil(err) - // Make request. var requestProcessor func(req *http.Request) *http.Request if test.tokener != nil { requestProcessor = func(req *http.Request) *http.Request { return jwt.AddTokenToRequest(req, test.tokener()) } } - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/jwt/" + test.id + "/1234567890", - RequestProcessor: requestProcessor, - }) - assert.Equal(resp.Status, test.status) + // Make request(s). + runs := 1 + if test.runs != 0 { + runs = test.runs + } + for i := 0; i < runs; i++ { + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/jwt/" + test.id + "/1234567890", + RequestProcessor: requestProcessor, + }) + assert.Equal(resp.Status, test.status) + } } } From 53c2380042432a8f09b8999491a5af73ac5cf60d Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 17 Sep 2016 20:34:19 +0000 Subject: [PATCH 037/127] Added tests for JWT authorization handler gatekeeper --- handlers/handlers_test.go | 42 +++++++++++++++++++++++++++++++++++++++ handlers/jwtauth.go | 9 ++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index c945ddf..a4d632b 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -13,6 +13,7 @@ package handlers_test import ( "bufio" + "errors" "io/ioutil" "mime/multipart" "net/http" @@ -167,6 +168,47 @@ func TestJWTAuthorizationHandler(t *testing.T) { }, runs: 5, status: 200, + }, { + id: "cached-token-verify-positive-gatekeeper", + tokener: func() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("test") + out, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + return out + }, + config: &handlers.JWTAuthorizationConfig{ + Cache: jwt.NewCache(time.Minute, time.Minute, time.Minute, 10), + Key: key, + Gatekeeper: func(job rest.Job, claims jwt.Claims) error { + subject, ok := claims.Subject() + assert.True(ok) + assert.Equal(subject, "test") + return nil + }, + }, + runs: 5, + status: 200, + }, { + id: "cached-token-verify-negative-gatekeeper", + tokener: func() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("test") + out, err := jwt.Encode(claims, key, jwt.HS512) + assert.Nil(err) + return out + }, + config: &handlers.JWTAuthorizationConfig{ + Cache: jwt.NewCache(time.Minute, time.Minute, time.Minute, 10), + Key: key, + Gatekeeper: func(job rest.Job, claims jwt.Claims) error { + _, ok := claims.Subject() + assert.True(ok) + return errors.New("subject is test") + }, + }, + runs: 1, + status: 401, }, { id: "token-expired", tokener: func() jwt.JWT { diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index cc27980..3a00057 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -31,7 +31,7 @@ type JWTAuthorizationConfig struct { Cache jwt.Cache Key jwt.Key Leeway time.Duration - Gatekeeper func(job rest.Job, claims jwt.Claims) (bool, error) + Gatekeeper func(job rest.Job, claims jwt.Claims) error } // jwtAuthorizationHandler checks for a valid token and then runs @@ -41,7 +41,7 @@ type jwtAuthorizationHandler struct { cache jwt.Cache key jwt.Key leeway time.Duration - gatekeeper func(job rest.Job, claims jwt.Claims) (bool, error) + gatekeeper func(job rest.Job, claims jwt.Claims) error } // NewJWTAuthorizationHandler creates a handler checking for a valid JSON @@ -137,7 +137,10 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { return false, h.deny(job, "invalid JSON Web Token") } if h.gatekeeper != nil { - return h.gatekeeper(job, jobJWT.Claims()) + err := h.gatekeeper(job, jobJWT.Claims()) + if err != nil { + return false, h.deny(job, "gatekeeper denied:"+err.Error()) + } } return true, nil } From 379e83a75f73bdda74ec6737e439edc0df8ded5d Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 18 Sep 2016 17:40:38 +0000 Subject: [PATCH 038/127] Finalizing for release --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/doc.go | 2 +- jwt/doc.go | 2 +- rest/doc.go | 2 +- restaudit/doc.go | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbb9da..9252a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-09-19 + +- Finished rework after adding of JSON Web Token package + ## 2016-08-21 - Migrated *Tideland Go Library* web package after some rework diff --git a/README.md b/README.md index f47f2b7..63fd826 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 beta 2016-09-17 +Version 2.0.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index e4f02d4..2c4ce0f 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-17") + return version.New(2, 0, 0) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index 313a03c..b533e5b 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-15") + return version.New(2, 0, 0) } // EOF diff --git a/rest/doc.go b/rest/doc.go index 369d364..e1072bb 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -91,7 +91,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-12") + return version.New(2, 0, 0) } // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index 2f754f2..ea1fcec 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0, "beta", "2016-09-15") + return version.New(2, 0, 0) } // EOF From 208f61af167adafa75075ecbc654ae0bc72551f7 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 27 Sep 2016 13:29:31 +0200 Subject: [PATCH 039/127] Added methods for the lazy loading and rendering of templates --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/doc.go | 2 +- jwt/doc.go | 2 +- rest/doc.go | 2 +- rest/job.go | 12 +++++++++++- rest/templates.go | 25 ++++++++++++++++++++++--- restaudit/doc.go | 2 +- 8 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9252a07..e3fc4a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-09-27 + +- Added methods for the lazy loading and rendering of templates + ## 2016-09-19 - Finished rework after adding of JSON Web Token package diff --git a/README.md b/README.md index 63fd826..81e94bf 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.0.0 +Version 2.1.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 2c4ce0f..4d3fc28 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0) + return version.New(2, 1, 0) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index b533e5b..c170db0 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0) + return version.New(2, 1, 0) } // EOF diff --git a/rest/doc.go b/rest/doc.go index e1072bb..6cf2cc6 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -91,7 +91,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0) + return version.New(2, 1, 0) } // EOF diff --git a/rest/job.go b/rest/job.go index a9d13d8..9965f37 100644 --- a/rest/job.go +++ b/rest/job.go @@ -66,9 +66,14 @@ type Job interface { // Redirect to a domain, resource and resource ID (optional). Redirect(domain, resource, resourceID string) - // RenderTemplate renders a template with the passed data. + // RenderTemplate renders a cached template with the passed data. RenderTemplate(templateID string, data interface{}) error + // LoadAndRenderTemplate lazy loads a template into the cache and renders + // it with the passed data. So in case it already has been loaded and + // parsed once it will be reused. + LoadAndRenderTemplate(templateID, filename, contentType string, data interface{}) error + // GOB returns a GOB formatter. GOB() Formatter @@ -228,6 +233,11 @@ func (j *job) RenderTemplate(templateID string, data interface{}) error { return j.environment.Templates().Render(j.responseWriter, templateID, data) } +// LoadAndRenderTemplate is specified on the Job interface. +func (j *job) LoadAndRenderTemplate(templateID, filename, contentType string, data interface{}) error { + return j.environment.Templates().LoadAndRender(j.responseWriter, templateID, filename, contentType, data) +} + // GOB is specified on the Job interface. func (j *job) GOB() Formatter { return &gobFormatter{j} diff --git a/rest/templates.go b/rest/templates.go index 4343f14..51dc0a5 100644 --- a/rest/templates.go +++ b/rest/templates.go @@ -66,6 +66,12 @@ type TemplatesCache interface { // Render executes the pre-parsed template with the data. // It also sets the content type header. Render(rw http.ResponseWriter, id string, data interface{}) error + + // LoadAndRender checks if the template with the given id + // has already been parsed. In this case it will use it, + // otherwise the template will be loaded, parsed, added + // to the cache, and used then. + LoadAndRender(rw http.ResponseWriter, id, filename, contentType string, data interface{}) error } // templates implements the TemplatesCache interface. @@ -81,7 +87,7 @@ func NewTemplatesCache() TemplatesCache { } } -// Parse is specified on the Templates interface. +// Parse impements the TemplatesCache interface. func (t *templates) Parse(id, rawTemplate, contentType string) error { t.mutex.Lock() defer t.mutex.Unlock() @@ -93,7 +99,7 @@ func (t *templates) Parse(id, rawTemplate, contentType string) error { return nil } -// LoadAndParse is specified on the Templates interface. +// LoadAndParse implements the TemplatesCache interface. func (t *templates) LoadAndParse(id, filename, contentType string) error { rawTemplate, err := ioutil.ReadFile(filename) if err != nil { @@ -102,7 +108,7 @@ func (t *templates) LoadAndParse(id, filename, contentType string) error { return t.Parse(id, string(rawTemplate), contentType) } -// Render is specified on the Templates interface. +// Render implements the TemplatesCache interface. func (t *templates) Render(rw http.ResponseWriter, id string, data interface{}) error { t.mutex.RLock() defer t.mutex.RUnlock() @@ -113,4 +119,17 @@ func (t *templates) Render(rw http.ResponseWriter, id string, data interface{}) return entry.render(rw, data) } +// LoadAndRender implements the TemplatesCache interface. +func (t *templates) LoadAndRender(rw http.ResponseWriter, id, filename, contentType string, data interface{}) error { + t.mutex.RLock() + _, ok := t.items[id] + t.mutex.RUnlock() + if !ok { + if err := t.LoadAndParse(id, filename, contentType); err != nil { + return err + } + } + return t.Render(rw, id, data) +} + // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index ea1fcec..35bcb41 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 0, 0) + return version.New(2, 1, 0) } // EOF From 4d6f8ab4ba383456db1a9428d0dfe8f2f977a5c6 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 27 Sep 2016 19:33:42 +0000 Subject: [PATCH 040/127] Finished change of the template rendering interface --- CHANGELOG.md | 1 + rest/job.go | 20 ++++---------- rest/rest_test.go | 2 +- rest/templates.go | 68 +++++++++++++++++++++++++++++++++++------------ 4 files changed, 58 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fc4a4..7d8dd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2016-09-27 - Added methods for the lazy loading and rendering of templates +- Sadly has little impact on the rendering interface ## 2016-09-19 diff --git a/rest/job.go b/rest/job.go index 9965f37..411cd94 100644 --- a/rest/job.go +++ b/rest/job.go @@ -66,13 +66,8 @@ type Job interface { // Redirect to a domain, resource and resource ID (optional). Redirect(domain, resource, resourceID string) - // RenderTemplate renders a cached template with the passed data. - RenderTemplate(templateID string, data interface{}) error - - // LoadAndRenderTemplate lazy loads a template into the cache and renders - // it with the passed data. So in case it already has been loaded and - // parsed once it will be reused. - LoadAndRenderTemplate(templateID, filename, contentType string, data interface{}) error + // Renderer returns a template renderer. + Renderer() Renderer // GOB returns a GOB formatter. GOB() Formatter @@ -228,14 +223,9 @@ func (j *job) Redirect(domain, resource, resourceID string) { http.Redirect(j.responseWriter, j.request, path, http.StatusTemporaryRedirect) } -// RenderTemplate is specified on the Job interface. -func (j *job) RenderTemplate(templateID string, data interface{}) error { - return j.environment.Templates().Render(j.responseWriter, templateID, data) -} - -// LoadAndRenderTemplate is specified on the Job interface. -func (j *job) LoadAndRenderTemplate(templateID, filename, contentType string, data interface{}) error { - return j.environment.Templates().LoadAndRender(j.responseWriter, templateID, filename, contentType, data) +// Renderer is specified on the Job interface. +func (j *job) Renderer() Renderer { + return &renderer{j.responseWriter, j.environment.Templates()} } // GOB is specified on the Job interface. diff --git a/rest/rest_test.go b/rest/rest_test.go index adde3dc..aaec2de 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -333,7 +333,7 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { job.JSON(true).Write(data) default: th.assert.Logf("GET HTML") - job.RenderTemplate("test:context:html", data) + job.Renderer().Render("test:context:html", data) } return true, nil } diff --git a/rest/templates.go b/rest/templates.go index 51dc0a5..d9b949b 100644 --- a/rest/templates.go +++ b/rest/templates.go @@ -1,4 +1,4 @@ -// Tideland Go REST Server Library - REST - Templates +// Tideland Go REST Server Library - REST - templatesCache // // Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany // @@ -22,12 +22,12 @@ import ( ) //-------------------- -// TEMPLATE CACHE ITEM +// templatesCache CACHE ITEM //-------------------- -// templatesItem stores the parsed template and the +// templatesCacheItem stores the parsed template and the // content type. -type templatesItem struct { +type templatesCacheItem struct { id string timestamp time.Time parsedTemplate *template.Template @@ -36,12 +36,12 @@ type templatesItem struct { // isValid checks if the the entry is younger than the // passed validity period. -func (ti *templatesItem) isValid(validityPeriod time.Duration) bool { +func (ti *templatesCacheItem) isValid(validityPeriod time.Duration) bool { return ti.timestamp.Add(validityPeriod).After(time.Now()) } // render the cached entry. -func (ti *templatesItem) render(rw http.ResponseWriter, data interface{}) error { +func (ti *templatesCacheItem) render(rw http.ResponseWriter, data interface{}) error { rw.Header().Set("Content-Type", ti.contentType) if err := ti.parsedTemplate.Execute(rw, data); err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) @@ -51,7 +51,7 @@ func (ti *templatesItem) render(rw http.ResponseWriter, data interface{}) error } //-------------------- -// TEMPLATE CACHE +// TEMPLATES CACHE //-------------------- // TemplatesCache caches and renders templates. @@ -74,33 +74,33 @@ type TemplatesCache interface { LoadAndRender(rw http.ResponseWriter, id, filename, contentType string, data interface{}) error } -// templates implements the TemplatesCache interface. -type templates struct { +// templatesCache implements the TemplatesCache interface. +type templatesCache struct { mutex sync.RWMutex - items map[string]*templatesItem + items map[string]*templatesCacheItem } // NewTemplates creates a new template cache. func NewTemplatesCache() TemplatesCache { - return &templates{ - items: make(map[string]*templatesItem), + return &templatesCache{ + items: make(map[string]*templatesCacheItem), } } // Parse impements the TemplatesCache interface. -func (t *templates) Parse(id, rawTemplate, contentType string) error { +func (t *templatesCache) Parse(id, rawTemplate, contentType string) error { t.mutex.Lock() defer t.mutex.Unlock() parsedTemplate, err := template.New(id).Parse(rawTemplate) if err != nil { return err } - t.items[id] = &templatesItem{id, time.Now(), parsedTemplate, contentType} + t.items[id] = &templatesCacheItem{id, time.Now(), parsedTemplate, contentType} return nil } // LoadAndParse implements the TemplatesCache interface. -func (t *templates) LoadAndParse(id, filename, contentType string) error { +func (t *templatesCache) LoadAndParse(id, filename, contentType string) error { rawTemplate, err := ioutil.ReadFile(filename) if err != nil { return err @@ -109,7 +109,7 @@ func (t *templates) LoadAndParse(id, filename, contentType string) error { } // Render implements the TemplatesCache interface. -func (t *templates) Render(rw http.ResponseWriter, id string, data interface{}) error { +func (t *templatesCache) Render(rw http.ResponseWriter, id string, data interface{}) error { t.mutex.RLock() defer t.mutex.RUnlock() entry, ok := t.items[id] @@ -120,7 +120,7 @@ func (t *templates) Render(rw http.ResponseWriter, id string, data interface{}) } // LoadAndRender implements the TemplatesCache interface. -func (t *templates) LoadAndRender(rw http.ResponseWriter, id, filename, contentType string, data interface{}) error { +func (t *templatesCache) LoadAndRender(rw http.ResponseWriter, id, filename, contentType string, data interface{}) error { t.mutex.RLock() _, ok := t.items[id] t.mutex.RUnlock() @@ -132,4 +132,38 @@ func (t *templates) LoadAndRender(rw http.ResponseWriter, id, filename, contentT return t.Render(rw, id, data) } +//-------------------- +// RENDERER +//-------------------- + +// Renderer renders templates. It is returned by a Job and knows +// where to render it. +type Renderer interface { + // Render executes the pre-parsed template with the data. + // It also sets the content type header. + Render(id string, data interface{}) error + + // LoadAndRender checks if the template with the given id + // has already been parsed. In this case it will use it, + // otherwise the template will be loaded, parsed, added + // to the cache, and used then. + LoadAndRender(id, filename, contentType string, data interface{}) error +} + +// renderer implements the Renderer interface. +type renderer struct { + rw http.ResponseWriter + tc TemplatesCache +} + +// Render implements the Renderer interface. +func (r *renderer) Render(id string, data interface{}) error { + return r.tc.Render(r.rw, id, data) +} + +// LoadAndRender implements the Renderer interface. +func (r *renderer) LoadAndRender(id, filename, contentType string, data interface{}) error { + return r.tc.LoadAndRender(r.rw, id, filename, contentType, data) +} + // EOF From 7ef17416370c8ebba805d0c13dbf76f8d2adeb54 Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 29 Sep 2016 11:24:49 +0200 Subject: [PATCH 041/127] Fixed public handler bug --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/doc.go | 2 +- handlers/fileserve.go | 16 ++++++++-------- handlers/fileupload.go | 12 ++++++------ handlers/wrapper.go | 29 +++++++++++++++-------------- jwt/doc.go | 2 +- rest/doc.go | 2 +- restaudit/doc.go | 2 +- 9 files changed, 38 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d8dd93..534def3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-09-29 + +- Fixed bug with public handler types + ## 2016-09-27 - Added methods for the lazy loading and rendering of templates diff --git a/README.md b/README.md index 81e94bf..bfb278b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.1.0 +Version 2.1.1 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 4d3fc28..26ad263 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 0) + return version.New(2, 1, 1) } // EOF diff --git a/handlers/fileserve.go b/handlers/fileserve.go index 12b77ed..b22b815 100644 --- a/handlers/fileserve.go +++ b/handlers/fileserve.go @@ -25,34 +25,34 @@ import ( // FILE SERVER HANDLER //-------------------- -// FileServeHandler serves files identified by the resource ID part out -// of the configured local directory. -type FileServeHandler struct { +// fileServeHandler implements the file server. +type fileServeHandler struct { id string dir string } -// NewFileServeHandler creates a new handler with a directory. +// NewFileServeHandler creates a new handler serving the files names +// by the resource ID part out of the passed directory. func NewFileServeHandler(id, dir string) rest.ResourceHandler { pdir := filepath.FromSlash(dir) if !strings.HasSuffix(pdir, string(filepath.Separator)) { pdir += string(filepath.Separator) } - return &FileServeHandler{id, pdir} + return &fileServeHandler{id, pdir} } // ID is specified on the ResourceHandler interface. -func (h *FileServeHandler) ID() string { +func (h *fileServeHandler) ID() string { return h.id } // Init is specified on the ResourceHandler interface. -func (h *FileServeHandler) Init(env rest.Environment, domain, resource string) error { +func (h *fileServeHandler) Init(env rest.Environment, domain, resource string) error { return nil } // Get is specified on the GetResourceHandler interface. -func (h *FileServeHandler) Get(job rest.Job) (bool, error) { +func (h *fileServeHandler) Get(job rest.Job) (bool, error) { filename := h.dir + job.ResourceID() logger.Infof("serving file %q", filename) http.ServeFile(job.ResponseWriter(), job.Request(), filename) diff --git a/handlers/fileupload.go b/handlers/fileupload.go index e48a1cb..79f2696 100644 --- a/handlers/fileupload.go +++ b/handlers/fileupload.go @@ -32,32 +32,32 @@ const defaultMaxMemory = 32 << 20 // 32 MB // a database. type FileUploadProcessor func(job rest.Job, header *multipart.FileHeader, file multipart.File) error -// FileUploadHandler handles uploading POST requests. -type FileUploadHandler struct { +// fileUploadHandler handles uploading POST requests. +type fileUploadHandler struct { id string processor FileUploadProcessor } // NewFileUploadHandler creates a new handler for the uploading of files. func NewFileUploadHandler(id string, processor FileUploadProcessor) rest.ResourceHandler { - return &FileUploadHandler{ + return &fileUploadHandler{ id: id, processor: processor, } } // Init is specified on the ResourceHandler interface. -func (h *FileUploadHandler) ID() string { +func (h *fileUploadHandler) ID() string { return h.id } // ID is specified on the ResourceHandler interface. -func (h *FileUploadHandler) Init(env rest.Environment, domain, resource string) error { +func (h *fileUploadHandler) Init(env rest.Environment, domain, resource string) error { return nil } // Post is specified on the PostResourceHandler interface. -func (h *FileUploadHandler) Post(job rest.Job) (bool, error) { +func (h *fileUploadHandler) Post(job rest.Job) (bool, error) { if err := job.Request().ParseMultipartForm(defaultMaxMemory); err != nil { return false, errors.Annotate(err, ErrUploadingFile, errorMessages) } diff --git a/handlers/wrapper.go b/handlers/wrapper.go index d555c9c..6561e28 100644 --- a/handlers/wrapper.go +++ b/handlers/wrapper.go @@ -21,66 +21,67 @@ import ( // WRAPPER HANDLER //-------------------- -// WrapperHandler wraps existing handler functions for a usage inside -// the rest package. -type WrapperHandler struct { +// wrapperHandler wraps existing handler functions for a usage inside +// the rest library. +type wrapperHandler struct { id string handle http.HandlerFunc } -// NewWrapperHandler creates a new wrapper around a handler function. +// NewWrapperHandler creates a new wrapper around a standard +// handler function. func NewWrapperHandler(id string, hf http.HandlerFunc) rest.ResourceHandler { - return &WrapperHandler{id, hf} + return &wrapperHandler{id, hf} } // ID is specified on the ResourceHandler interface. -func (h *WrapperHandler) ID() string { +func (h *wrapperHandler) ID() string { return h.id } // Init is specified on the ResourceHandler interface. -func (h *WrapperHandler) Init(env rest.Environment, domain, resource string) error { +func (h *wrapperHandler) Init(env rest.Environment, domain, resource string) error { return nil } // Get is specified on the GetResourceHandler interface. -func (h *WrapperHandler) Get(job rest.Job) (bool, error) { +func (h *wrapperHandler) Get(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } // Head is specified on the HeadResourceHandler interface. -func (h *WrapperHandler) Head(job rest.Job) (bool, error) { +func (h *wrapperHandler) Head(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } // Put is specified on the PutResourceHandler interface. -func (h *WrapperHandler) Put(job rest.Job) (bool, error) { +func (h *wrapperHandler) Put(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } // Post is specified on the PostResourceHandler interface. -func (h *WrapperHandler) Post(job rest.Job) (bool, error) { +func (h *wrapperHandler) Post(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } // Patch is specified on the PatchResourceHandler interface. -func (h *WrapperHandler) Patch(job rest.Job) (bool, error) { +func (h *wrapperHandler) Patch(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } // Delete is specified on the DeleteResourceHandler interface. -func (h *WrapperHandler) Delete(job rest.Job) (bool, error) { +func (h *wrapperHandler) Delete(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } // Options is specified on the OptionsResourceHandler interface. -func (h *WrapperHandler) Options(job rest.Job) (bool, error) { +func (h *wrapperHandler) Options(job rest.Job) (bool, error) { h.handle(job.ResponseWriter(), job.Request()) return true, nil } diff --git a/jwt/doc.go b/jwt/doc.go index c170db0..c1fb51b 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 0) + return version.New(2, 1, 1) } // EOF diff --git a/rest/doc.go b/rest/doc.go index 6cf2cc6..85ddceb 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -91,7 +91,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 0) + return version.New(2, 1, 1) } // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index 35bcb41..e25c9a1 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 0) + return version.New(2, 1, 1) } // EOF From e602aa2a4872b712aa56d3a1cfbac2abb1aec26b Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 4 Oct 2016 16:05:59 +0200 Subject: [PATCH 042/127] Improved mutliplexer startup External context now can be passed into a new environment. Additionally the confiuration now is based on the etc package that is part of the Tideland Go Library. While changing this found an unsafe basepath behavior, so fixed it. --- CHANGELOG.md | 8 +++ README.md | 2 +- handlers/doc.go | 2 +- handlers/handlers_test.go | 25 +++++-- jwt/doc.go | 2 +- jwt/header_test.go | 23 +++++-- rest/context.go | 32 +++++++-- rest/doc.go | 18 +++-- rest/environment.go | 140 +++++++++++++++----------------------- rest/job.go | 68 +++++++++--------- rest/multiplexer.go | 22 ++++-- rest/rest_test.go | 71 ++++++++++++------- rest/templates.go | 4 +- restaudit/doc.go | 2 +- 14 files changed, 247 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 534def3..a7e0f24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Tideland Go REST Server Library +## 2016-10-04 + +- Improved passing external contexts into an environment, e.g. + containing database connection pools +- Changed multiplexer configuration to now use *etc.Etc* from + the *Tideland Go Library* +- More robust basepath handling now + ## 2016-09-29 - Fixed bug with public handler types diff --git a/README.md b/README.md index bfb278b..bdb8822 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.1.1 +Version 2.2.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 26ad263..040f1d5 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 1) + return version.New(2, 2, 0) } // EOF diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index a4d632b..3554028 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -13,6 +13,7 @@ package handlers_test import ( "bufio" + "context" "errors" "io/ioutil" "mime/multipart" @@ -23,6 +24,7 @@ import ( "time" "github.com/tideland/golib/audit" + "github.com/tideland/golib/etc" "github.com/tideland/gorest/handlers" "github.com/tideland/gorest/jwt" @@ -40,7 +42,7 @@ func TestWrapperHandler(t *testing.T) { assert := audit.NewTestingAssertion(t, true) data := "Been there, done that!" // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() handler := func(rw http.ResponseWriter, r *http.Request) { @@ -61,7 +63,7 @@ func TestFileServeHandler(t *testing.T) { assert := audit.NewTestingAssertion(t, true) data := "Been there, done that!" // Setup the test file. - dir, err := ioutil.TempDir("", "golib-rest") + dir, err := ioutil.TempDir("", "gorest") assert.Nil(err) defer os.RemoveAll(dir) filename := filepath.Join(dir, "foo.txt") @@ -73,7 +75,7 @@ func TestFileServeHandler(t *testing.T) { err = f.Close() assert.Nil(err) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err = mux.Register("test", "files", handlers.NewFileServeHandler("files", dir)) @@ -105,7 +107,7 @@ func TestFileUploadHandler(t *testing.T) { return nil } // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "files", handlers.NewFileUploadHandler("files", processor)) @@ -223,7 +225,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { }, } // Run defined tests. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() for i, test := range tests { @@ -253,4 +255,17 @@ func TestJWTAuthorizationHandler(t *testing.T) { } } +//-------------------- +// HELPERS +//-------------------- + +// newMultiplexer creates a new multiplexer with a testing context +// and a testing configuration. +func newMultiplexer(assert audit.Assertion) rest.Multiplexer { + cfgStr := "{etc {basepath /}{default-domain default}{default-resource default}}" + cfg, err := etc.ReadString(cfgStr) + assert.Nil(err) + return rest.NewMultiplexer(context.Background(), cfg) +} + // EOF diff --git a/jwt/doc.go b/jwt/doc.go index c1fb51b..29c8c61 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 1) + return version.New(2, 2, 0) } // EOF diff --git a/jwt/header_test.go b/jwt/header_test.go index d4cc88d..c592a5c 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -12,12 +12,14 @@ package jwt_test //-------------------- import ( + "context" "encoding/json" "net/http" "testing" "time" "github.com/tideland/golib/audit" + "github.com/tideland/golib/etc" "github.com/tideland/gorest/jwt" "github.com/tideland/gorest/rest" @@ -38,7 +40,7 @@ func TestDecodeRequest(t *testing.T) { jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) assert.Nil(err) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) @@ -68,7 +70,7 @@ func TestDecodeCachedRequest(t *testing.T) { jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) assert.Nil(err) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, true)) @@ -110,7 +112,7 @@ func TestVerifyRequest(t *testing.T) { jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) assert.Nil(err) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, false)) @@ -140,7 +142,7 @@ func TestVerifyCachedRequest(t *testing.T) { jwtIn, err := jwt.Encode(claimsIn, key, jwt.HS512) assert.Nil(err) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, true)) @@ -242,4 +244,17 @@ func (th *testHandler) testVerify(job rest.Job) (bool, error) { return true, nil } +//-------------------- +// HELPERS +//-------------------- + +// newMultiplexer creates a new multiplexer with a testing context +// and a testing configuration. +func newMultiplexer(assert audit.Assertion) rest.Multiplexer { + cfgStr := "{etc {basepath /}{default-domain default}{default-resource default}}" + cfg, err := etc.ReadString(cfgStr) + assert.Nil(err) + return rest.NewMultiplexer(context.Background(), cfg) +} + // EOF diff --git a/rest/context.go b/rest/context.go index 4d10664..4857ef0 100644 --- a/rest/context.go +++ b/rest/context.go @@ -22,20 +22,38 @@ import ( // contextKey is used to address data inside a context. type contextKey int -// jobKey addresses the worksheet inside the context. -const jobKey contextKey = 0 +const ( + // envKey addresses the environment inside the context. + envKey contextKey = 0 + + // jobKey addresses the job inside the context. + jobKey contextKey = 1 +) //-------------------- // CONTEXT //-------------------- -// newContext creates a context containing the passed job. -func newContext(job Job) context.Context { - return context.WithValue(context.Background(), jobKey, job) +// newEnvironmentContext creates a context based on the passed one +// and containing the passed environment. +func newEnvironmentContext(ctx context.Context, env Environment) context.Context { + return context.WithValue(ctx, envKey, env) +} + +// newJobContext creates a context based on the passed one +// and containing the passed job. +func newJobContext(ctx context.Context, job Job) context.Context { + return context.WithValue(ctx, jobKey, job) +} + +// EnvironmentFromContext retrieves the environment out of a context. +func EnvironmentFromContext(ctx context.Context) (Environment, bool) { + env, ok := ctx.Value(envKey).(Environment) + return env, ok } -// FromContext retrieves the job out of a context. -func FromContext(ctx context.Context) (Job, bool) { +// JobFromContext retrieves the job out of a context. +func JobFromContext(ctx context.Context) (Job, bool) { job, ok := ctx.Value(jobKey).(Job) return job, ok } diff --git a/rest/doc.go b/rest/doc.go index 85ddceb..dcd0e79 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -67,14 +67,24 @@ // the Handler interface of the net/http package. So the typical order // is // -// mux := rest.NewMultiplexer() +// mux := rest.NewMultiplexer(ctx, cfg) +// +// to start the multiplexer with a given context and the configuration +// for the multiplexer. The configuration is using the Tideland Go +// Library etc.Etc, parameters can be found at the NewMultiplexer +// documentation. After creating the multiplexer call +// // mux.Register("domain", "resource-type-a", NewTypeAHandler("foo")) // mux.Register("domain", "resource-type-b", NewTypeBHandler("bar")) // mux.Register("admin", "user", NewUserManagementHandler()) +// +// to register the handlers per domain and resource. The server then can +// be started by the standard +// // http.ListenAndServe(":8000", mux) // -// Additionally further handlers can be registered or running once -// removed during runtime. +// Additionally further handlers can be registered or running ones +// removed during the runtime. package rest //-------------------- @@ -91,7 +101,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 1) + return version.New(2, 2, 0) } // EOF diff --git a/rest/environment.go b/rest/environment.go index b390de1..e02d337 100644 --- a/rest/environment.go +++ b/rest/environment.go @@ -11,15 +11,23 @@ package rest // IMPORTS //-------------------- -import () +import ( + "context" + + "github.com/tideland/golib/etc" + "github.com/tideland/golib/stringex" +) //-------------------- // ENVIRONMENT //-------------------- type Environment interface { - // BasePath returns the configured base path. - BasePath() string + // Context returns the context of the environment. + Context() context.Context + + // Basepath returns the configured basepath. + Basepath() string // DefaultDomain returns the configured default domain. DefaultDomain() string @@ -27,114 +35,76 @@ type Environment interface { // DefaultResource returns the configured default resource. DefaultResource() string - // Templates returns the template cache. - Templates() TemplatesCache + // TemplatesCache returns the template cache. + TemplatesCache() TemplatesCache } // environment implements the Environment interface. type environment struct { - basePath string + ctx context.Context + basepath string + baseparts []string + basepartsLen int defaultDomain string defaultResource string - templates TemplatesCache + templatesCache TemplatesCache } -// Option defines a function for setting an option. -type Option func(env Environment) - -// newEnvironment crerates the default environment and -// checks the passed options. -func newEnvironment(options ...Option) Environment { +// newEnvironment crerates an environment using the +// passed context and configuration. +func newEnvironment(ctx context.Context, cfg etc.Etc) *environment { env := &environment{ - basePath: "/", + basepath: "/", + baseparts: []string{}, defaultDomain: "default", defaultResource: "default", - templates: NewTemplatesCache(), + templatesCache: newTemplatesCache(), + } + // Check configuration. + if cfg != nil { + env.basepath = cfg.ValueAsString("basepath", env.basepath) + env.defaultDomain = cfg.ValueAsString("default-domain", env.defaultDomain) + env.defaultResource = cfg.ValueAsString("default-resource", env.defaultResource) } - for _, option := range options { - option(env) + // Check basepath and remove empty parts. + env.baseparts = stringex.SplitMap(env.basepath, "/", func(p string) (string, bool) { + if p == "" { + return "", false + } + return p, true + }) + env.basepartsLen = len(env.baseparts) + // Set context. + if ctx == nil { + ctx = context.Background() } + env.ctx = newEnvironmentContext(ctx, env) return env } -// BasePath is specified on the Environment interface. -func (env *environment) BasePath() string { - return env.basePath +// Context implements the Environment interface. +func (env *environment) Context() context.Context { + return env.ctx +} + +// Basepath implements the Environment interface. +func (env *environment) Basepath() string { + return env.basepath } -// DefaultDomain is specified on the Environment interface. +// DefaultDomain implements the Environment interface. func (env *environment) DefaultDomain() string { return env.defaultDomain } -// DefaultResource is specified on the Environment interface. +// DefaultResource implements the Environment interface. func (env *environment) DefaultResource() string { return env.defaultResource } -// Templates is specified on the Environment interface. -func (env *environment) Templates() TemplatesCache { - return env.templates -} - -//-------------------- -// OPTIONS -//-------------------- - -// BasePath sets the path thats used as prefix before -// domain and resource. -func BasePath(basePath string) Option { - return func(env Environment) { - if basePath == "" { - basePath = "/" - } - if basePath[len(basePath)-1] != '/' { - basePath += "/" - } - envImpl, ok := env.(*environment) - if ok { - envImpl.basePath = basePath - } - } -} - -// DefaultDomain sets the default domain. -func DefaultDomain(defaultDomain string) Option { - return func(env Environment) { - if defaultDomain == "" { - defaultDomain = "default" - } - envImpl, ok := env.(*environment) - if ok { - envImpl.defaultDomain = defaultDomain - } - } -} - -// DefaultResource sets the default resource. -func DefaultResource(defaultResource string) Option { - return func(env Environment) { - if defaultResource == "" { - defaultResource = "default" - } - envImpl, ok := env.(*environment) - if ok { - envImpl.defaultResource = defaultResource - } - } -} - -// Templates sets the templates cache. -func Templates(templates TemplatesCache) Option { - return func(env Environment) { - if templates == nil { - templates = NewTemplatesCache() - } - envImpl, ok := env.(*environment) - if ok { - envImpl.templates = templates - } - } +// TemplatesCache implements the Environment interface. +func (env *environment) TemplatesCache() TemplatesCache { + return env.templatesCache } // EOF diff --git a/rest/job.go b/rest/job.go index 411cd94..e9682a9 100644 --- a/rest/job.go +++ b/rest/job.go @@ -18,6 +18,8 @@ import ( "sort" "strconv" "strings" + + "github.com/tideland/golib/stringex" ) //-------------------- @@ -81,7 +83,7 @@ type Job interface { // job implements the Job interface. type job struct { - environment Environment + environment *environment ctx context.Context request *http.Request responseWriter http.ResponseWriter @@ -91,7 +93,7 @@ type job struct { } // newJob parses the URL and returns the prepared job. -func newJob(env Environment, r *http.Request, rw http.ResponseWriter) Job { +func newJob(env *environment, r *http.Request, rw http.ResponseWriter) Job { // Init the job. j := &job{ environment: env, @@ -99,7 +101,12 @@ func newJob(env Environment, r *http.Request, rw http.ResponseWriter) Job { responseWriter: rw, } // Split path for REST identifiers. - parts := strings.Split(r.URL.Path[len(env.BasePath()):], "/") + parts := stringex.SplitMap(r.URL.Path, "/", func(p string) (string, bool) { + if p == "" { + return "", false + } + return p, true + })[env.basepartsLen:] switch len(parts) { case 3: j.resourceID = parts[2] @@ -109,11 +116,11 @@ func newJob(env Environment, r *http.Request, rw http.ResponseWriter) Job { j.resource = parts[1] j.domain = parts[0] case 1: - j.resource = j.environment.DefaultResource() + j.resource = j.environment.defaultResource j.domain = parts[0] case 0: - j.resource = j.environment.DefaultResource() - j.domain = j.environment.DefaultDomain() + j.resource = j.environment.defaultResource + j.domain = j.environment.defaultDomain default: j.resourceID = strings.Join(parts[2:], "/") j.resource = parts[1] @@ -124,62 +131,60 @@ func newJob(env Environment, r *http.Request, rw http.ResponseWriter) Job { // String is defined on the Stringer interface. func (j *job) String() string { - if j.resourceID == "" { - return fmt.Sprintf("%s /%s/%s", j.request.Method, j.domain, j.resource) - } - return fmt.Sprintf("%s /%s/%s/%s", j.request.Method, j.domain, j.resource, j.resourceID) + path := j.createPath(j.domain, j.resource, j.resourceID) + return fmt.Sprintf("%s %s", j.request.Method, path) } -// Environment is specified on the Job interface. +// Environment implements the Job interface. func (j *job) Environment() Environment { return j.environment } -// Request is specified on the Job interface. +// Request implements the Job interface. func (j *job) Request() *http.Request { return j.request } -// ResponseWriter is specified on the Job interface. +// ResponseWriter implements the Job interface. func (j *job) ResponseWriter() http.ResponseWriter { return j.responseWriter } -// Domain is specified on the Job interface. +// Domain implements the Job interface. func (j *job) Domain() string { return j.domain } -// Resource is specified on the Job interface. +// Resource implements the Job interface. func (j *job) Resource() string { return j.resource } -// ResourceID is specified on the Job interface. +// ResourceID implements the Job interface. func (j *job) ResourceID() string { return j.resourceID } -// Context is specified on the Job interface. +// Context implements the Job interface. func (j *job) Context() context.Context { // Lazy init. if j.ctx == nil { - j.ctx = newContext(j) + j.ctx = newJobContext(j.environment.ctx, j) } return j.ctx } -// AcceptsContentType is specified on the Job interface. +// AcceptsContentType implements the Job interface. func (j *job) AcceptsContentType(contentType string) bool { return strings.Contains(j.request.Header.Get("Accept"), contentType) } -// HasContentType is specified on the Job interface. +// HasContentType implements the Job interface. func (j *job) HasContentType(contentType string) bool { return strings.Contains(j.request.Header.Get("Content-Type"), contentType) } -// Languages is specified on the Job interface. +// Languages implements the Job interface. func (j *job) Languages() Languages { accept := j.request.Header.Get("Accept-Language") languages := Languages{} @@ -201,14 +206,15 @@ func (j *job) Languages() Languages { // createPath creates a path out of the major URL parts. func (j *job) createPath(domain, resource, resourceID string) string { - path := j.environment.BasePath() + domain + "/" + resource + parts := append(j.environment.baseparts, domain, resource) if resourceID != "" { - path = path + "/" + resourceID + parts = append(parts, resourceID) } - return path + path := strings.Join(parts, "/") + return "/" + path } -// InternalPath is specified on the Job interface. +// InternalPath implements the Job interface. func (j *job) InternalPath(domain, resource, resourceID string, query ...KeyValue) string { path := j.createPath(domain, resource, resourceID) if len(query) > 0 { @@ -217,28 +223,28 @@ func (j *job) InternalPath(domain, resource, resourceID string, query ...KeyValu return path } -// Redirect is specified on the Job interface. +// Redirect implements the Job interface. func (j *job) Redirect(domain, resource, resourceID string) { path := j.createPath(domain, resource, resourceID) http.Redirect(j.responseWriter, j.request, path, http.StatusTemporaryRedirect) } -// Renderer is specified on the Job interface. +// Renderer implements the Job interface. func (j *job) Renderer() Renderer { - return &renderer{j.responseWriter, j.environment.Templates()} + return &renderer{j.responseWriter, j.environment.templatesCache} } -// GOB is specified on the Job interface. +// GOB implements the Job interface. func (j *job) GOB() Formatter { return &gobFormatter{j} } -// JSON is specified on the Job interface. +// JSON implements the Job interface. func (j *job) JSON(html bool) Formatter { return &jsonFormatter{j, html} } -// XML is specified on the Job interface. +// XML implements the Job interface. func (j *job) XML() Formatter { return &xmlFormatter{j} } diff --git a/rest/multiplexer.go b/rest/multiplexer.go index 363465e..bfac12a 100644 --- a/rest/multiplexer.go +++ b/rest/multiplexer.go @@ -12,10 +12,12 @@ package rest //-------------------- import ( + "context" "fmt" "net/http" "sync" + "github.com/tideland/golib/etc" "github.com/tideland/golib/logger" "github.com/tideland/golib/monitoring" ) @@ -56,14 +58,26 @@ type Multiplexer interface { // multiplexer implements the Multiplexer interface. type multiplexer struct { mutex sync.RWMutex - environment Environment + environment *environment mapping *mapping } -// NewMultiplexer creates a new HTTP multiplexer. -func NewMultiplexer(options ...Option) Multiplexer { +// NewMultiplexer creates a new HTTP multiplexer. The passed context +// will be used if a handler requests a context from a job, the +// configuration allows to configure the multiplexer. The allowed +// parameters are +// +// {etc +// {basepath /} +// {default-domain default} +// {default-resource default} +// } +// +// The values shown here are the default values if the configuration +// is nil or missing these settings. +func NewMultiplexer(ctx context.Context, cfg etc.Etc) Multiplexer { return &multiplexer{ - environment: newEnvironment(options...), + environment: newEnvironment(ctx, cfg), mapping: newMapping(), } } diff --git a/rest/rest_test.go b/rest/rest_test.go index aaec2de..4061265 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -13,12 +13,14 @@ package rest_test import ( "bytes" + "context" "encoding/gob" "encoding/json" "encoding/xml" "testing" "github.com/tideland/golib/audit" + "github.com/tideland/golib/etc" "github.com/tideland/golib/logger" "github.com/tideland/gorest/rest" @@ -41,7 +43,7 @@ func init() { func TestGetJSON(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "json", NewTestHandler("json", assert)) @@ -49,30 +51,31 @@ func TestGetJSON(t *testing.T) { // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/test/json/4711", + Path: "/base/test/json/4711", Header: restaudit.KeyValues{"Accept": "application/json"}, }) var data TestRequestData err = json.Unmarshal(resp.Body, &data) assert.Nil(err) assert.Equal(data.ResourceID, "4711") + assert.Equal(data.Context, "foo") } // TestPutJSON tests the PUT command with a JSON payload and result. func TestPutJSON(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - reqData := TestRequestData{"foo", "bar", "4711"} + reqData := TestRequestData{"foo", "bar", "4711", ""} reqBuf, _ := json.Marshal(reqData) resp := ts.DoRequest(&restaudit.Request{ Method: "PUT", - Path: "/test/json/4711", + Path: "/base/test/json/4711", Header: restaudit.KeyValues{"Content-Type": "application/json", "Accept": "application/json"}, Body: reqBuf, }) @@ -86,7 +89,7 @@ func TestPutJSON(t *testing.T) { func TestGetXML(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "xml", NewTestHandler("xml", assert)) @@ -94,7 +97,7 @@ func TestGetXML(t *testing.T) { // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/test/xml/4711", + Path: "/base/test/xml/4711", Header: restaudit.KeyValues{"Accept": "application/xml"}, }) assert.Substring("4711", string(resp.Body)) @@ -104,17 +107,17 @@ func TestGetXML(t *testing.T) { func TestPutXML(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - reqData := TestRequestData{"foo", "bar", "4711"} + reqData := TestRequestData{"foo", "bar", "4711", ""} reqBuf, _ := xml.Marshal(reqData) resp := ts.DoRequest(&restaudit.Request{ Method: "PUT", - Path: "/test/xml/4711", + Path: "/base/test/xml/4711", Header: restaudit.KeyValues{"Content-Type": "application/xml", "Accept": "application/xml"}, Body: reqBuf, }) @@ -128,7 +131,7 @@ func TestPutXML(t *testing.T) { func TestPutGOB(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "gob", NewTestHandler("putgob", assert)) @@ -138,10 +141,9 @@ func TestPutGOB(t *testing.T) { reqBuf := new(bytes.Buffer) err = gob.NewEncoder(reqBuf).Encode(reqData) assert.Nil(err, "GOB encode.") - assert.Logf("%q", reqBuf.String()) resp := ts.DoRequest(&restaudit.Request{ Method: "POST", - Path: "/test/gob", + Path: "/base/test/gob", Header: restaudit.KeyValues{"Content-Type": "application/vnd.tideland.gob"}, Body: reqBuf.Bytes(), }) @@ -156,7 +158,7 @@ func TestPutGOB(t *testing.T) { func TestLongPath(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("content", "blog", NewTestHandler("default", assert)) @@ -164,7 +166,7 @@ func TestLongPath(t *testing.T) { // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/content/blog/2014/09/30/just-a-test", + Path: "/base/content/blog/2014/09/30/just-a-test", }) assert.Substring("
  • Resource ID: 2014/09/30/just-a-test
  • ", string(resp.Body)) } @@ -173,15 +175,15 @@ func TestLongPath(t *testing.T) { func TestFallbackDefault(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err := mux.Register("default", "default", NewTestHandler("default", assert)) + err := mux.Register("testing", "index", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/x/y", + Path: "/base/x/y", }) assert.Substring("
  • Resource: y
  • ", string(resp.Body)) } @@ -190,7 +192,7 @@ func TestFallbackDefault(t *testing.T) { func TestHandlerStack(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.RegisterAll(rest.Registrations{ @@ -202,20 +204,20 @@ func TestHandlerStack(t *testing.T) { // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/test/stack", + Path: "/base/test/stack", }) token := resp.Header["Token"] assert.Equal(token, "foo") assert.Substring("
  • Resource: token
  • ", string(resp.Body)) resp = ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/test/stack", + Path: "/base/test/stack", Header: restaudit.KeyValues{"token": "foo"}, }) assert.Substring("
  • Resource: stack
  • ", string(resp.Body)) resp = ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/test/stack", + Path: "/base/test/stack", Header: restaudit.KeyValues{"token": "foo"}, }) assert.Substring("
  • Resource: stack
  • ", string(resp.Body)) @@ -225,7 +227,7 @@ func TestHandlerStack(t *testing.T) { func TestMethodNotSupported(t *testing.T) { assert := audit.NewTestingAssertion(t, true) // Setup the test server. - mux := rest.NewMultiplexer() + mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() err := mux.Register("test", "method", NewTestHandler("method", assert)) @@ -233,7 +235,7 @@ func TestMethodNotSupported(t *testing.T) { // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "OPTION", - Path: "/test/method", + Path: "/base/test/method", }) assert.Substring("OPTION", string(resp.Body)) } @@ -276,6 +278,7 @@ type TestRequestData struct { Domain string Resource string ResourceID string + Context string } type TestCounterData struct { @@ -296,6 +299,7 @@ const testTemplateHTML = `
  • Domain: {{.Domain}}
  • Resource: {{.Resource}}
  • Resource ID: {{.ResourceID}}
  • +
  • Context: {{.Context}}
  • @@ -315,7 +319,7 @@ func (th *TestHandler) ID() string { } func (th *TestHandler) Init(env rest.Environment, domain, resource string) error { - env.Templates().Parse("test:context:html", testTemplateHTML, "text/html") + env.TemplatesCache().Parse("test:context:html", testTemplateHTML, "text/html") return nil } @@ -323,7 +327,8 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { if th.id == "auth:token" { job.ResponseWriter().Header().Add("Token", "foo") } - data := TestRequestData{job.Domain(), job.Resource(), job.ResourceID()} + ctxTest := job.Context().Value("test") + data := TestRequestData{job.Domain(), job.Resource(), job.ResourceID(), ctxTest.(string)} switch { case job.AcceptsContentType(rest.ContentTypeXML): th.assert.Logf("GET XML") @@ -379,4 +384,18 @@ func (th *TestHandler) Delete(job rest.Job) (bool, error) { return false, nil } +//-------------------- +// HELPERS +//-------------------- + +// newMultiplexer creates a new multiplexer with a testing context +// and a testing configuration. +func newMultiplexer(assert audit.Assertion) rest.Multiplexer { + ctx := context.WithValue(context.Background(), "test", "foo") + cfgStr := "{etc {basepath /base/}{default-domain testing}{default-resource index}}" + cfg, err := etc.ReadString(cfgStr) + assert.Nil(err) + return rest.NewMultiplexer(ctx, cfg) +} + // EOF diff --git a/rest/templates.go b/rest/templates.go index d9b949b..4f39435 100644 --- a/rest/templates.go +++ b/rest/templates.go @@ -80,8 +80,8 @@ type templatesCache struct { items map[string]*templatesCacheItem } -// NewTemplates creates a new template cache. -func NewTemplatesCache() TemplatesCache { +// newTemplatesCache creates a new template cache. +func newTemplatesCache() *templatesCache { return &templatesCache{ items: make(map[string]*templatesCacheItem), } diff --git a/restaudit/doc.go b/restaudit/doc.go index e25c9a1..ee6f5db 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 1, 1) + return version.New(2, 2, 0) } // EOF From 3f1f64f838a30accef98a53e23ea812515fd5e03 Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 5 Oct 2016 10:30:16 +0200 Subject: [PATCH 043/127] Changed Formatter.Write() to also write the status code --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/doc.go | 2 +- handlers/jwtauth.go | 7 +++---- jwt/doc.go | 2 +- jwt/header_test.go | 4 ++-- rest/doc.go | 2 +- rest/formatter.go | 33 +++++++++++++++++++++++---------- rest/rest_test.go | 16 ++++++++-------- restaudit/doc.go | 2 +- 10 files changed, 45 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e0f24..37c5922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-10-05 + +- *Formatter.Write()* now also writes the status code + ## 2016-10-04 - Improved passing external contexts into an environment, e.g. diff --git a/README.md b/README.md index bdb8822..f3ae324 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.2.0 +Version 2.3.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 040f1d5..f7f94eb 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 2, 0) + return version.New(2, 3, 0) } // EOF diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 3a00057..ec4a641 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -12,7 +12,6 @@ package handlers //-------------------- import ( - "net/http" "time" "github.com/tideland/gorest/jwt" @@ -147,13 +146,13 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { // deny sends a negative feedback to the caller. func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { - job.ResponseWriter().WriteHeader(http.StatusUnauthorized) switch { case job.AcceptsContentType(rest.ContentTypeJSON): - return rest.NegativeFeedback(job.JSON(true), msg) + return rest.NegativeFeedback(job.JSON(true), rest.StatusUnauthorized, msg) case job.AcceptsContentType(rest.ContentTypeXML): - return rest.NegativeFeedback(job.XML(), msg) + return rest.NegativeFeedback(job.XML(), rest.StatusUnauthorized, msg) default: + job.ResponseWriter().WriteHeader(rest.StatusUnauthorized) job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypePlain) job.ResponseWriter().Write([]byte(msg)) return nil diff --git a/jwt/doc.go b/jwt/doc.go index 29c8c61..f353503 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 2, 0) + return version.New(2, 3, 0) } // EOF diff --git a/jwt/header_test.go b/jwt/header_test.go index c592a5c..07be705 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -223,7 +223,7 @@ func (th *testHandler) testDecode(job rest.Job) (bool, error) { subject, ok := jwtOut.Claims().Subject() th.assert.True(ok) th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(jwtOut.Claims()) + job.JSON(true).Write(rest.StatusOK, jwtOut.Claims()) return true, nil } @@ -240,7 +240,7 @@ func (th *testHandler) testVerify(job rest.Job) (bool, error) { subject, ok := jwtOut.Claims().Subject() th.assert.True(ok) th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(jwtOut.Claims()) + job.JSON(true).Write(rest.StatusOK, jwtOut.Claims()) return true, nil } diff --git a/rest/doc.go b/rest/doc.go index dcd0e79..699382b 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 2, 0) + return version.New(2, 3, 0) } // EOF diff --git a/rest/formatter.go b/rest/formatter.go index 6319b67..19c91e1 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -28,6 +28,16 @@ import ( //-------------------- const ( + // Standard REST status codes. + StatusOK = http.StatusOK + StatusCreated = http.StatusCreated + StatusNoContent = http.StatusNoContent + StatusBadRequest = http.StatusBadRequest + StatusUnauthorized = http.StatusUnauthorized + StatusNotFound = http.StatusNotFound + StatusConflict = http.StatusConflict + + // Standard REST content types. ContentTypePlain = "text/plain" ContentTypeHTML = "text/html" ContentTypeXML = "application/xml" @@ -54,8 +64,8 @@ type Envelope struct { type Formatter interface { // Write encodes the passed data to implementers format and writes - // it to the response writer. - Write(data interface{}) error + // it with the passed status code to the response writer. + Write(status int, data interface{}) error // Read checks if the request content type matches the implementers // format, reads its body and decodes it to the value pointed to by @@ -65,14 +75,14 @@ type Formatter interface { // PositiveFeedback writes a positive feedback envelope to the formatter. func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) error { - amsg := fmt.Sprintf(msg, args...) - return f.Write(&Envelope{true, amsg, payload}) + fmsg := fmt.Sprintf(msg, args...) + return f.Write(StatusOK, &Envelope{true, fmsg, payload}) } // NegativeFeedback writes a negative feedback envelope to the formatter. -func NegativeFeedback(f Formatter, msg string, args ...interface{}) error { - amsg := fmt.Sprintf(msg, args...) - return f.Write(&Envelope{false, amsg, nil}) +func NegativeFeedback(f Formatter, status int, msg string, args ...interface{}) error { + fmsg := fmt.Sprintf(msg, args...) + return f.Write(status, &Envelope{false, fmsg, nil}) } //-------------------- @@ -85,8 +95,9 @@ type gobFormatter struct { } // Write is specified on the Formatter interface. -func (gf *gobFormatter) Write(data interface{}) error { +func (gf *gobFormatter) Write(status int, data interface{}) error { enc := gob.NewEncoder(gf.job.ResponseWriter()) + gf.job.ResponseWriter().WriteHeader(status) gf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeGOB) err := enc.Encode(data) if err != nil { @@ -118,7 +129,7 @@ type jsonFormatter struct { } // Write is specified on the Formatter interface. -func (jf *jsonFormatter) Write(data interface{}) error { +func (jf *jsonFormatter) Write(status int, data interface{}) error { body, err := json.Marshal(data) if err != nil { http.Error(jf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) @@ -129,6 +140,7 @@ func (jf *jsonFormatter) Write(data interface{}) error { json.HTMLEscape(&buf, body) body = buf.Bytes() } + jf.job.ResponseWriter().WriteHeader(status) jf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeJSON) _, err = jf.job.ResponseWriter().Write(body) return err @@ -157,12 +169,13 @@ type xmlFormatter struct { } // Write is specified on the Formatter interface. -func (xf *xmlFormatter) Write(data interface{}) error { +func (xf *xmlFormatter) Write(status int, data interface{}) error { body, err := xml.Marshal(data) if err != nil { http.Error(xf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) return err } + xf.job.ResponseWriter().WriteHeader(status) xf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeXML) _, err = xf.job.ResponseWriter().Write(body) return err diff --git a/rest/rest_test.go b/rest/rest_test.go index 4061265..f290a76 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -332,10 +332,10 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { switch { case job.AcceptsContentType(rest.ContentTypeXML): th.assert.Logf("GET XML") - job.XML().Write(data) + job.XML().Write(rest.StatusOK, data) case job.AcceptsContentType(rest.ContentTypeJSON): th.assert.Logf("GET JSON") - job.JSON(true).Write(data) + job.JSON(true).Write(rest.StatusOK, data) default: th.assert.Logf("GET HTML") job.Renderer().Render("test:context:html", data) @@ -353,16 +353,16 @@ func (th *TestHandler) Put(job rest.Job) (bool, error) { case job.HasContentType(rest.ContentTypeJSON): err := job.JSON(true).Read(&data) if err != nil { - job.JSON(true).Write(TestErrorData{err.Error()}) + job.JSON(true).Write(rest.StatusBadRequest, TestErrorData{err.Error()}) } else { - job.JSON(true).Write(data) + job.JSON(true).Write(rest.StatusOK, data) } case job.HasContentType(rest.ContentTypeXML): err := job.XML().Read(&data) if err != nil { - job.XML().Write(TestErrorData{err.Error()}) + job.XML().Write(rest.StatusBadRequest, TestErrorData{err.Error()}) } else { - job.XML().Write(data) + job.XML().Write(rest.StatusOK, data) } } @@ -373,9 +373,9 @@ func (th *TestHandler) Post(job rest.Job) (bool, error) { var data TestCounterData err := job.GOB().Read(&data) if err != nil { - job.GOB().Write(err) + job.GOB().Write(rest.StatusBadRequest, err) } else { - job.GOB().Write(data) + job.GOB().Write(rest.StatusOK, data) } return true, nil } diff --git a/restaudit/doc.go b/restaudit/doc.go index ee6f5db..b4cb43b 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 2, 0) + return version.New(2, 3, 0) } // EOF From e17afea13d9223672c029f5556016714d8057304 Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 5 Oct 2016 14:50:24 +0200 Subject: [PATCH 044/127] Added a little configuration option to ignore favicon.ico --- README.md | 2 +- handlers/doc.go | 2 +- jwt/doc.go | 2 +- rest/doc.go | 2 +- rest/mapping.go | 16 +++++++++++++--- rest/multiplexer.go | 3 ++- restaudit/doc.go | 2 +- 7 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f3ae324..810f494 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.3.0 +Version 2.3.1 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index f7f94eb..b728f74 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 0) + return version.New(2, 3, 1) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index f353503..0d3ff00 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 0) + return version.New(2, 3, 1) } // EOF diff --git a/rest/doc.go b/rest/doc.go index 699382b..0213206 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 0) + return version.New(2, 3, 1) } // EOF diff --git a/rest/mapping.go b/rest/mapping.go index 7904dd4..a2bff39 100644 --- a/rest/mapping.go +++ b/rest/mapping.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/tideland/golib/errors" + "github.com/tideland/golib/etc" "github.com/tideland/golib/logger" ) @@ -96,13 +97,15 @@ func (hl *handlerList) handle(job Job) error { // mapping maps domains and resources to lists of // resource handlers. type mapping struct { - handlers map[string]*handlerList + ignoreFavicon bool + handlers map[string]*handlerList } // newMapping returns a new handler mapping. -func newMapping() *mapping { +func newMapping(cfg etc.Etc) *mapping { return &mapping{ - handlers: make(map[string]*handlerList), + ignoreFavicon: cfg.ValueAsBool("ignore-favicon", true), + handlers: make(map[string]*handlerList), } } @@ -132,6 +135,13 @@ func (m *mapping) deregister(domain, resource string, id string) { // handle handles a request. func (m *mapping) handle(job Job) error { + // Check for favicon.ico. + if m.ignoreFavicon { + if job.Domain() == "favicon.ico" { + job.ResponseWriter().WriteHeader(StatusNoContent) + return nil + } + } // Find handler list. hl, err := m.handlerList(job) if err != nil { diff --git a/rest/multiplexer.go b/rest/multiplexer.go index bfac12a..8130fad 100644 --- a/rest/multiplexer.go +++ b/rest/multiplexer.go @@ -71,6 +71,7 @@ type multiplexer struct { // {basepath /} // {default-domain default} // {default-resource default} +// {ignore-favicon true} // } // // The values shown here are the default values if the configuration @@ -78,7 +79,7 @@ type multiplexer struct { func NewMultiplexer(ctx context.Context, cfg etc.Etc) Multiplexer { return &multiplexer{ environment: newEnvironment(ctx, cfg), - mapping: newMapping(), + mapping: newMapping(cfg), } } diff --git a/restaudit/doc.go b/restaudit/doc.go index b4cb43b..20775df 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 0) + return version.New(2, 3, 1) } // EOF From 87001c1fef7b0196b06292780f24d102ce3f8ba6 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 8 Oct 2016 22:35:42 +0200 Subject: [PATCH 045/127] Improved context handling Handlers now can enhance the context, first usage is show in the JWTAuthorizationHandler. It stores a successfully checked token in the context. --- CHANGELOG.md | 6 ++++++ README.md | 2 +- handlers/doc.go | 2 +- handlers/jwtauth.go | 23 ++++++++++++++++++++++- jwt/doc.go | 2 +- rest/doc.go | 2 +- rest/job.go | 17 ++++++++++++++++- rest/rest_test.go | 7 +++++++ restaudit/doc.go | 2 +- 9 files changed, 56 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c5922..8193519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Tideland Go REST Server Library +## 2016-10-08 + +- *Job* allows now to enhance its context for following handlers +- *JWTAuthorizationHandler* stores a successfully checked token + in the job context + ## 2016-10-05 - *Formatter.Write()* now also writes the status code diff --git a/README.md b/README.md index 810f494..5f7314d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.3.1 +Version 2.4.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index b728f74..7d7536c 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 1) + return version.New(2, 4, 0) } // EOF diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index ec4a641..e46dfdd 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -12,6 +12,7 @@ package handlers //-------------------- import ( + "context" "time" "github.com/tideland/gorest/jwt" @@ -22,6 +23,12 @@ import ( // JWT AUTHORIZATION HANDLER //-------------------- +// key for the storage of values in a context. +type key int + +// jwtKey for the storrage of a JWT. +var jwtKey key = 0 + // JWTAuthorizationConfig allows to control how the JWT authorization // handler works. All values are optional. In this case tokens are only // decoded without using a cache, validated for the time, and there's @@ -34,7 +41,8 @@ type JWTAuthorizationConfig struct { } // jwtAuthorizationHandler checks for a valid token and then runs -// a gatekeeper function. +// a gatekeeper function. If everythinh is fine the token is stored +// in the job context for the following handlers. type jwtAuthorizationHandler struct { id string cache jwt.Cache @@ -126,6 +134,7 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { default: jobJWT, err = jwt.DecodeFromJob(job) } + // Now do the checks. if err != nil { return false, h.deny(job, err.Error()) } @@ -141,6 +150,10 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { return false, h.deny(job, "gatekeeper denied:"+err.Error()) } } + // All fine, store token in context. + job.EnhanceContext(func(ctx context.Context) context.Context { + return context.WithValue(ctx, jwtKey, jobJWT) + }) return true, nil } @@ -159,4 +172,12 @@ func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { } } +// JWTFromJob retrieves a JWT out of the context of a job, when a +// JWTAuthorizationHandler earlier in the queue of handlers successfully +// received and checked one. +func JWTFormJob(job rest.Job) (jwt.JWT, bool) { + jobJWT, ok := context.Value(job.Context(), jwtKey).(jwt.JWT) + return jobJWT, ok +} + // EOF diff --git a/jwt/doc.go b/jwt/doc.go index 0d3ff00..4cf4fe0 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 1) + return version.New(2, 4, 0) } // EOF diff --git a/rest/doc.go b/rest/doc.go index 0213206..e544251 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 1) + return version.New(2, 4, 0) } // EOF diff --git a/rest/job.go b/rest/job.go index e9682a9..2e4daf7 100644 --- a/rest/job.go +++ b/rest/job.go @@ -50,9 +50,18 @@ type Job interface { // ResourceID return the requests resource ID. ResourceID() string - // Context returns a context containing the job. + // Context returns a job context also containing the + // job itself. Context() context.Context + // EnhanceContext allows to enhance the job context + // values, a deadline, a timeout, or a cancel. So + // e.g. a first handler in a handler queue can + // store authentication information in the context + // and a following handler can use it (see the + // JWTAuthorizationHandler). + EnhanceContext(func(ctx context.Context) context.Context) + // AcceptsContentType checks if the requestor accepts a given content type. AcceptsContentType(contentType string) bool @@ -174,6 +183,12 @@ func (j *job) Context() context.Context { return j.ctx } +// EnhanceContext implements the Job interface. +func (j *job) EnhanceContext(f func(ctx context.Context) context.Context) { + ctx := j.Context() + j.ctx = f(ctx) +} + // AcceptsContentType implements the Job interface. func (j *job) AcceptsContentType(contentType string) bool { return strings.Contains(j.request.Header.Get("Accept"), contentType) diff --git a/rest/rest_test.go b/rest/rest_test.go index f290a76..39383fc 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -267,6 +267,9 @@ func (ah *AuthHandler) Get(job rest.Job) (bool, error) { job.Redirect("authentication", "token", "") return false, nil } + job.ExtentContext(func(ctx context.Context) context.Context { + return context.WithValue(ctx, "Token", "foo") + }) return true, nil } @@ -327,6 +330,10 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { if th.id == "auth:token" { job.ResponseWriter().Header().Add("Token", "foo") } + if th.id == "stack:test" { + ctxToken := job.Context().Value("Token") + th.assert.Equal(ctxToken, "foo") + } ctxTest := job.Context().Value("test") data := TestRequestData{job.Domain(), job.Resource(), job.ResourceID(), ctxTest.(string)} switch { diff --git a/restaudit/doc.go b/restaudit/doc.go index 20775df..a053cbb 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 3, 1) + return version.New(2, 4, 0) } // EOF From d044b5254b3d998b8462fd0cb38453e6a25ce868 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 8 Oct 2016 20:47:14 +0000 Subject: [PATCH 046/127] Fixed errors after context changes --- handlers/jwtauth.go | 2 +- rest/rest_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index e46dfdd..a8657db 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -176,7 +176,7 @@ func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { // JWTAuthorizationHandler earlier in the queue of handlers successfully // received and checked one. func JWTFormJob(job rest.Job) (jwt.JWT, bool) { - jobJWT, ok := context.Value(job.Context(), jwtKey).(jwt.JWT) + jobJWT, ok := job.Context().Value(jwtKey).(jwt.JWT) return jobJWT, ok } diff --git a/rest/rest_test.go b/rest/rest_test.go index 39383fc..69d3b9d 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -267,7 +267,7 @@ func (ah *AuthHandler) Get(job rest.Job) (bool, error) { job.Redirect("authentication", "token", "") return false, nil } - job.ExtentContext(func(ctx context.Context) context.Context { + job.EnhanceContext(func(ctx context.Context) context.Context { return context.WithValue(ctx, "Token", "foo") }) return true, nil From 184f4afef363ba2ac518008e017745731f299c43 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 9 Oct 2016 11:04:41 +0200 Subject: [PATCH 047/127] Added AuditHandler for better testing Also fixed typo when retrieving a JWT from a job. --- README.md | 2 +- handlers/audit.go | 87 +++++++++++++++++++++++++++++++++++++++ handlers/doc.go | 2 +- handlers/handlers_test.go | 13 ++++++ handlers/jwtauth.go | 2 +- 5 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 handlers/audit.go diff --git a/README.md b/README.md index 5f7314d..6555f44 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.4.0 +Version 2.4.1 ## Packages diff --git a/handlers/audit.go b/handlers/audit.go new file mode 100644 index 0000000..d2c527a --- /dev/null +++ b/handlers/audit.go @@ -0,0 +1,87 @@ +// Tideland Go REST Server Library - Handlers - Audit Handler +// +// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package handlers + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "github.com/tideland/golib/audit" + + "github.com/tideland/gorest/rest" +) + +//-------------------- +// AUDIT HANDLER +//-------------------- + +// AuditHandlerFunc defines the function which will be executed +// for each request. The assert can be used for tests. +func AuditHandlerFunc func(assert audit.Assertion, job rest.Job) (bool, error) + +// auditHandler helps testing other handlers. +type auditHandler struct { + id string + assert audit.Assertion + handle AuditHandlerFunc +} + +// NewAuditHandler creates a handler able to handle all types of +// requests with the passed AuditHandlerFunc. Here the tests can +// be done. +func NewAuditHandler(id string, assert audit.Assertion, ahf AuditHandlerFunc) rest.ResourceHandler { + return &auditHandler{id, assert, ahf} +} + +// ID is specified on the ResourceHandler interface. +func (h *auditHandler) ID() string { + return h.id +} + +// Init is specified on the ResourceHandler interface. +func (h *auditHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +// Get is specified on the GetResourceHandler interface. +func (h *auditHandler) Get(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// Head is specified on the HeadResourceHandler interface. +func (h *auditHandler) Head(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// Put is specified on the PutResourceHandler interface. +func (h *auditHandler) Put(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// Post is specified on the PostResourceHandler interface. +func (h *auditHandler) Post(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// Patch is specified on the PatchResourceHandler interface. +func (h *auditHandler) Patch(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// Delete is specified on the DeleteResourceHandler interface. +func (h *auditHandler) Delete(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// Options is specified on the OptionsResourceHandler interface. +func (h *auditHandler) Options(job rest.Job) (bool, error) { + return h.handle(h.assert, job) +} + +// EOF diff --git a/handlers/doc.go b/handlers/doc.go index 7d7536c..935ea87 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // PackageVersion returns the version of the version package. func PackageVersion() version.Version { - return version.New(2, 4, 0) + return version.New(2, 4, 1) } // EOF diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 3554028..ff38c10 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -128,6 +128,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { runs int status int body string + auditf handlers.AuditHandlerFunc }{ { id: "no-token", @@ -142,6 +143,14 @@ func TestJWTAuthorizationHandler(t *testing.T) { return out }, status: 200, + auditf: func(assert audit.Assertion, job rest.Job) (bool, error) { + auditJWT, ok := handler.JWTFromJob(job) + assert.True(ok) + assert.NotNil(auditJWT) + subject, ok := auditJWT.Claims().Subject() + assert.True(ok) + assert.Equal(subject, "test") + }, }, { id: "token-verify-no-gatekeeper", tokener: func() jwt.JWT { @@ -233,6 +242,10 @@ func TestJWTAuthorizationHandler(t *testing.T) { assert.Logf("JWT test #%d: %s", i, test.id) err := mux.Register("jwt", test.id, handlers.NewJWTAuthorizationHandler(test.id, test.config)) assert.Nil(err) + if test.auditf != nil { + err := mux.Register("jwt", test.id, handlers.NewAuditHandler("audit", assert, test.auditf) + assert.Nil(err) + } var requestProcessor func(req *http.Request) *http.Request if test.tokener != nil { requestProcessor = func(req *http.Request) *http.Request { diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index a8657db..2d2ad3d 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -175,7 +175,7 @@ func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { // JWTFromJob retrieves a JWT out of the context of a job, when a // JWTAuthorizationHandler earlier in the queue of handlers successfully // received and checked one. -func JWTFormJob(job rest.Job) (jwt.JWT, bool) { +func JWTFromJob(job rest.Job) (jwt.JWT, bool) { jobJWT, ok := job.Context().Value(jwtKey).(jwt.JWT) return jobJWT, ok } From 355a5a611e3f54f3a233b2525215e1a3c753b0aa Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 9 Oct 2016 09:10:01 +0000 Subject: [PATCH 048/127] Fixed handler tests --- handlers/audit.go | 4 ++-- handlers/handlers_test.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/handlers/audit.go b/handlers/audit.go index d2c527a..fd49bf9 100644 --- a/handlers/audit.go +++ b/handlers/audit.go @@ -13,7 +13,7 @@ package handlers import ( "github.com/tideland/golib/audit" - + "github.com/tideland/gorest/rest" ) @@ -23,7 +23,7 @@ import ( // AuditHandlerFunc defines the function which will be executed // for each request. The assert can be used for tests. -func AuditHandlerFunc func(assert audit.Assertion, job rest.Job) (bool, error) +type AuditHandlerFunc func(assert audit.Assertion, job rest.Job) (bool, error) // auditHandler helps testing other handlers. type auditHandler struct { diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index ff38c10..fc164db 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -144,12 +144,13 @@ func TestJWTAuthorizationHandler(t *testing.T) { }, status: 200, auditf: func(assert audit.Assertion, job rest.Job) (bool, error) { - auditJWT, ok := handler.JWTFromJob(job) + auditJWT, ok := handlers.JWTFromJob(job) assert.True(ok) assert.NotNil(auditJWT) subject, ok := auditJWT.Claims().Subject() assert.True(ok) assert.Equal(subject, "test") + return true, nil }, }, { id: "token-verify-no-gatekeeper", @@ -243,7 +244,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { err := mux.Register("jwt", test.id, handlers.NewJWTAuthorizationHandler(test.id, test.config)) assert.Nil(err) if test.auditf != nil { - err := mux.Register("jwt", test.id, handlers.NewAuditHandler("audit", assert, test.auditf) + err := mux.Register("jwt", test.id, handlers.NewAuditHandler("audit", assert, test.auditf)) assert.Nil(err) } var requestProcessor func(req *http.Request) *http.Request From b6ed782bfe591694643f485ce0f6485f1ee73f63 Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 10 Oct 2016 17:31:11 +0200 Subject: [PATCH 049/127] Change naming scheme of version function Will be changed for the other packages in other projects step by step too. --- handlers/doc.go | 6 +++--- jwt/doc.go | 6 +++--- rest/doc.go | 6 +++--- restaudit/doc.go | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/handlers/doc.go b/handlers/doc.go index 935ea87..fd9014e 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -21,9 +21,9 @@ import ( // VERSION //-------------------- -// PackageVersion returns the version of the version package. -func PackageVersion() version.Version { - return version.New(2, 4, 1) +// Version returns the version of the handlers package. +func Version() version.Version { + return version.New(2, 4, 2) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index 4cf4fe0..a36174a 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -21,9 +21,9 @@ import ( // VERSION //-------------------- -// PackageVersion returns the version of the version package. -func PackageVersion() version.Version { - return version.New(2, 4, 0) +// Version returns the version of the JSON Web Token package. +func Version() version.Version { + return version.New(2, 4, 2) } // EOF diff --git a/rest/doc.go b/rest/doc.go index e544251..e2466ac 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -99,9 +99,9 @@ import ( // VERSION //-------------------- -// PackageVersion returns the version of the version package. -func PackageVersion() version.Version { - return version.New(2, 4, 0) +// Version returns the version of the REST package. +func Version() version.Version { + return version.New(2, 4, 2) } // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index a053cbb..0817aad 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -22,9 +22,9 @@ import ( // VERSION //-------------------- -// PackageVersion returns the version of the version package. -func PackageVersion() version.Version { - return version.New(2, 4, 0) +// Version returns the version of the REST Audit package. +func Version() version.Version { + return version.New(2, 4, 2) } // EOF From 1dcf877fb38a35f027dff4d39e2cdb38fcdf01ba Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 10 Oct 2016 17:34:43 +0200 Subject: [PATCH 050/127] Fixed missing new version number --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6555f44..0b18cf3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.4.1 +Version 2.4.2 ## Packages From edb9028f1ab3cdf69eccb116d91006e0b27bc401 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 18 Oct 2016 17:00:57 +0200 Subject: [PATCH 051/127] Added more convenient access to query values --- CHANGELOG.md | 5 +++ README.md | 2 +- handlers/doc.go | 2 +- jwt/doc.go | 2 +- rest/doc.go | 2 +- rest/errors.go | 2 + rest/formatter.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ rest/job.go | 8 ++++ rest/rest_test.go | 12 ++++-- restaudit/doc.go | 2 +- 10 files changed, 126 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8193519..4d3f6ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Tideland Go REST Server Library +## 2016-10-18 + +- Added *Query* type and method for more concenient access to + query values + ## 2016-10-08 - *Job* allows now to enhance its context for following handlers diff --git a/README.md b/README.md index 0b18cf3..ea89563 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.4.2 +Version 2.5.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index fd9014e..e471cc4 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the handlers package. func Version() version.Version { - return version.New(2, 4, 2) + return version.New(2, 5, 0) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index a36174a..39c6fcd 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the JSON Web Token package. func Version() version.Version { - return version.New(2, 4, 2) + return version.New(2, 5, 0) } // EOF diff --git a/rest/doc.go b/rest/doc.go index e2466ac..e993219 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 4, 2) + return version.New(2, 5, 0) } // EOF diff --git a/rest/errors.go b/rest/errors.go index 6318566..1973471 100644 --- a/rest/errors.go +++ b/rest/errors.go @@ -35,6 +35,7 @@ const ( ErrUploadingFile ErrInvalidContentType ErrNoCachedTemplate + ErrQueryValueNotFound ) var errorMessages = errors.Messages{ @@ -53,6 +54,7 @@ var errorMessages = errors.Messages{ ErrUploadingFile: "uploaded file cannot be handled by %q", ErrInvalidContentType: "content type is not %q", ErrNoCachedTemplate: "template %q is not cached", + ErrQueryValueNotFound: "query value not found", } // EOF diff --git a/rest/formatter.go b/rest/formatter.go index 19c91e1..0fa3391 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -19,8 +19,11 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" + "time" "github.com/tideland/golib/errors" + "github.com/tideland/golib/stringex" ) //-------------------- @@ -45,6 +48,14 @@ const ( ContentTypeGOB = "application/vnd.tideland.gob" ) +//-------------------- +// GLOBAL +//-------------------- + +var ( + defaulter = stringex.NewDefaulter("job", false) +) + //-------------------- // ENVELOPE //-------------------- @@ -194,4 +205,91 @@ func (xf *xmlFormatter) Read(data interface{}) error { return xml.Unmarshal(body, &data) } +//-------------------- +// QUERY +//-------------------- + +// Query allows typed access with default values to a jobs +// request values passed as query. +type Query interface { + // ValueAsString retrieves the string value of a given key. If it + // doesn't exist the default value dv is returned. + ValueAsString(key, dv string) string + + // ValueAsBool retrieves the bool value of a given key. If it + // doesn't exist the default value dv is returned. + ValueAsBool(key string, dv bool) bool + + // ValueAsInt retrieves the int value of a given key. If it + // doesn't exist the default value dv is returned. + ValueAsInt(key string, dv int) int + + // ValueAsFloat64 retrieves the float64 value of a given key. If it + // doesn't exist the default value dv is returned. + ValueAsFloat64(key string, dv float64) float64 + + // ValueAsTime retrieves the string value of a given key and + // interprets it as time with the passed format. If it + // doesn't exist the default value dv is returned. + ValueAsTime(key, layout string, dv time.Time) time.Time + + // ValueAsDuration retrieves the duration value of a given key. + // If it doesn't exist the default value dv is returned. + ValueAsDuration(key string, dv time.Duration) time.Duration +} + +// query implements Query. +type query struct { + values url.Values +} + +// ValueAsString implements the Query interface. +func (q *query) ValueAsString(key, dv string) string { + value := queryValuer(q.values.Get(key)) + return defaulter.AsString(value, dv) +} + +// ValueAsBool implements the Query interface. +func (q *query) ValueAsBool(key string, dv bool) bool { + value := queryValuer(q.values.Get(key)) + return defaulter.AsBool(value, dv) +} + +// ValueAsInt implements the Query interface. +func (q *query) ValueAsInt(key string, dv int) int { + value := queryValuer(q.values.Get(key)) + return defaulter.AsInt(value, dv) +} + +// ValueAsFloat64 implements the Query interface. +func (q *query) ValueAsFloat64(key string, dv float64) float64 { + value := queryValuer(q.values.Get(key)) + return defaulter.AsFloat64(value, dv) +} + +// ValueAsTime implements the Query interface. +func (q *query) ValueAsTime(key, format string, dv time.Time) time.Time { + value := queryValuer(q.values.Get(key)) + return defaulter.AsTime(value, format, dv) +} + +// ValueAsDuration implements the Query interface. +func (q *query) ValueAsDuration(key string, dv time.Duration) time.Duration { + value := queryValuer(q.values.Get(key)) + return defaulter.AsDuration(value, dv) +} + +// queryValues implements the stringex.Valuer interface for +// the usage inside of query. +type queryValuer string + +// Value implements the Valuer interface. +func (qv queryValuer) Value() (string, error) { + v := string(qv) + if len(v) == 0 { + return "", errors.New(ErrQueryValueNotFound, errorMessages) + } + return v, nil +} + // EOF diff --git a/rest/job.go b/rest/job.go index 2e4daf7..6af4821 100644 --- a/rest/job.go +++ b/rest/job.go @@ -88,6 +88,9 @@ type Job interface { // XML returns a XML formatter. XML() Formatter + + // Query returns a convenient access to query values. + Query() Query } // job implements the Job interface. @@ -264,4 +267,9 @@ func (j *job) XML() Formatter { return &xmlFormatter{j} } +// Query implements the Job interface. +func (j *job) Query() Query { + return &query{j.request.URL.Query()} +} + // EOF diff --git a/rest/rest_test.go b/rest/rest_test.go index 69d3b9d..293b6b3 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -51,13 +51,14 @@ func TestGetJSON(t *testing.T) { // Perform test requests. resp := ts.DoRequest(&restaudit.Request{ Method: "GET", - Path: "/base/test/json/4711", + Path: "/base/test/json/4711?foo=0815", Header: restaudit.KeyValues{"Accept": "application/json"}, }) var data TestRequestData err = json.Unmarshal(resp.Body, &data) assert.Nil(err) assert.Equal(data.ResourceID, "4711") + assert.Equal(data.Query, "0815") assert.Equal(data.Context, "foo") } @@ -71,7 +72,7 @@ func TestPutJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - reqData := TestRequestData{"foo", "bar", "4711", ""} + reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} reqBuf, _ := json.Marshal(reqData) resp := ts.DoRequest(&restaudit.Request{ Method: "PUT", @@ -113,7 +114,7 @@ func TestPutXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - reqData := TestRequestData{"foo", "bar", "4711", ""} + reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} reqBuf, _ := xml.Marshal(reqData) resp := ts.DoRequest(&restaudit.Request{ Method: "PUT", @@ -281,6 +282,7 @@ type TestRequestData struct { Domain string Resource string ResourceID string + Query string Context string } @@ -302,6 +304,7 @@ const testTemplateHTML = `
  • Domain: {{.Domain}}
  • Resource: {{.Resource}}
  • Resource ID: {{.ResourceID}}
  • +
  • Query {{.Query}}
  • Context: {{.Context}}
  • @@ -335,7 +338,8 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { th.assert.Equal(ctxToken, "foo") } ctxTest := job.Context().Value("test") - data := TestRequestData{job.Domain(), job.Resource(), job.ResourceID(), ctxTest.(string)} + query := job.Query().ValueAsString("foo", "bar") + data := TestRequestData{job.Domain(), job.Resource(), job.ResourceID(), query, ctxTest.(string)} switch { case job.AcceptsContentType(rest.ContentTypeXML): th.assert.Logf("GET XML") diff --git a/restaudit/doc.go b/restaudit/doc.go index 0817aad..e42e0b1 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // Version returns the version of the REST Audit package. func Version() version.Version { - return version.New(2, 4, 2) + return version.New(2, 5, 0) } // EOF From 19fab2b0012cc4c4d510f81ee06e2831d6dc4e6f Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 24 Oct 2016 14:06:42 +0200 Subject: [PATCH 052/127] Fixed feedback marshalling bug --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/doc.go | 2 +- handlers/jwtauth.go | 2 +- jwt/doc.go | 2 +- rest/doc.go | 2 +- rest/formatter.go | 14 +++++++------- restaudit/doc.go | 2 +- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3f6ae..d261905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-10-24 + +- Fixed marshalling bug of positive or negative feedback + ## 2016-10-18 - Added *Query* type and method for more concenient access to diff --git a/README.md b/README.md index ea89563..55c4d8a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.5.0 +Version 2.5.1 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index e471cc4..b521b45 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the handlers package. func Version() version.Version { - return version.New(2, 5, 0) + return version.New(2, 5, 1) } // EOF diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 2d2ad3d..e6472ce 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -161,7 +161,7 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { switch { case job.AcceptsContentType(rest.ContentTypeJSON): - return rest.NegativeFeedback(job.JSON(true), rest.StatusUnauthorized, msg) + return rest.NegativeFeedback(job.JSON(false), rest.StatusUnauthorized, msg) case job.AcceptsContentType(rest.ContentTypeXML): return rest.NegativeFeedback(job.XML(), rest.StatusUnauthorized, msg) default: diff --git a/jwt/doc.go b/jwt/doc.go index 39c6fcd..b2d1317 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the JSON Web Token package. func Version() version.Version { - return version.New(2, 5, 0) + return version.New(2, 5, 1) } // EOF diff --git a/rest/doc.go b/rest/doc.go index e993219..e6dbf8e 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 5, 0) + return version.New(2, 5, 1) } // EOF diff --git a/rest/formatter.go b/rest/formatter.go index 0fa3391..dd273d0 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -60,13 +60,13 @@ var ( // ENVELOPE //-------------------- -// Envelope is a helper to give a qualified feedback in RESTful requests. +// envelope is a helper to give a qualified feedback in RESTful requests. // It contains wether the request has been successful, in case of an // error an additional message and the payload. -type Envelope struct { - Success bool - Message string - Payload interface{} +type envelope struct { + Status string `json:"status" xml:"status"` + Message string `json:"message,omitempty" xml:"message,omitempty"` + Payload interface{} `json:"payload,omitempty" xml:"payload,omitempty"` } //-------------------- @@ -87,13 +87,13 @@ type Formatter interface { // PositiveFeedback writes a positive feedback envelope to the formatter. func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) error { fmsg := fmt.Sprintf(msg, args...) - return f.Write(StatusOK, &Envelope{true, fmsg, payload}) + return f.Write(StatusOK, envelope{"success", fmsg, payload}) } // NegativeFeedback writes a negative feedback envelope to the formatter. func NegativeFeedback(f Formatter, status int, msg string, args ...interface{}) error { fmsg := fmt.Sprintf(msg, args...) - return f.Write(status, &Envelope{false, fmsg, nil}) + return f.Write(status, envelope{"fail", fmsg, nil}) } //-------------------- diff --git a/restaudit/doc.go b/restaudit/doc.go index e42e0b1..8291b92 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // Version returns the version of the REST Audit package. func Version() version.Version { - return version.New(2, 5, 0) + return version.New(2, 5, 1) } // EOF From bc154018a45503b923e1f315fd899865a09d8ed7 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 25 Oct 2016 16:44:58 +0200 Subject: [PATCH 053/127] Fixed feedback for JWT access denial --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/doc.go | 2 +- handlers/jwtauth.go | 15 +++++++++++++-- jwt/doc.go | 2 +- rest/doc.go | 2 +- restaudit/doc.go | 2 +- 7 files changed, 22 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d261905..1ac4c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-10-25 + +- Fixed missing feedback after JWT authorization denial + ## 2016-10-24 - Fixed marshalling bug of positive or negative feedback diff --git a/README.md b/README.md index 55c4d8a..6fab4aa 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.5.1 +Version 2.5.2 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index b521b45..6ac9ce6 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the handlers package. func Version() version.Version { - return version.New(2, 5, 1) + return version.New(2, 5, 2) } // EOF diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index e6472ce..21c955c 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -15,6 +15,8 @@ import ( "context" "time" + "github.com/tideland/golib/logger" + "github.com/tideland/gorest/jwt" "github.com/tideland/gorest/rest" ) @@ -38,6 +40,7 @@ type JWTAuthorizationConfig struct { Key jwt.Key Leeway time.Duration Gatekeeper func(job rest.Job, claims jwt.Claims) error + Logger func(job rest.Job, msg string) } // jwtAuthorizationHandler checks for a valid token and then runs @@ -49,6 +52,7 @@ type jwtAuthorizationHandler struct { key jwt.Key leeway time.Duration gatekeeper func(job rest.Job, claims jwt.Claims) error + logger func(job rest.Job, msg string) } // NewJWTAuthorizationHandler creates a handler checking for a valid JSON @@ -57,6 +61,9 @@ func NewJWTAuthorizationHandler(id string, config *JWTAuthorizationConfig) rest. h := &jwtAuthorizationHandler{ id: id, leeway: time.Minute, + logger: func(job rest.Job, msg string) { + logger.Warningf("access denied for %v: %s", job, msg) + }, } if config != nil { if config.Cache != nil { @@ -71,6 +78,9 @@ func NewJWTAuthorizationHandler(id string, config *JWTAuthorizationConfig) rest. if config.Gatekeeper != nil { h.gatekeeper = config.Gatekeeper } + if config.Logger != nil { + h.logger = config.Logger + } } return h } @@ -142,12 +152,12 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { return false, h.deny(job, "no JSON Web Token") } if !jobJWT.IsValid(h.leeway) { - return false, h.deny(job, "invalid JSON Web Token") + return false, h.deny(job, "JSON Web Token claims 'nbf' and/or 'exp' are not valid") } if h.gatekeeper != nil { err := h.gatekeeper(job, jobJWT.Claims()) if err != nil { - return false, h.deny(job, "gatekeeper denied:"+err.Error()) + return false, h.deny(job, "access rejected by gatekeeper: "+err.Error()) } } // All fine, store token in context. @@ -159,6 +169,7 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { // deny sends a negative feedback to the caller. func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { + h.logger(job, msg) switch { case job.AcceptsContentType(rest.ContentTypeJSON): return rest.NegativeFeedback(job.JSON(false), rest.StatusUnauthorized, msg) diff --git a/jwt/doc.go b/jwt/doc.go index b2d1317..41bed19 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the JSON Web Token package. func Version() version.Version { - return version.New(2, 5, 1) + return version.New(2, 5, 2) } // EOF diff --git a/rest/doc.go b/rest/doc.go index e6dbf8e..da69d71 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 5, 1) + return version.New(2, 5, 2) } // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index 8291b92..03d3fb0 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // Version returns the version of the REST Audit package. func Version() version.Version { - return version.New(2, 5, 1) + return version.New(2, 5, 2) } // EOF From 53b21a2672c1e565c277710227d58a7b7c6a081e Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 26 Oct 2016 11:06:57 +0200 Subject: [PATCH 054/127] Small comment extension Configuration of JWT Authorization Handler should be better understandable now. --- handlers/jwtauth.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 21c955c..3c5e4a2 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -33,8 +33,10 @@ var jwtKey key = 0 // JWTAuthorizationConfig allows to control how the JWT authorization // handler works. All values are optional. In this case tokens are only -// decoded without using a cache, validated for the time, and there's -// no user defined gatekeeper function running afterwards. +// decoded without using a cache, validated for the current time plus/minus +// a minute leeway, and there's no user defined gatekeeper function +// running afterwards. In case of a denial a warning is written with +// the standard logger. type JWTAuthorizationConfig struct { Cache jwt.Cache Key jwt.Key From b0c90da1756876ee69f06805ac52a3a0379e6c4d Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 27 Oct 2016 17:26:50 +0200 Subject: [PATCH 055/127] Started new package request --- request/doc.go | 29 +++++++++ request/errors.go | 36 ++++++++++++ request/request.go | 143 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 request/doc.go create mode 100644 request/errors.go create mode 100644 request/request.go diff --git a/request/doc.go b/request/doc.go new file mode 100644 index 0000000..7641e07 --- /dev/null +++ b/request/doc.go @@ -0,0 +1,29 @@ +// Tideland Go REST Server Library - REST Request +// +// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +// The Tideland Go REST Server Library request provides simpler +// requests to handlers of the Tideland Go REST Server Library world. +package request + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "github.com/tideland/golib/version" +) + +//-------------------- +// VERSION +//-------------------- + +// Version returns the version of the REST Audit package. +func Version() version.Version { + return version.New(2, 5, 2) +} + +// EOF diff --git a/request/errors.go b/request/errors.go new file mode 100644 index 0000000..d56208c --- /dev/null +++ b/request/errors.go @@ -0,0 +1,36 @@ +// Tideland Go REST Server Library - Request - Errors +// +// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package rest + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "github.com/tideland/golib/errors" +) + +//-------------------- +// CONSTANTS +//-------------------- + +const ( + ErrServiceNotConfigured = iota + 1 + ErrCannotPrepareRequest + ErrHTTPRequestFailed + ErrReadingResponse +) + +var errorMessages = errors.Messages{ + ErrServiceNotConfigured: "service '%s' is not configured", + ErrCannotPrepareRequest: "cannot prepare request", + ErrHTTPRequestFailed: "HTTP request failed", + ErrReadingResponse: "cannot read the HTTP response", +} + +// EOF diff --git a/request/request.go b/request/request.go new file mode 100644 index 0000000..88f5ebf --- /dev/null +++ b/request/request.go @@ -0,0 +1,143 @@ +// Tideland Go REST Server Library - Request +// +// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package restaudit + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "bytes" + "context" + "crypto/tls" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "strings" + + "github.com/tideland/golib/errors" + + "github.com/tideland/gorest/jwt" + "github.com/tideland/gorest/rest" +) + +//-------------------- +// TEST TOOLS +//-------------------- + +// KeyValues handles keys and values for request headers and cookies. +type KeyValues map[string]string + +// Response wraps all infos of a test response. +type Response struct { + Status int + Header KeyValues + ContentType string + Content interface{} +} + +//-------------------- +// CALLER +//-------------------- + +// Service contains the configuration of one service. +type Service struct { + Transport *http.Transport + BaseURL string +} + +// Services maps IDs of services to their base URL. +type Services map[string]Service + +// Parameters allows to pass parameters to a call. +type Parameters struct { + Token jwt.JWT + ContentType string + Content interface{} +} + +// Caller provides an interface to make calls to +// configured services. +type Caller interface { + // Get performs a GET request on the defined service. + Get(service, domain, resource, resourceID string, params *Parameters) (*Response, error) +} + +// caller implements the Caller interface. +type caller struct { + services Services +} + +// NewCaller creates a configured caller. +func NewCaller(services Services) Caller { + return &caller{services} +} + +// Get implements the Caller interface. +func (c *caller) Get(service, domain, resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("GET", service, domain, resource, resourceID, params) +} + +// request performs all requests. +func (c *caller) request(method, service, domain, resource, resourceID string, params *Parameters) (*Response, error) { + svc, ok := c.services[service] + if !ok { + return nil, errors.New(ErrServiceNotConfigured, errorMessages, service) + } + // Prepare client and request. + client := &http.Client{} + if svc.Transport != nil { + client.Transport = svc.Transport + } + parts := append(svc.BaseURL, domain, resource) + if resourceID != "" { + parts = append(parts, resourceID) + } + url := strings.Join(parts, "/") + var content io.Reader + if params.Content != nil { + // Process content based on content type. + + } + request, err := http.NewRequest(method, url, content) + if err != nil { + return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) + } + if params.Token != nil { + request = jwt.AddTokenToRequest(request, params.Token) + } + // Perform request. + response, err := client.Do(request) + if err != nil { + return nil, errors.Annotate(err, ErrHTTPRequestFailed, errorMessages) + } + // Analyze response. + return analyzeResponse(response) +} + +// analyzeResponse creates a response struct out of the HTTP response. +func analyzeResponse(response *http.Response) (*Response, error) { + header := KeyValues{} + for key, values := range response.Header { + header[key] = strings.Join(values, ", ") + } + content, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, errors.Annotate(err, ErrReadingResponse) + } + response.Body.Close() + return &Response{ + Status: response.StatusCode, + Header: header, + ContentType: header["Content-Type"], + Content: content, + } +} + +// EOF From 349f43aa49e59b19efa31e36b6c6c9dfc625fab4 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 27 Oct 2016 22:49:04 +0200 Subject: [PATCH 056/127] Added content marshalling for requests --- request/errors.go | 10 ++++++---- request/request.go | 28 +++++++++++++++++++++++++--- rest/formatter.go | 11 ++++++----- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/request/errors.go b/request/errors.go index d56208c..3599094 100644 --- a/request/errors.go +++ b/request/errors.go @@ -23,14 +23,16 @@ const ( ErrServiceNotConfigured = iota + 1 ErrCannotPrepareRequest ErrHTTPRequestFailed + ErrProcessingRequestContent ErrReadingResponse ) var errorMessages = errors.Messages{ - ErrServiceNotConfigured: "service '%s' is not configured", - ErrCannotPrepareRequest: "cannot prepare request", - ErrHTTPRequestFailed: "HTTP request failed", - ErrReadingResponse: "cannot read the HTTP response", + ErrServiceNotConfigured: "service '%s' is not configured", + ErrCannotPrepareRequest: "cannot prepare request", + ErrHTTPRequestFailed: "HTTP request failed", + ErrProcessingRequestContent: "cannot process request content", + ErrReadingResponse: "cannot read the HTTP response", } // EOF diff --git a/request/request.go b/request/request.go index 88f5ebf..cd78918 100644 --- a/request/request.go +++ b/request/request.go @@ -100,15 +100,37 @@ func (c *caller) request(method, service, domain, resource, resourceID string, p parts = append(parts, resourceID) } url := strings.Join(parts, "/") - var content io.Reader + var buffer io.Reader if params.Content != nil { // Process content based on content type. - + switch params.ContentType { + case ContentTypeXML: + tmp, err := xml.Marshal(params.Content) + if err != nil { + return nil, error.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + buffer = bytes.NewReader(tmp) + case ContentTypeJSON: + tmp, err := json.Marshal(params.Content) + if err != nil { + return nil, error.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + buffer = bytes.NewReader(tmp) + case ContentTypeGOB: + enc := gob.NewEncoder(buffer) + if err := enc.Encode(content); err != nil { + return nil, error.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + case ContentTypeURLEncoded: + } } - request, err := http.NewRequest(method, url, content) + request, err := http.NewRequest(method, url, buffer) if err != nil { return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) } + if params.ContentType != "" { + request.Header.Set("Content-Type", params.ContentType) + } if params.Token != nil { request = jwt.AddTokenToRequest(request, params.Token) } diff --git a/rest/formatter.go b/rest/formatter.go index dd273d0..6b26a52 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -41,11 +41,12 @@ const ( StatusConflict = http.StatusConflict // Standard REST content types. - ContentTypePlain = "text/plain" - ContentTypeHTML = "text/html" - ContentTypeXML = "application/xml" - ContentTypeJSON = "application/json" - ContentTypeGOB = "application/vnd.tideland.gob" + ContentTypePlain = "text/plain" + ContentTypeHTML = "text/html" + ContentTypeXML = "application/xml" + ContentTypeJSON = "application/json" + ContentTypeGOB = "application/vnd.tideland.gob" + ContentTypeURLEncoded = "application/x-www-form-urlencoded" ) //-------------------- From c95a9a0e32d917d2376943d2c98c4c943a2d5d3c Mon Sep 17 00:00:00 2001 From: themue Date: Fri, 28 Oct 2016 16:17:25 +0200 Subject: [PATCH 057/127] Request handling almost finished --- request/doc.go | 2 +- request/errors.go | 8 +- request/request.go | 227 +++++++++++++++++++++++++++++++++------------ 3 files changed, 174 insertions(+), 63 deletions(-) diff --git a/request/doc.go b/request/doc.go index 7641e07..0a5fa37 100644 --- a/request/doc.go +++ b/request/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the REST Audit package. func Version() version.Version { - return version.New(2, 5, 2) + return version.New(2, 6, 0, "alpha", "2016-10-28") } // EOF diff --git a/request/errors.go b/request/errors.go index 3599094..7c1f49d 100644 --- a/request/errors.go +++ b/request/errors.go @@ -5,7 +5,7 @@ // All rights reserved. Use of this source code is governed // by the new BSD license. -package rest +package request //-------------------- // IMPORTS @@ -20,18 +20,20 @@ import ( //-------------------- const ( - ErrServiceNotConfigured = iota + 1 + ErrNoServerDefined = iota + 1 ErrCannotPrepareRequest ErrHTTPRequestFailed ErrProcessingRequestContent + ErrContentNotKeyValue ErrReadingResponse ) var errorMessages = errors.Messages{ - ErrServiceNotConfigured: "service '%s' is not configured", + ErrNoServerDefined: "no server for domain '%s' configured", ErrCannotPrepareRequest: "cannot prepare request", ErrHTTPRequestFailed: "HTTP request failed", ErrProcessingRequestContent: "cannot process request content", + ErrContentNotKeyValue: "content is not key/value", ErrReadingResponse: "cannot read the HTTP response", } diff --git a/request/request.go b/request/request.go index cd78918..e6ae65a 100644 --- a/request/request.go +++ b/request/request.go @@ -5,7 +5,7 @@ // All rights reserved. Use of this source code is governed // by the new BSD license. -package restaudit +package request //-------------------- // IMPORTS @@ -14,12 +14,17 @@ package restaudit import ( "bytes" "context" - "crypto/tls" + "encoding/gob" + "encoding/json" + "encoding/xml" "io" "io/ioutil" - "mime/multipart" + "math/rand" "net/http" + "net/url" "strings" + "sync" + "time" "github.com/tideland/golib/errors" @@ -28,7 +33,81 @@ import ( ) //-------------------- -// TEST TOOLS +// SERVERS +//-------------------- + +// key is to address the servers inside a context. +type key int + +var serversKey key = 0 + +// server contains the configuration of one server. +type server struct { + URL string + Transport *http.Transport +} + +// Servers maps IDs of domains to their server configurations. +// Multiple ones can be added per domain for spreading the +// load or provide higher availability. +type Servers interface { + // Add adds a domain server configuration. + Add(domain string, url string, transport *http.Transport) + + // Caller retrieves a caller for a domain. + Caller(domain string) (Caller, error) +} + +// servers implements servers. +type servers struct { + mutex sync.RWMutex + servers map[string][]*server +} + +// NewServers creates a new servers manager. +func NewServers() Servers { + rand.Seed(time.Now().Unix()) + return &servers{ + servers: make(map[string][]*server), + } +} + +// Add implements the Servers interface. +func (s *servers) Add(domain, url string, transport *http.Transport) { + s.mutex.Lock() + defer s.mutex.Unlock() + srvs, ok := s.servers[domain] + if ok { + s.servers[domain] = append(srvs, &server{url, transport}) + return + } + s.servers[domain] = []*server{&server{url, transport}} +} + +// Caller implements the Servers interface. +func (s *servers) Caller(domain string) (Caller, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + srvs, ok := s.servers[domain] + if !ok { + return nil, errors.New(ErrNoServerDefined, errorMessages, domain) + } + return newCaller(domain, srvs), nil +} + +// NewContext returns a new context that carries configured servers. +func NewContext(ctx context.Context, servers Servers) context.Context { + return context.WithValue(ctx, serversKey, servers) +} + +// FromContext returns the servers configuration stored in ctx, if any. +func FromContext(ctx context.Context) (Servers, bool) { + servers, ok := ctx.Value(serversKey).(Servers) + return servers, ok +} + +//-------------------- +// GENERAL HELPERS //-------------------- // KeyValues handles keys and values for request headers and cookies. @@ -43,18 +122,9 @@ type Response struct { } //-------------------- -// CALLER +// CALL PARAMETERS //-------------------- -// Service contains the configuration of one service. -type Service struct { - Transport *http.Transport - BaseURL string -} - -// Services maps IDs of services to their base URL. -type Services map[string]Service - // Parameters allows to pass parameters to a call. type Parameters struct { Token jwt.JWT @@ -62,73 +132,112 @@ type Parameters struct { Content interface{} } +// body returns the content as body data depending on +// the content type. +func (p *Parameters) body() (io.Reader, error) { + buffer := bytes.NewBuffer(nil) + if p.Content != nil { + // Process content based on content type. + switch p.ContentType { + case rest.ContentTypeXML: + tmp, err := xml.Marshal(p.Content) + if err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + buffer.Write(tmp) + case rest.ContentTypeJSON: + tmp, err := json.Marshal(p.Content) + if err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + buffer.Write(tmp) + case rest.ContentTypeGOB: + enc := gob.NewEncoder(buffer) + if err := enc.Encode(p.Content); err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + } + } + return buffer, nil +} + +// values returns the content as URL encoded values. +func (p *Parameters) values() (url.Values, error) { + kvs, ok := p.Content.(KeyValues) + if !ok { + return nil, errors.New(ErrContentNotKeyValue, errorMessages) + } + values := url.Values{} + for key, value := range kvs { + values.Set(key, value) + } + return values, nil +} + +//-------------------- +// CALLER +//-------------------- + // Caller provides an interface to make calls to // configured services. type Caller interface { // Get performs a GET request on the defined service. - Get(service, domain, resource, resourceID string, params *Parameters) (*Response, error) + Get(resource, resourceID string, params *Parameters) (*Response, error) } // caller implements the Caller interface. type caller struct { - services Services + domain string + srvs []*server } -// NewCaller creates a configured caller. -func NewCaller(services Services) Caller { - return &caller{services} +// newCaller creates a configured caller. +func newCaller(domain string, srvs []*server) Caller { + return &caller{domain, srvs} } // Get implements the Caller interface. -func (c *caller) Get(service, domain, resource, resourceID string, params *Parameters) (*Response, error) { - return c.request("GET", service, domain, resource, resourceID, params) +func (c *caller) Get(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("GET", resource, resourceID, params) } // request performs all requests. -func (c *caller) request(method, service, domain, resource, resourceID string, params *Parameters) (*Response, error) { - svc, ok := c.services[service] - if !ok { - return nil, errors.New(ErrServiceNotConfigured, errorMessages, service) - } - // Prepare client and request. +func (c *caller) request(method, resource, resourceID string, params *Parameters) (*Response, error) { + // Prepare client. + // TODO Mue 2016-10-28 Add more algorithms than just random selection. + srv := c.srvs[rand.Intn(len(c.srvs))] client := &http.Client{} - if svc.Transport != nil { - client.Transport = svc.Transport + if srv.Transport != nil { + client.Transport = srv.Transport + } + u, err := url.Parse(srv.URL) + if err != nil { + return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) } - parts := append(svc.BaseURL, domain, resource) + upath := strings.Trim(u.Path, "/") + path := []string{upath, c.domain, resource} if resourceID != "" { - parts = append(parts, resourceID) + path = append(path, resourceID) } - url := strings.Join(parts, "/") - var buffer io.Reader - if params.Content != nil { - // Process content based on content type. - switch params.ContentType { - case ContentTypeXML: - tmp, err := xml.Marshal(params.Content) - if err != nil { - return nil, error.Annotate(err, ErrProcessingRequestContent, errorMessages) - } - buffer = bytes.NewReader(tmp) - case ContentTypeJSON: - tmp, err := json.Marshal(params.Content) - if err != nil { - return nil, error.Annotate(err, ErrProcessingRequestContent, errorMessages) - } - buffer = bytes.NewReader(tmp) - case ContentTypeGOB: - enc := gob.NewEncoder(buffer) - if err := enc.Encode(content); err != nil { - return nil, error.Annotate(err, ErrProcessingRequestContent, errorMessages) - } - case ContentTypeURLEncoded: - } + u.Path = strings.Join(path, "/") + // Prepare request. + body, err := params.body() + if err != nil { + return nil, err } - request, err := http.NewRequest(method, url, buffer) + request, err := http.NewRequest(method, u.String(), body) if err != nil { return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) } - if params.ContentType != "" { + switch params.ContentType { + case rest.ContentTypeURLEncoded: + values, err := params.values() + if err != nil { + return nil, err + } + request.URL.RawQuery = values.Encode() + request.Header.Set("Content-Type", params.ContentType) + case rest.ContentTypeGOB, rest.ContentTypeJSON, rest.ContentTypeXML: request.Header.Set("Content-Type", params.ContentType) } if params.Token != nil { @@ -151,7 +260,7 @@ func analyzeResponse(response *http.Response) (*Response, error) { } content, err := ioutil.ReadAll(response.Body) if err != nil { - return nil, errors.Annotate(err, ErrReadingResponse) + return nil, errors.Annotate(err, ErrReadingResponse, errorMessages) } response.Body.Close() return &Response{ @@ -159,7 +268,7 @@ func analyzeResponse(response *http.Response) (*Response, error) { Header: header, ContentType: header["Content-Type"], Content: content, - } + }, nil } // EOF From 816c3b7e020217ddfbe57180807b90b30ae0ecfd Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 30 Oct 2016 22:28:51 +0100 Subject: [PATCH 058/127] Started with the unit tests of the request package --- request/doc.go | 10 +-- request/request.go | 5 +- request/request_test.go | 160 ++++++++++++++++++++++++++++++++++++++++ rest/errors.go | 44 +++++++---- 4 files changed, 197 insertions(+), 22 deletions(-) create mode 100644 request/request_test.go diff --git a/request/doc.go b/request/doc.go index 0a5fa37..bdf03ed 100644 --- a/request/doc.go +++ b/request/doc.go @@ -1,12 +1,12 @@ -// Tideland Go REST Server Library - REST Request +// Tideland Go REST Server Library - Request // // Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. -// The Tideland Go REST Server Library request provides simpler -// requests to handlers of the Tideland Go REST Server Library world. +// The request package provides a simple way to handle cross-server +// requests in the Tideland REST ecosystem. package request //-------------------- @@ -21,9 +21,9 @@ import ( // VERSION //-------------------- -// Version returns the version of the REST Audit package. +// Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 6, 0, "alpha", "2016-10-28") + return version.New(2, 6, 0, "alpha", "2016-10-30") } // EOF diff --git a/request/request.go b/request/request.go index e6ae65a..a25635d 100644 --- a/request/request.go +++ b/request/request.go @@ -220,7 +220,10 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters path = append(path, resourceID) } u.Path = strings.Join(path, "/") - // Prepare request. + // Prepare request, check the parameters first. + if params == nil { + params = &Parameters{} + } body, err := params.body() if err != nil { return nil, err diff --git a/request/request_test.go b/request/request_test.go new file mode 100644 index 0000000..77b3e1e --- /dev/null +++ b/request/request_test.go @@ -0,0 +1,160 @@ +// Tideland Go REST Server Library - Request - Unit Tests +// +// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package request_test + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "context" + "net/http" + "testing" + + "github.com/tideland/golib/audit" + "github.com/tideland/golib/etc" + "github.com/tideland/golib/logger" + + "github.com/tideland/gorest/request" + "github.com/tideland/gorest/rest" +) + +//-------------------- +// TESTS +//-------------------- + +// tests defines requests and asserts. +var tests = []struct { + name string + method string + resource string + id string + params *request.Parameters +}{ + { + name: "GET for one item formatted in JSON", + method: "GET", + resource: "item", + id: "foo", + params: &request.Parameters{ + ContentType: rest.ContentTypeJSON, + }, + }, +} + +// TestRequests runs the different request tests. +func TestRequests(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + servers := newServers(assert) + // Run the tests. + for i, test := range tests { + assert.Logf("test #%d: %s", i, test.name) + caller, err := servers.Caller("testing") + assert.Nil(err) + var response *request.Response + switch test.method { + case "GET": + response, err = caller.Get(test.resource, test.id, test.params) + assert.Nil(err) + } + assert.Logf("response: %+v", response) + } +} + +//-------------------- +// TEST HANDLER +//-------------------- + +type TestHandler struct { + index int + assert audit.Assertion +} + +func NewTestHandler(index int, assert audit.Assertion) rest.ResourceHandler { + return &TestHandler{index, assert} +} + +func (th *TestHandler) ID() string { + return "test" +} + +func (th *TestHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +func (th *TestHandler) Get(job rest.Job) (bool, error) { + switch { + case job.HasContentType(rest.ContentTypeJSON): + job.JSON(true).Write(rest.StatusOK, th.item()) + case job.HasContentType(rest.ContentTypeXML): + job.XML().Write(rest.StatusOK, th.item()) + } + return true, nil +} + +func (th *TestHandler) Head(job rest.Job) (bool, error) { + return true, nil +} + +func (th *TestHandler) Put(job rest.Job) (bool, error) { + return true, nil +} + +func (th *TestHandler) Post(job rest.Job) (bool, error) { + return true, nil +} + +func (th *TestHandler) Patch(job rest.Job) (bool, error) { + return true, nil +} + +func (th *TestHandler) Delete(job rest.Job) (bool, error) { + return true, nil +} + +func (th *TestHandler) Options(job rest.Job) (bool, error) { + return true, nil +} + +func (th *TestHandler) item() map[string]interface{} { + return map[string]interface{}{ + "index": th.index, + "name": "Item", + } +} + +//-------------------- +// SERVER +//-------------------- + +// newServers starts the server map for the requests. +func newServers(assert audit.Assertion) request.Servers { + // Preparation. + logger.SetLevel(logger.LevelDebug) + cfgStr := "{etc {basepath /}{default-domain testing}{default-resource item}}" + cfg, err := etc.ReadString(cfgStr) + assert.Nil(err) + addresses := []string{":12345", ":12346", ":12347", ":12348", ":12349"} + servers := request.NewServers() + // Start and register each server. + for i, address := range addresses { + mux := rest.NewMultiplexer(context.Background(), cfg) + h := NewTestHandler(i, assert) + err = mux.Register("testing", "item", h) + assert.Nil(err) + err = mux.Register("testing", "items", h) + assert.Nil(err) + go func() { + http.ListenAndServe(address, mux) + }() + servers.Add("testing", "http://localhost"+address, nil) + } + return servers +} + +// EOF diff --git a/rest/errors.go b/rest/errors.go index 1973471..9174fb0 100644 --- a/rest/errors.go +++ b/rest/errors.go @@ -36,25 +36,37 @@ const ( ErrInvalidContentType ErrNoCachedTemplate ErrQueryValueNotFound + ErrNoServerDefined + ErrCannotPrepareRequest + ErrHTTPRequestFailed + ErrProcessingRequestContent + ErrContentNotKeyValue + ErrReadingResponse ) var errorMessages = errors.Messages{ - ErrDuplicateHandler: "cannot register handler %q, it is already registered", - ErrInitHandler: "error during initialization of handler %q", - ErrIllegalRequest: "illegal request containing too many parts", - ErrNoHandler: "found no handler with ID %q", - ErrNoGetHandler: "handler %q is no handler for GET requests", - ErrNoHeadHandler: "handler %q is no handler for HEAD requests", - ErrNoPutHandler: "handler %q is no handler for PUT requests", - ErrNoPostHandler: "handler %q is no handler for POST requests", - ErrNoPatchHandler: "handler %q is no handler for PATCH requests", - ErrNoDeleteHandler: "handler %q is no handler for DELETE requests", - ErrNoOptionsHandler: "handler %q is no handler for OPTIONS requests", - ErrMethodNotSupported: "method %q is not supported", - ErrUploadingFile: "uploaded file cannot be handled by %q", - ErrInvalidContentType: "content type is not %q", - ErrNoCachedTemplate: "template %q is not cached", - ErrQueryValueNotFound: "query value not found", + ErrDuplicateHandler: "cannot register handler %q, it is already registered", + ErrInitHandler: "error during initialization of handler %q", + ErrIllegalRequest: "illegal request containing too many parts", + ErrNoHandler: "found no handler with ID %q", + ErrNoGetHandler: "handler %q is no handler for GET requests", + ErrNoHeadHandler: "handler %q is no handler for HEAD requests", + ErrNoPutHandler: "handler %q is no handler for PUT requests", + ErrNoPostHandler: "handler %q is no handler for POST requests", + ErrNoPatchHandler: "handler %q is no handler for PATCH requests", + ErrNoDeleteHandler: "handler %q is no handler for DELETE requests", + ErrNoOptionsHandler: "handler %q is no handler for OPTIONS requests", + ErrMethodNotSupported: "method %q is not supported", + ErrUploadingFile: "uploaded file cannot be handled by %q", + ErrInvalidContentType: "content type is not %q", + ErrNoCachedTemplate: "template %q is not cached", + ErrQueryValueNotFound: "query value not found", + ErrNoServerDefined: "no server for domain '%s' configured", + ErrCannotPrepareRequest: "cannot prepare request", + ErrHTTPRequestFailed: "HTTP request failed", + ErrProcessingRequestContent: "cannot process request content", + ErrContentNotKeyValue: "content is not key/value", + ErrReadingResponse: "cannot read the HTTP response", } // EOF From a2877b1335c1c510085098811b24a62462835cd9 Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 31 Oct 2016 15:49:38 +0100 Subject: [PATCH 059/127] Enhanced request testing --- request/errors.go | 8 ++++-- request/request.go | 35 +++++++++++++++++++++++--- request/request_test.go | 54 +++++++++++++++++++++++++++++++++-------- rest/formatter.go | 6 ++--- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/request/errors.go b/request/errors.go index 7c1f49d..04b6e69 100644 --- a/request/errors.go +++ b/request/errors.go @@ -25,7 +25,9 @@ const ( ErrHTTPRequestFailed ErrProcessingRequestContent ErrContentNotKeyValue - ErrReadingResponse + ErrAnalyzingResponse + ErrDecodingResponse + ErrInvalidContentType ) var errorMessages = errors.Messages{ @@ -34,7 +36,9 @@ var errorMessages = errors.Messages{ ErrHTTPRequestFailed: "HTTP request failed", ErrProcessingRequestContent: "cannot process request content", ErrContentNotKeyValue: "content is not key/value", - ErrReadingResponse: "cannot read the HTTP response", + ErrAnalyzingResponse: "cannot analyze the HTTP response", + ErrDecodingResponse: "cannot decode the HTTP response", + ErrInvalidContentType: "invalid content type '%s'", } // EOF diff --git a/request/request.go b/request/request.go index a25635d..d4bcd5a 100644 --- a/request/request.go +++ b/request/request.go @@ -107,7 +107,7 @@ func FromContext(ctx context.Context) (Servers, bool) { } //-------------------- -// GENERAL HELPERS +// RESPONSE //-------------------- // KeyValues handles keys and values for request headers and cookies. @@ -118,7 +118,36 @@ type Response struct { Status int Header KeyValues ContentType string - Content interface{} + Content []byte +} + +// HasContentType checks the content type regardless of charsets. +func (r *Response) HasContentType(contentType string) bool { + return strings.Contains(r.ContentType, contentType) +} + +// Read decodes the content into the passed data depending +// on the content type. +func (r *Response) Read(data interface{}) error { + switch { + case r.HasContentType(rest.ContentTypeGOB): + dec := gob.NewDecoder(bytes.NewBuffer(r.Content)) + if err := dec.Decode(data); err != nil { + return errors.Annotate(err, ErrDecodingResponse, errorMessages) + } + return nil + case r.HasContentType(rest.ContentTypeJSON): + if err := json.Unmarshal(r.Content, &data); err != nil { + return errors.Annotate(err, ErrDecodingResponse, errorMessages) + } + return nil + case r.HasContentType(rest.ContentTypeXML): + if err := xml.Unmarshal(r.Content, &data); err != nil { + return errors.Annotate(err, ErrDecodingResponse, errorMessages) + } + return nil + } + return errors.New(ErrInvalidContentType, errorMessages, r.ContentType) } //-------------------- @@ -263,7 +292,7 @@ func analyzeResponse(response *http.Response) (*Response, error) { } content, err := ioutil.ReadAll(response.Body) if err != nil { - return nil, errors.Annotate(err, ErrReadingResponse, errorMessages) + return nil, errors.Annotate(err, ErrAnalyzingResponse, errorMessages) } response.Body.Close() return &Response{ diff --git a/request/request_test.go b/request/request_test.go index 77b3e1e..440f654 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -15,6 +15,7 @@ import ( "context" "net/http" "testing" + "time" "github.com/tideland/golib/audit" "github.com/tideland/golib/etc" @@ -28,6 +29,12 @@ import ( // TESTS //-------------------- +// data is used for transfer data in the tests. +type data struct { + Index int + Name string +} + // tests defines requests and asserts. var tests = []struct { name string @@ -35,15 +42,32 @@ var tests = []struct { resource string id string params *request.Parameters + expected *data }{ { name: "GET for one item formatted in JSON", method: "GET", resource: "item", id: "foo", - params: &request.Parameters{ + params: &request.Parameters{ ContentType: rest.ContentTypeJSON, }, + expected: &data{ + Index: 0, + Name: "foo", + }, + }, { + name: "GET for one item formatted in XML", + method: "GET", + resource: "item", + id: "foo", + params: &request.Parameters{ + ContentType: rest.ContentTypeXML, + }, + expected: &data{ + Index: 0, + Name: "foo", + }, }, } @@ -60,9 +84,14 @@ func TestRequests(t *testing.T) { switch test.method { case "GET": response, err = caller.Get(test.resource, test.id, test.params) - assert.Nil(err) } - assert.Logf("response: %+v", response) + assert.Nil(err) + assert.True(response.HasContentType(test.params.ContentType)) + var content data + err = response.Read(&content) + if test.expected != nil { + assert.Equal(content.Name, test.expected.Name) + } } } @@ -88,11 +117,14 @@ func (th *TestHandler) Init(env rest.Environment, domain, resource string) error } func (th *TestHandler) Get(job rest.Job) (bool, error) { + th.assert.Logf("CT: %v", job.Request().Header.Get("Content-Type")) switch { case job.HasContentType(rest.ContentTypeJSON): - job.JSON(true).Write(rest.StatusOK, th.item()) + th.assert.Logf("CT: JSON") + job.JSON(true).Write(rest.StatusOK, th.data(job.ResourceID())) case job.HasContentType(rest.ContentTypeXML): - job.XML().Write(rest.StatusOK, th.item()) + th.assert.Logf("CT: XML") + job.XML().Write(rest.StatusOK, th.data(job.ResourceID())) } return true, nil } @@ -121,10 +153,10 @@ func (th *TestHandler) Options(job rest.Job) (bool, error) { return true, nil } -func (th *TestHandler) item() map[string]interface{} { - return map[string]interface{}{ - "index": th.index, - "name": "Item", +func (th *TestHandler) data(name string) *data { + return &data{ + Index: th.index, + Name: name, } } @@ -139,7 +171,8 @@ func newServers(assert audit.Assertion) request.Servers { cfgStr := "{etc {basepath /}{default-domain testing}{default-resource item}}" cfg, err := etc.ReadString(cfgStr) assert.Nil(err) - addresses := []string{":12345", ":12346", ":12347", ":12348", ":12349"} + // addresses := []string{":12345", ":12346", ":12347", ":12348", ":12349"} + addresses := []string{":12345"} servers := request.NewServers() // Start and register each server. for i, address := range addresses { @@ -154,6 +187,7 @@ func newServers(assert audit.Assertion) request.Servers { }() servers.Add("testing", "http://localhost"+address, nil) } + time.Sleep(5 * time.Millisecond) return servers } diff --git a/rest/formatter.go b/rest/formatter.go index 6b26a52..fd4c1a8 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -109,8 +109,8 @@ type gobFormatter struct { // Write is specified on the Formatter interface. func (gf *gobFormatter) Write(status int, data interface{}) error { enc := gob.NewEncoder(gf.job.ResponseWriter()) - gf.job.ResponseWriter().WriteHeader(status) gf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeGOB) + gf.job.ResponseWriter().WriteHeader(status) err := enc.Encode(data) if err != nil { http.Error(gf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) @@ -152,8 +152,8 @@ func (jf *jsonFormatter) Write(status int, data interface{}) error { json.HTMLEscape(&buf, body) body = buf.Bytes() } - jf.job.ResponseWriter().WriteHeader(status) jf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeJSON) + jf.job.ResponseWriter().WriteHeader(status) _, err = jf.job.ResponseWriter().Write(body) return err } @@ -187,8 +187,8 @@ func (xf *xmlFormatter) Write(status int, data interface{}) error { http.Error(xf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) return err } - xf.job.ResponseWriter().WriteHeader(status) xf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeXML) + xf.job.ResponseWriter().WriteHeader(status) _, err = xf.job.ResponseWriter().Write(body) return err } From f6959db3bde643e7c8622e5ae32856bb6156d65a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 31 Oct 2016 22:40:35 +0100 Subject: [PATCH 060/127] Added more HTTP methods and improved content handling --- request/request.go | 62 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/request/request.go b/request/request.go index d4bcd5a..b8dc64f 100644 --- a/request/request.go +++ b/request/request.go @@ -185,6 +185,15 @@ func (p *Parameters) body() (io.Reader, error) { if err := enc.Encode(p.Content); err != nil { return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) } + case rest.ContentTypeURLEncoded: + values, err := p.values() + if err != nil { + return nil, err + } + _, err = buffer.WriteString(values.Encode()) + if err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } } } return buffer, nil @@ -210,8 +219,26 @@ func (p *Parameters) values() (url.Values, error) { // Caller provides an interface to make calls to // configured services. type Caller interface { - // Get performs a GET request on the defined service. + // Get performs a GET request on the defined resource. Get(resource, resourceID string, params *Parameters) (*Response, error) + + // Head performs a HEAD request on the defined resource. + Head(resource, resourceID string, params *Parameters) (*Response, error) + + // Put performs a PUT request on the defined resource. + Put(resource, resourceID string, params *Parameters) (*Response, error) + + // Post performs a POST request on the defined resource. + Post(resource, resourceID string, params *Parameters) (*Response, error) + + // Patch performs a PATCH request on the defined resource. + Patch(resource, resourceID string, params *Parameters) (*Response, error) + + // Delete performs a DELETE request on the defined resource. + Delete(resource, resourceID string, params *Parameters) (*Response, error) + + // Options performs a OPTIONS request on the defined resource. + Options(resource, resourceID string, params *Parameters) (*Response, error) } // caller implements the Caller interface. @@ -230,6 +257,36 @@ func (c *caller) Get(resource, resourceID string, params *Parameters) (*Response return c.request("GET", resource, resourceID, params) } +// Head implements the Caller interface. +func (c *caller) Head(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("HEAD", resource, resourceID, params) +} + +// Put implements the Caller interface. +func (c *caller) Put(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("PUT", resource, resourceID, params) +} + +// Post implements the Caller interface. +func (c *caller) Post(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("POST", resource, resourceID, params) +} + +// Patch implements the Caller interface. +func (c *caller) Patch(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("PATCH", resource, resourceID, params) +} + +// Delete implements the Caller interface. +func (c *caller) Delete(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("DELETE", resource, resourceID, params) +} + +// Options implements the Caller interface. +func (c *caller) Options(resource, resourceID string, params *Parameters) (*Response, error) { + return c.request("OPTIONS", resource, resourceID, params) +} + // request performs all requests. func (c *caller) request(method, resource, resourceID string, params *Parameters) (*Response, error) { // Prepare client. @@ -275,6 +332,9 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters if params.Token != nil { request = jwt.AddTokenToRequest(request, params.Token) } + if params.ContentType != "" { + request.Header.Set("Content-Type", params.ContentType) + } // Perform request. response, err := client.Do(request) if err != nil { From a509bc5b30fa9f4ebbed6f4f741b7837dae2abea Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 1 Nov 2016 17:01:48 +0100 Subject: [PATCH 061/127] Added fixes and tests --- request/errors.go | 4 +- request/request.go | 136 ++++++++++++++++++++++++++-------------- request/request_test.go | 94 +++++++++++++++++++-------- 3 files changed, 160 insertions(+), 74 deletions(-) diff --git a/request/errors.go b/request/errors.go index 04b6e69..e749079 100644 --- a/request/errors.go +++ b/request/errors.go @@ -24,7 +24,7 @@ const ( ErrCannotPrepareRequest ErrHTTPRequestFailed ErrProcessingRequestContent - ErrContentNotKeyValue + ErrInvalidContent ErrAnalyzingResponse ErrDecodingResponse ErrInvalidContentType @@ -35,7 +35,7 @@ var errorMessages = errors.Messages{ ErrCannotPrepareRequest: "cannot prepare request", ErrHTTPRequestFailed: "HTTP request failed", ErrProcessingRequestContent: "cannot process request content", - ErrContentNotKeyValue: "content is not key/value", + ErrInvalidContent: "content invalid for URL encoding", ErrAnalyzingResponse: "cannot analyze the HTTP response", ErrDecodingResponse: "cannot decode the HTTP response", ErrInvalidContentType: "invalid content type '%s'", diff --git a/request/request.go b/request/request.go index b8dc64f..12cb57d 100644 --- a/request/request.go +++ b/request/request.go @@ -146,6 +146,28 @@ func (r *Response) Read(data interface{}) error { return errors.Annotate(err, ErrDecodingResponse, errorMessages) } return nil + case r.HasContentType(rest.ContentTypeURLEncoded): + values, err := url.ParseQuery(string(r.Content)) + if err != nil { + return errors.Annotate(err, ErrDecodingResponse, errorMessages) + } + // Check for data type url.Values. + duv, ok := data.(url.Values) + if ok { + for key, value := range values { + duv[key] = value + } + return nil + } + // Check for data type KeyValues. + kvv, ok := data.(KeyValues) + if !ok { + return errors.New(ErrDecodingResponse, errorMessages) + } + for key, value := range values { + kvv[key] = strings.Join(value, " / ") + } + return nil } return errors.New(ErrInvalidContentType, errorMessages, r.ContentType) } @@ -159,41 +181,43 @@ type Parameters struct { Token jwt.JWT ContentType string Content interface{} + Accept string } // body returns the content as body data depending on // the content type. func (p *Parameters) body() (io.Reader, error) { buffer := bytes.NewBuffer(nil) - if p.Content != nil { - // Process content based on content type. - switch p.ContentType { - case rest.ContentTypeXML: - tmp, err := xml.Marshal(p.Content) - if err != nil { - return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) - } - buffer.Write(tmp) - case rest.ContentTypeJSON: - tmp, err := json.Marshal(p.Content) - if err != nil { - return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) - } - buffer.Write(tmp) - case rest.ContentTypeGOB: - enc := gob.NewEncoder(buffer) - if err := enc.Encode(p.Content); err != nil { - return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) - } - case rest.ContentTypeURLEncoded: - values, err := p.values() - if err != nil { - return nil, err - } - _, err = buffer.WriteString(values.Encode()) - if err != nil { - return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) - } + if p.Content == nil { + return buffer, nil + } + // Process content based on content type. + switch p.ContentType { + case rest.ContentTypeXML: + tmp, err := xml.Marshal(p.Content) + if err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + buffer.Write(tmp) + case rest.ContentTypeJSON: + tmp, err := json.Marshal(p.Content) + if err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + buffer.Write(tmp) + case rest.ContentTypeGOB: + enc := gob.NewEncoder(buffer) + if err := enc.Encode(p.Content); err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) + } + case rest.ContentTypeURLEncoded: + values, err := p.values() + if err != nil { + return nil, err + } + _, err = buffer.WriteString(values.Encode()) + if err != nil { + return nil, errors.Annotate(err, ErrProcessingRequestContent, errorMessages) } } return buffer, nil @@ -201,9 +225,18 @@ func (p *Parameters) body() (io.Reader, error) { // values returns the content as URL encoded values. func (p *Parameters) values() (url.Values, error) { + if p.Content == nil { + return url.Values{}, nil + } + // Check if type is already ok. + urlvs, ok := p.Content.(url.Values) + if ok { + return urlvs, nil + } + // Check for simple key/values. kvs, ok := p.Content.(KeyValues) if !ok { - return nil, errors.New(ErrContentNotKeyValue, errorMessages) + return nil, errors.New(ErrInvalidContent, errorMessages) } values := url.Values{} for key, value := range kvs { @@ -289,6 +322,9 @@ func (c *caller) Options(resource, resourceID string, params *Parameters) (*Resp // request performs all requests. func (c *caller) request(method, resource, resourceID string, params *Parameters) (*Response, error) { + if params == nil { + params = &Parameters{} + } // Prepare client. // TODO Mue 2016-10-28 Add more algorithms than just random selection. srv := c.srvs[rand.Intn(len(c.srvs))] @@ -307,33 +343,39 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters } u.Path = strings.Join(path, "/") // Prepare request, check the parameters first. - if params == nil { - params = &Parameters{} - } - body, err := params.body() - if err != nil { - return nil, err - } - request, err := http.NewRequest(method, u.String(), body) - if err != nil { - return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) - } - switch params.ContentType { - case rest.ContentTypeURLEncoded: + var request *http.Request + if method == "GET" || method == "HEAD" { + // These allow only URL encoded. + request, err = http.NewRequest(method, u.String(), nil) + if err != nil { + return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) + } values, err := params.values() if err != nil { return nil, err } request.URL.RawQuery = values.Encode() - request.Header.Set("Content-Type", params.ContentType) - case rest.ContentTypeGOB, rest.ContentTypeJSON, rest.ContentTypeXML: + request.Header.Set("Content-Type", rest.ContentTypeURLEncoded) + } else { + // Here use the body for content. + body, err := params.body() + if err != nil { + return nil, err + } + request, err = http.NewRequest(method, u.String(), body) + if err != nil { + return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) + } request.Header.Set("Content-Type", params.ContentType) } if params.Token != nil { request = jwt.AddTokenToRequest(request, params.Token) } - if params.ContentType != "" { - request.Header.Set("Content-Type", params.ContentType) + if params.Accept == "" { + params.Accept = params.ContentType + } + if params.Accept != "" { + request.Header.Set("Accept", params.Accept) } // Perform request. response, err := client.Do(request) diff --git a/request/request_test.go b/request/request_test.go index 440f654..d0bcb7c 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -13,7 +13,9 @@ package request_test import ( "context" + "fmt" "net/http" + "net/url" "testing" "time" @@ -42,7 +44,7 @@ var tests = []struct { resource string id string params *request.Parameters - expected *data + check func(assert audit.Assertion, response *request.Response) }{ { name: "GET for one item formatted in JSON", @@ -50,11 +52,14 @@ var tests = []struct { resource: "item", id: "foo", params: &request.Parameters{ - ContentType: rest.ContentTypeJSON, + Accept: rest.ContentTypeJSON, }, - expected: &data{ - Index: 0, - Name: "foo", + check: func(assert audit.Assertion, response *request.Response) { + assert.True(response.HasContentType(rest.ContentTypeJSON)) + var content data + err := response.Read(&content) + assert.Nil(err) + assert.Equal(content.Name, "foo") }, }, { name: "GET for one item formatted in XML", @@ -62,11 +67,37 @@ var tests = []struct { resource: "item", id: "foo", params: &request.Parameters{ - ContentType: rest.ContentTypeXML, + Accept: rest.ContentTypeXML, }, - expected: &data{ - Index: 0, - Name: "foo", + check: func(assert audit.Assertion, response *request.Response) { + assert.True(response.HasContentType(rest.ContentTypeXML)) + var content data + err := response.Read(&content) + assert.Nil(err) + assert.Equal(content.Name, "foo") + }, + }, { + name: "GET for one item formatted URL encoded", + method: "GET", + resource: "item", + id: "foo", + params: &request.Parameters{ + Accept: rest.ContentTypeURLEncoded, + }, + check: func(assert audit.Assertion, response *request.Response) { + assert.True(response.HasContentType(rest.ContentTypeURLEncoded)) + values := url.Values{} + err := response.Read(values) + assert.Nil(err) + assert.Equal(values["name"][0], "foo") + }, + }, { + name: "HEAD returning the resource ID as header", + method: "HEAD", + resource: "item", + id: "foo", + check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Header["Resource-Id"], "foo") }, }, } @@ -74,7 +105,7 @@ var tests = []struct { // TestRequests runs the different request tests. func TestRequests(t *testing.T) { assert := audit.NewTestingAssertion(t, true) - servers := newServers(assert) + servers := newServers(assert, 12345, 12346) // Run the tests. for i, test := range tests { assert.Logf("test #%d: %s", i, test.name) @@ -84,14 +115,21 @@ func TestRequests(t *testing.T) { switch test.method { case "GET": response, err = caller.Get(test.resource, test.id, test.params) + case "HEAD": + response, err = caller.Head(test.resource, test.id, test.params) + case "PUT": + response, err = caller.Put(test.resource, test.id, test.params) + case "POST": + response, err = caller.Post(test.resource, test.id, test.params) + case "PATCH": + response, err = caller.Patch(test.resource, test.id, test.params) + case "DELETE": + response, err = caller.Delete(test.resource, test.id, test.params) + case "OPTIONS": + response, err = caller.Options(test.resource, test.id, test.params) } assert.Nil(err) - assert.True(response.HasContentType(test.params.ContentType)) - var content data - err = response.Read(&content) - if test.expected != nil { - assert.Equal(content.Name, test.expected.Name) - } + test.check(assert, response) } } @@ -117,19 +155,26 @@ func (th *TestHandler) Init(env rest.Environment, domain, resource string) error } func (th *TestHandler) Get(job rest.Job) (bool, error) { - th.assert.Logf("CT: %v", job.Request().Header.Get("Content-Type")) + th.assert.Logf("HANDLER #%d: GET", th.index) switch { - case job.HasContentType(rest.ContentTypeJSON): - th.assert.Logf("CT: JSON") + case job.AcceptsContentType(rest.ContentTypeJSON): job.JSON(true).Write(rest.StatusOK, th.data(job.ResourceID())) - case job.HasContentType(rest.ContentTypeXML): - th.assert.Logf("CT: XML") + case job.AcceptsContentType(rest.ContentTypeXML): job.XML().Write(rest.StatusOK, th.data(job.ResourceID())) + case job.AcceptsContentType(rest.ContentTypeURLEncoded): + values := url.Values{} + values.Set("name", job.ResourceID()) + values.Set("index", fmt.Sprintf("%d", th.index)) + job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypeURLEncoded) + job.ResponseWriter().Write([]byte(values.Encode())) } return true, nil } func (th *TestHandler) Head(job rest.Job) (bool, error) { + th.assert.Logf("HANDLER #%d: HEAD", th.index) + job.ResponseWriter().Header().Set("Resource-Id", job.ResourceID()) + job.ResponseWriter().WriteHeader(rest.StatusOK) return true, nil } @@ -165,23 +210,22 @@ func (th *TestHandler) data(name string) *data { //-------------------- // newServers starts the server map for the requests. -func newServers(assert audit.Assertion) request.Servers { +func newServers(assert audit.Assertion, ports ...int) request.Servers { // Preparation. logger.SetLevel(logger.LevelDebug) cfgStr := "{etc {basepath /}{default-domain testing}{default-resource item}}" cfg, err := etc.ReadString(cfgStr) assert.Nil(err) - // addresses := []string{":12345", ":12346", ":12347", ":12348", ":12349"} - addresses := []string{":12345"} servers := request.NewServers() // Start and register each server. - for i, address := range addresses { + for i, port := range ports { mux := rest.NewMultiplexer(context.Background(), cfg) h := NewTestHandler(i, assert) err = mux.Register("testing", "item", h) assert.Nil(err) err = mux.Register("testing", "items", h) assert.Nil(err) + address := fmt.Sprintf(":%d", port) go func() { http.ListenAndServe(address, mux) }() From 526343c492521ed3a4933b7b861c5a2875875846 Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 2 Nov 2016 14:34:57 +0100 Subject: [PATCH 062/127] More request testing and added header to Formatter --- request/request_test.go | 101 +++++++++++++++++++++++++++++++--------- rest/formatter.go | 20 ++++++-- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/request/request_test.go b/request/request_test.go index d0bcb7c..d03d7f0 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -31,12 +31,6 @@ import ( // TESTS //-------------------- -// data is used for transfer data in the tests. -type data struct { - Index int - Name string -} - // tests defines requests and asserts. var tests = []struct { name string @@ -44,6 +38,7 @@ var tests = []struct { resource string id string params *request.Parameters + show bool check func(assert audit.Assertion, response *request.Response) }{ { @@ -55,8 +50,9 @@ var tests = []struct { Accept: rest.ContentTypeJSON, }, check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeJSON)) - var content data + content := Content{} err := response.Read(&content) assert.Nil(err) assert.Equal(content.Name, "foo") @@ -70,8 +66,9 @@ var tests = []struct { Accept: rest.ContentTypeXML, }, check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeXML)) - var content data + content := Content{} err := response.Read(&content) assert.Nil(err) assert.Equal(content.Name, "foo") @@ -85,6 +82,7 @@ var tests = []struct { Accept: rest.ContentTypeURLEncoded, }, check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeURLEncoded)) values := url.Values{} err := response.Read(values) @@ -92,13 +90,50 @@ var tests = []struct { assert.Equal(values["name"][0], "foo") }, }, { - name: "HEAD returning the resource ID as header", + name: "HEAD returns the resource ID as header", method: "HEAD", resource: "item", id: "foo", check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) assert.Equal(response.Header["Resource-Id"], "foo") }, + }, { + name: "PUT returns content based on sent content", + method: "PUT", + resource: "item", + id: "foo", + params: &request.Parameters{ + ContentType: rest.ContentTypeJSON, + Content: &Content{ + Version: 1, + }, + Accept: rest.ContentTypeJSON, + }, + check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) + assert.True(response.HasContentType(rest.ContentTypeJSON)) + content := Content{} + err := response.Read(&content) + assert.Nil(err) + assert.Equal(content.Version, 2) + assert.Equal(content.Name, "foo") + }, + }, { + name: "POST returns the location header based on sent content", + method: "POST", + resource: "items", + params: &request.Parameters{ + ContentType: rest.ContentTypeJSON, + Content: &Content{ + Version: 1, + Name: "bar", + }, + }, + check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusCreated) + assert.Equal(response.Header["Location"], "/testing/item/bar") + }, }, } @@ -129,6 +164,9 @@ func TestRequests(t *testing.T) { response, err = caller.Options(test.resource, test.id, test.params) } assert.Nil(err) + if test.show { + assert.Logf("response: %#v", response) + } test.check(assert, response) } } @@ -137,6 +175,14 @@ func TestRequests(t *testing.T) { // TEST HANDLER //-------------------- +// Content is used for the data transfer. +type Content struct { + Index int + Version int + Name string +} + +// TestHandler handles all the test requests. type TestHandler struct { index int assert audit.Assertion @@ -155,16 +201,22 @@ func (th *TestHandler) Init(env rest.Environment, domain, resource string) error } func (th *TestHandler) Get(job rest.Job) (bool, error) { - th.assert.Logf("HANDLER #%d: GET", th.index) + th.assert.Logf("handler #%d: GET", th.index) + createContent := func() *Content { + return &Content{ + Index: th.index, + Name: job.ResourceID(), + } + } switch { case job.AcceptsContentType(rest.ContentTypeJSON): - job.JSON(true).Write(rest.StatusOK, th.data(job.ResourceID())) + job.JSON(true).Write(rest.StatusOK, createContent()) case job.AcceptsContentType(rest.ContentTypeXML): - job.XML().Write(rest.StatusOK, th.data(job.ResourceID())) + job.XML().Write(rest.StatusOK, createContent()) case job.AcceptsContentType(rest.ContentTypeURLEncoded): values := url.Values{} - values.Set("name", job.ResourceID()) values.Set("index", fmt.Sprintf("%d", th.index)) + values.Set("name", job.ResourceID()) job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypeURLEncoded) job.ResponseWriter().Write([]byte(values.Encode())) } @@ -172,17 +224,31 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { } func (th *TestHandler) Head(job rest.Job) (bool, error) { - th.assert.Logf("HANDLER #%d: HEAD", th.index) + th.assert.Logf("handler #%d: HEAD", th.index) job.ResponseWriter().Header().Set("Resource-Id", job.ResourceID()) job.ResponseWriter().WriteHeader(rest.StatusOK) return true, nil } func (th *TestHandler) Put(job rest.Job) (bool, error) { + th.assert.Logf("handler #%d: PUT", th.index) + content := Content{} + err := job.JSON(true).Read(&content) + th.assert.Nil(err) + content.Version += 1 + content.Name = job.ResourceID() + job.JSON(true).Write(rest.StatusOK, content) return true, nil } func (th *TestHandler) Post(job rest.Job) (bool, error) { + th.assert.Logf("handler #%d: POST", th.index) + content := Content{} + err := job.JSON(true).Read(&content) + th.assert.Nil(err) + location := job.InternalPath(job.Domain(), "item", content.Name) + job.ResponseWriter().Header().Set("Location", location) + job.ResponseWriter().WriteHeader(rest.StatusCreated) return true, nil } @@ -198,13 +264,6 @@ func (th *TestHandler) Options(job rest.Job) (bool, error) { return true, nil } -func (th *TestHandler) data(name string) *data { - return &data{ - Index: th.index, - Name: name, - } -} - //-------------------- // SERVER //-------------------- diff --git a/rest/formatter.go b/rest/formatter.go index fd4c1a8..5266ded 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -76,8 +76,9 @@ type envelope struct { type Formatter interface { // Write encodes the passed data to implementers format and writes - // it with the passed status code to the response writer. - Write(status int, data interface{}) error + // it with the passed status code and possible header values to the + // response writer. + Write(status int, data interface{}, headers ...KeyValue) error // Read checks if the request content type matches the implementers // format, reads its body and decodes it to the value pointed to by @@ -107,8 +108,11 @@ type gobFormatter struct { } // Write is specified on the Formatter interface. -func (gf *gobFormatter) Write(status int, data interface{}) error { +func (gf *gobFormatter) Write(status int, data interface{}, headers ...KeyValue) error { enc := gob.NewEncoder(gf.job.ResponseWriter()) + for _, header := range headers { + gf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) + } gf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeGOB) gf.job.ResponseWriter().WriteHeader(status) err := enc.Encode(data) @@ -141,7 +145,7 @@ type jsonFormatter struct { } // Write is specified on the Formatter interface. -func (jf *jsonFormatter) Write(status int, data interface{}) error { +func (jf *jsonFormatter) Write(status int, data interface{}, headers ...KeyValue) error { body, err := json.Marshal(data) if err != nil { http.Error(jf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) @@ -152,6 +156,9 @@ func (jf *jsonFormatter) Write(status int, data interface{}) error { json.HTMLEscape(&buf, body) body = buf.Bytes() } + for _, header := range headers { + jf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) + } jf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeJSON) jf.job.ResponseWriter().WriteHeader(status) _, err = jf.job.ResponseWriter().Write(body) @@ -181,12 +188,15 @@ type xmlFormatter struct { } // Write is specified on the Formatter interface. -func (xf *xmlFormatter) Write(status int, data interface{}) error { +func (xf *xmlFormatter) Write(status int, data interface{}, headers ...KeyValue) error { body, err := xml.Marshal(data) if err != nil { http.Error(xf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) return err } + for _, header := range headers { + xf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) + } xf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeXML) xf.job.ResponseWriter().WriteHeader(status) _, err = xf.job.ResponseWriter().Write(body) From 1834656b72a4e87e3cec50713995cecf99435a25 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 2 Nov 2016 23:19:52 +0100 Subject: [PATCH 063/127] Completed testing of all methods --- request/doc.go | 2 +- request/request_test.go | 98 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/request/doc.go b/request/doc.go index bdf03ed..156cf50 100644 --- a/request/doc.go +++ b/request/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 6, 0, "alpha", "2016-10-30") + return version.New(2, 6, 0, "alpha", "2016-11-02") } // EOF diff --git a/request/request_test.go b/request/request_test.go index d03d7f0..58a6f0d 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -134,13 +134,67 @@ var tests = []struct { assert.Equal(response.Status, rest.StatusCreated) assert.Equal(response.Header["Location"], "/testing/item/bar") }, + }, { + name: "PATCH returns content and header based on sent content", + method: "PATCH", + resource: "item", + id: "bar", + params: &request.Parameters{ + ContentType: rest.ContentTypeJSON, + Content: &Content{ + Version: 1, + }, + Accept: rest.ContentTypeJSON, + }, + check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) + assert.Equal(response.Header["Resource-Id"], "bar") + assert.True(response.HasContentType(rest.ContentTypeJSON)) + content := Content{} + err := response.Read(&content) + assert.Nil(err) + assert.Equal(content.Version, 2) + assert.Equal(content.Name, "bar") + }, + }, { + name: "DELETE for one item, current data formatted in JSON", + method: "DELETE", + resource: "item", + id: "foo", + params: &request.Parameters{ + Accept: rest.ContentTypeJSON, + }, + check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) + assert.True(response.HasContentType(rest.ContentTypeJSON)) + content := Content{} + err := response.Read(&content) + assert.Nil(err) + assert.Equal(content.Version, 5) + assert.Equal(content.Name, "foo") + }, + }, { + name: "OPTIONS for one resource formatted in JSON", + method: "OPTIONS", + resource: "item", + params: &request.Parameters{ + Accept: rest.ContentTypeJSON, + }, + check: func(assert audit.Assertion, response *request.Response) { + assert.Equal(response.Status, rest.StatusOK) + assert.True(response.HasContentType(rest.ContentTypeJSON)) + options := Options{} + err := response.Read(&options) + assert.Nil(err) + assert.Equal(options.Methods, "GET, HEAD, PUT, POST, PATCH, DELETE") + }, }, } // TestRequests runs the different request tests. func TestRequests(t *testing.T) { assert := audit.NewTestingAssertion(t, true) - servers := newServers(assert, 12345, 12346) + servers := newServers(assert, 12345, 12346, 12346) // Run the tests. for i, test := range tests { assert.Logf("test #%d: %s", i, test.name) @@ -162,6 +216,8 @@ func TestRequests(t *testing.T) { response, err = caller.Delete(test.resource, test.id, test.params) case "OPTIONS": response, err = caller.Options(test.resource, test.id, test.params) + default: + assert.Fail("illegal method", test.method) } assert.Nil(err) if test.show { @@ -175,13 +231,18 @@ func TestRequests(t *testing.T) { // TEST HANDLER //-------------------- -// Content is used for the data transfer. +// Content is used for the data transfer of contents. type Content struct { Index int Version int Name string } +// Options is used for the data transfer of options. +type Options struct { + Methods string +} + // TestHandler handles all the test requests. type TestHandler struct { index int @@ -202,20 +263,20 @@ func (th *TestHandler) Init(env rest.Environment, domain, resource string) error func (th *TestHandler) Get(job rest.Job) (bool, error) { th.assert.Logf("handler #%d: GET", th.index) - createContent := func() *Content { - return &Content{ - Index: th.index, - Name: job.ResourceID(), - } + content := &Content{ + Index: th.index, + Version: 1, + Name: job.ResourceID(), } switch { case job.AcceptsContentType(rest.ContentTypeJSON): - job.JSON(true).Write(rest.StatusOK, createContent()) + job.JSON(true).Write(rest.StatusOK, content) case job.AcceptsContentType(rest.ContentTypeXML): - job.XML().Write(rest.StatusOK, createContent()) + job.XML().Write(rest.StatusOK, content) case job.AcceptsContentType(rest.ContentTypeURLEncoded): values := url.Values{} values.Set("index", fmt.Sprintf("%d", th.index)) + values.Set("version", "1") values.Set("name", job.ResourceID()) job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypeURLEncoded) job.ResponseWriter().Write([]byte(values.Encode())) @@ -253,14 +314,33 @@ func (th *TestHandler) Post(job rest.Job) (bool, error) { } func (th *TestHandler) Patch(job rest.Job) (bool, error) { + th.assert.Logf("handler #%d: PATCH", th.index) + content := Content{} + err := job.JSON(true).Read(&content) + th.assert.Nil(err) + content.Version += 1 + content.Name = job.ResourceID() + job.JSON(true).Write(rest.StatusOK, content, rest.KeyValue{"Resource-Id", job.ResourceID()}) return true, nil } func (th *TestHandler) Delete(job rest.Job) (bool, error) { + th.assert.Logf("handler #%d: DELETE", th.index) + content := &Content{ + Index: th.index, + Version: 5, + Name: job.ResourceID(), + } + job.JSON(true).Write(rest.StatusOK, content) return true, nil } func (th *TestHandler) Options(job rest.Job) (bool, error) { + th.assert.Logf("handler #%d: OPTIONS", th.index) + options := &Options{ + Methods: "GET, HEAD, PUT, POST, PATCH, DELETE", + } + job.JSON(true).Write(rest.StatusOK, options) return true, nil } From af1db9053a7224ddcc88dc60ff81eb85f42cf9e5 Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 3 Nov 2016 11:35:25 +0100 Subject: [PATCH 064/127] Changed request to interface --- request/request.go | 107 ++++++++++++++++++++++++---------------- request/request_test.go | 46 ++++++++--------- 2 files changed, 87 insertions(+), 66 deletions(-) diff --git a/request/request.go b/request/request.go index 12cb57d..fc0c22b 100644 --- a/request/request.go +++ b/request/request.go @@ -114,40 +114,65 @@ func FromContext(ctx context.Context) (Servers, bool) { type KeyValues map[string]string // Response wraps all infos of a test response. -type Response struct { - Status int - Header KeyValues - ContentType string - Content []byte +type Response interface { + // StatusCode returns the HTTP status code of the response. + StatusCode() int + + // Header returns the HTTP header of the response. + Header() http.Header + + // HasContentType checks the content type regardless of charsets. + HasContentType(contentType string) bool + + // Read decodes the content into the passed data depending + // on the content type. + Read(data interface{}) error +} + +// response implements Response. +type response struct { + statusCode int + header http.Header + contentType string + content []byte } -// HasContentType checks the content type regardless of charsets. -func (r *Response) HasContentType(contentType string) bool { - return strings.Contains(r.ContentType, contentType) +// StatusCode implements the Response interface. +func (r *response) StatusCode() int { + return r.statusCode } -// Read decodes the content into the passed data depending -// on the content type. -func (r *Response) Read(data interface{}) error { +// Header implements the Response interface. +func (r *response) Header() http.Header { + return r.header +} + +// HasContentType implements the Response interface. +func (r *response) HasContentType(contentType string) bool { + return strings.Contains(r.contentType, contentType) +} + +// Read implements the Response interface. +func (r *response) Read(data interface{}) error { switch { case r.HasContentType(rest.ContentTypeGOB): - dec := gob.NewDecoder(bytes.NewBuffer(r.Content)) + dec := gob.NewDecoder(bytes.NewBuffer(r.content)) if err := dec.Decode(data); err != nil { return errors.Annotate(err, ErrDecodingResponse, errorMessages) } return nil case r.HasContentType(rest.ContentTypeJSON): - if err := json.Unmarshal(r.Content, &data); err != nil { + if err := json.Unmarshal(r.content, &data); err != nil { return errors.Annotate(err, ErrDecodingResponse, errorMessages) } return nil case r.HasContentType(rest.ContentTypeXML): - if err := xml.Unmarshal(r.Content, &data); err != nil { + if err := xml.Unmarshal(r.content, &data); err != nil { return errors.Annotate(err, ErrDecodingResponse, errorMessages) } return nil case r.HasContentType(rest.ContentTypeURLEncoded): - values, err := url.ParseQuery(string(r.Content)) + values, err := url.ParseQuery(string(r.content)) if err != nil { return errors.Annotate(err, ErrDecodingResponse, errorMessages) } @@ -169,7 +194,7 @@ func (r *Response) Read(data interface{}) error { } return nil } - return errors.New(ErrInvalidContentType, errorMessages, r.ContentType) + return errors.New(ErrInvalidContentType, errorMessages, r.contentType) } //-------------------- @@ -253,25 +278,25 @@ func (p *Parameters) values() (url.Values, error) { // configured services. type Caller interface { // Get performs a GET request on the defined resource. - Get(resource, resourceID string, params *Parameters) (*Response, error) + Get(resource, resourceID string, params *Parameters) (Response, error) // Head performs a HEAD request on the defined resource. - Head(resource, resourceID string, params *Parameters) (*Response, error) + Head(resource, resourceID string, params *Parameters) (Response, error) // Put performs a PUT request on the defined resource. - Put(resource, resourceID string, params *Parameters) (*Response, error) + Put(resource, resourceID string, params *Parameters) (Response, error) // Post performs a POST request on the defined resource. - Post(resource, resourceID string, params *Parameters) (*Response, error) + Post(resource, resourceID string, params *Parameters) (Response, error) // Patch performs a PATCH request on the defined resource. - Patch(resource, resourceID string, params *Parameters) (*Response, error) + Patch(resource, resourceID string, params *Parameters) (Response, error) // Delete performs a DELETE request on the defined resource. - Delete(resource, resourceID string, params *Parameters) (*Response, error) + Delete(resource, resourceID string, params *Parameters) (Response, error) // Options performs a OPTIONS request on the defined resource. - Options(resource, resourceID string, params *Parameters) (*Response, error) + Options(resource, resourceID string, params *Parameters) (Response, error) } // caller implements the Caller interface. @@ -286,42 +311,42 @@ func newCaller(domain string, srvs []*server) Caller { } // Get implements the Caller interface. -func (c *caller) Get(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Get(resource, resourceID string, params *Parameters) (Response, error) { return c.request("GET", resource, resourceID, params) } // Head implements the Caller interface. -func (c *caller) Head(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Head(resource, resourceID string, params *Parameters) (Response, error) { return c.request("HEAD", resource, resourceID, params) } // Put implements the Caller interface. -func (c *caller) Put(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Put(resource, resourceID string, params *Parameters) (Response, error) { return c.request("PUT", resource, resourceID, params) } // Post implements the Caller interface. -func (c *caller) Post(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Post(resource, resourceID string, params *Parameters) (Response, error) { return c.request("POST", resource, resourceID, params) } // Patch implements the Caller interface. -func (c *caller) Patch(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Patch(resource, resourceID string, params *Parameters) (Response, error) { return c.request("PATCH", resource, resourceID, params) } // Delete implements the Caller interface. -func (c *caller) Delete(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Delete(resource, resourceID string, params *Parameters) (Response, error) { return c.request("DELETE", resource, resourceID, params) } // Options implements the Caller interface. -func (c *caller) Options(resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) Options(resource, resourceID string, params *Parameters) (Response, error) { return c.request("OPTIONS", resource, resourceID, params) } // request performs all requests. -func (c *caller) request(method, resource, resourceID string, params *Parameters) (*Response, error) { +func (c *caller) request(method, resource, resourceID string, params *Parameters) (Response, error) { if params == nil { params = &Parameters{} } @@ -387,21 +412,17 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters } // analyzeResponse creates a response struct out of the HTTP response. -func analyzeResponse(response *http.Response) (*Response, error) { - header := KeyValues{} - for key, values := range response.Header { - header[key] = strings.Join(values, ", ") - } - content, err := ioutil.ReadAll(response.Body) +func analyzeResponse(resp *http.Response) (Response, error) { + content, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, errors.Annotate(err, ErrAnalyzingResponse, errorMessages) } - response.Body.Close() - return &Response{ - Status: response.StatusCode, - Header: header, - ContentType: header["Content-Type"], - Content: content, + resp.Body.Close() + return &response{ + statusCode: resp.StatusCode, + header: resp.Header, + contentType: resp.Header.Get("Content-Type"), + content: content, }, nil } diff --git a/request/request_test.go b/request/request_test.go index 58a6f0d..0d97cca 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -39,7 +39,7 @@ var tests = []struct { id string params *request.Parameters show bool - check func(assert audit.Assertion, response *request.Response) + check func(assert audit.Assertion, response request.Response) }{ { name: "GET for one item formatted in JSON", @@ -49,8 +49,8 @@ var tests = []struct { params: &request.Parameters{ Accept: rest.ContentTypeJSON, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeJSON)) content := Content{} err := response.Read(&content) @@ -65,8 +65,8 @@ var tests = []struct { params: &request.Parameters{ Accept: rest.ContentTypeXML, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeXML)) content := Content{} err := response.Read(&content) @@ -81,8 +81,8 @@ var tests = []struct { params: &request.Parameters{ Accept: rest.ContentTypeURLEncoded, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeURLEncoded)) values := url.Values{} err := response.Read(values) @@ -94,9 +94,9 @@ var tests = []struct { method: "HEAD", resource: "item", id: "foo", - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) - assert.Equal(response.Header["Resource-Id"], "foo") + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) + assert.Equal(response.Header().Get("Resource-Id"), "foo") }, }, { name: "PUT returns content based on sent content", @@ -110,8 +110,8 @@ var tests = []struct { }, Accept: rest.ContentTypeJSON, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeJSON)) content := Content{} err := response.Read(&content) @@ -130,9 +130,9 @@ var tests = []struct { Name: "bar", }, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusCreated) - assert.Equal(response.Header["Location"], "/testing/item/bar") + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusCreated) + assert.Equal(response.Header().Get("Location"), "/testing/item/bar") }, }, { name: "PATCH returns content and header based on sent content", @@ -146,9 +146,9 @@ var tests = []struct { }, Accept: rest.ContentTypeJSON, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) - assert.Equal(response.Header["Resource-Id"], "bar") + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) + assert.Equal(response.Header().Get("Resource-Id"), "bar") assert.True(response.HasContentType(rest.ContentTypeJSON)) content := Content{} err := response.Read(&content) @@ -164,8 +164,8 @@ var tests = []struct { params: &request.Parameters{ Accept: rest.ContentTypeJSON, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeJSON)) content := Content{} err := response.Read(&content) @@ -180,8 +180,8 @@ var tests = []struct { params: &request.Parameters{ Accept: rest.ContentTypeJSON, }, - check: func(assert audit.Assertion, response *request.Response) { - assert.Equal(response.Status, rest.StatusOK) + check: func(assert audit.Assertion, response request.Response) { + assert.Equal(response.StatusCode(), rest.StatusOK) assert.True(response.HasContentType(rest.ContentTypeJSON)) options := Options{} err := response.Read(&options) @@ -200,7 +200,7 @@ func TestRequests(t *testing.T) { assert.Logf("test #%d: %s", i, test.name) caller, err := servers.Caller("testing") assert.Nil(err) - var response *request.Response + var response request.Response switch test.method { case "GET": response, err = caller.Get(test.resource, test.id, test.params) From 5dcb3594720c35899b0736e054565d5959fe383a Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 3 Nov 2016 15:33:00 +0100 Subject: [PATCH 065/127] Changed storage of HTTP request inside own request --- README.md | 2 +- request/doc.go | 2 +- request/request.go | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6fab4aa..267e487 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.5.2 +Version 2.6.0 ## Packages diff --git a/request/doc.go b/request/doc.go index 156cf50..e203e69 100644 --- a/request/doc.go +++ b/request/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 6, 0, "alpha", "2016-11-02") + return version.New(2, 6, 0, "alpha", "2016-11-03") } // EOF diff --git a/request/request.go b/request/request.go index fc0c22b..78bee0d 100644 --- a/request/request.go +++ b/request/request.go @@ -131,20 +131,19 @@ type Response interface { // response implements Response. type response struct { - statusCode int - header http.Header + httpResp *http.Response contentType string content []byte } // StatusCode implements the Response interface. func (r *response) StatusCode() int { - return r.statusCode + return r.httpResp.StatusCode } // Header implements the Response interface. func (r *response) Header() http.Header { - return r.header + return r.httpResp.Header } // HasContentType implements the Response interface. @@ -419,8 +418,7 @@ func analyzeResponse(resp *http.Response) (Response, error) { } resp.Body.Close() return &response{ - statusCode: resp.StatusCode, - header: resp.Header, + httpResp: resp, contentType: resp.Header.Get("Content-Type"), content: content, }, nil From 0013d4edc87718bd4db1a56a476f171fdc35bba8 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 3 Nov 2016 23:10:22 +0100 Subject: [PATCH 066/127] Added JWT to tests, prepared stuff for releasing --- CHANGELOG.md | 4 ++++ handlers/doc.go | 2 +- jwt/doc.go | 2 +- request/doc.go | 2 +- request/request_test.go | 26 +++++++++++++++++++++++--- rest/doc.go | 2 +- restaudit/doc.go | 2 +- 7 files changed, 32 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ac4c44..be2a093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-11-03 + +- Added *request* package for more convenient requests to REST APIs + ## 2016-10-25 - Fixed missing feedback after JWT authorization denial diff --git a/handlers/doc.go b/handlers/doc.go index 6ac9ce6..2126901 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the handlers package. func Version() version.Version { - return version.New(2, 5, 2) + return version.New(2, 6, 0) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index 41bed19..5c56a51 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the JSON Web Token package. func Version() version.Version { - return version.New(2, 5, 2) + return version.New(2, 6, 0) } // EOF diff --git a/request/doc.go b/request/doc.go index e203e69..e844bf0 100644 --- a/request/doc.go +++ b/request/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 6, 0, "alpha", "2016-11-03") + return version.New(2, 6, 0) } // EOF diff --git a/request/request_test.go b/request/request_test.go index 0d97cca..cc4856c 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -23,6 +23,7 @@ import ( "github.com/tideland/golib/etc" "github.com/tideland/golib/logger" + "github.com/tideland/gorest/jwt" "github.com/tideland/gorest/request" "github.com/tideland/gorest/rest" ) @@ -99,11 +100,12 @@ var tests = []struct { assert.Equal(response.Header().Get("Resource-Id"), "foo") }, }, { - name: "PUT returns content based on sent content", + name: "PUT returns content based on sent content, wants JWT", method: "PUT", resource: "item", id: "foo", params: &request.Parameters{ + Token: createToken(), ContentType: rest.ContentTypeJSON, Content: &Content{ Version: 1, @@ -293,8 +295,13 @@ func (th *TestHandler) Head(job rest.Job) (bool, error) { func (th *TestHandler) Put(job rest.Job) (bool, error) { th.assert.Logf("handler #%d: PUT", th.index) + token, err := jwt.DecodeFromJob(job) + th.assert.Nil(err) + name, ok := token.Claims().GetString("name") + th.assert.True(ok) + th.assert.Equal(name, "John Doe") content := Content{} - err := job.JSON(true).Read(&content) + err = job.JSON(true).Read(&content) th.assert.Nil(err) content.Version += 1 content.Name = job.ResourceID() @@ -345,7 +352,7 @@ func (th *TestHandler) Options(job rest.Job) (bool, error) { } //-------------------- -// SERVER +// HELPERS //-------------------- // newServers starts the server map for the requests. @@ -374,4 +381,17 @@ func newServers(assert audit.Assertion, ports ...int) request.Servers { return servers } +// createToken creates a test token. +func createToken() jwt.JWT { + claims := jwt.NewClaims() + claims.SetSubject("1234567890") + claims.Set("name", "John Doe") + claims.Set("admin", true) + token, err := jwt.Encode(claims, []byte("secret"), jwt.HS512) + if err != nil { + panic(err) + } + return token +} + // EOF diff --git a/rest/doc.go b/rest/doc.go index da69d71..f7dd2d3 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 5, 2) + return version.New(2, 6, 0) } // EOF diff --git a/restaudit/doc.go b/restaudit/doc.go index 03d3fb0..d92c2f4 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // Version returns the version of the REST Audit package. func Version() version.Version { - return version.New(2, 5, 2) + return version.New(2, 6, 0) } // EOF From 5a7167997f23fb84390036b4b10bef1e4d1fd8c8 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 3 Nov 2016 23:16:08 +0100 Subject: [PATCH 067/127] Quickly added link to documentation of new package --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 267e487..2a5e485 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ RESTful web request handling. [![GoDoc](https://godoc.org/github.com/tideland/gorest/rest?status.svg)](https://godoc.org/github.com/tideland/gorest/rest) +#### Request + +Convenient client requests to RESTful web services. + +[![GoDoc](https://godoc.org/github.com/tideland/gorest/request?status.svg)](https://godoc.org/github.com/tideland/gorest/request) + #### Handlers Some general purpose handlers for the library. From 370763d56e9e8629c6243704bbba2c9b758019bb Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 6 Nov 2016 22:23:52 +0100 Subject: [PATCH 068/127] Started change to derigiser multiple or all handlers at once --- rest/mapping.go | 38 +++++++++++++++++++++++--------------- rest/multiplexer.go | 9 +++++---- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/rest/mapping.go b/rest/mapping.go index a2bff39..7d5ac1d 100644 --- a/rest/mapping.go +++ b/rest/mapping.go @@ -56,22 +56,30 @@ func (hl *handlerList) register(handler ResourceHandler) error { } // deregister removes a resource handler. -func (hl *handlerList) deregister(id string) { - var head, tail *handlerListEntry - current := hl.head - for current != nil { - if current.handler.ID() != id { - if head == nil { - head = current - tail = current - } else { - tail.next = current - tail = tail.next +func (hl *handlerList) deregister(ids ...string) { + // Check if all shall be deregistered. + if len(ids) == 0 { + hl.head = nil + return + } + // No, so iterate over ids. + for _, id := range ids { + var head, tail *handlerListEntry + current := hl.head + for current != nil { + if current.handler.ID() != id { + if head == nil { + head = current + tail = current + } else { + tail.next = current + tail = tail.next + } } + current = current.next } - current = current.next + hl.head = head } - hl.head = head } // handle lets all resource handlers process the request. @@ -121,13 +129,13 @@ func (m *mapping) register(domain, resource string, handler ResourceHandler) err } // deregister removes a resource handler. -func (m *mapping) deregister(domain, resource string, id string) { +func (m *mapping) deregister(domain, resource string, ids ...string) { location := m.location(domain, resource) hl, ok := m.handlers[location] if !ok { return } - hl.deregister(id) + hl.deregister(ids...) if hl.head == nil { delete(m.handlers, location) } diff --git a/rest/multiplexer.go b/rest/multiplexer.go index 8130fad..02795d5 100644 --- a/rest/multiplexer.go +++ b/rest/multiplexer.go @@ -51,8 +51,9 @@ type Multiplexer interface { // RegisterAll allows to register multiple handler in one run. RegisterAll(registrations Registrations) error - // Deregister removes a resource handler for a given domain and resource. - Deregister(domain, resource, id string) + // Deregister removes one, more, or all resource handler for a + // given domain and resource. + Deregister(domain, resource string, ids ...string) } // multiplexer implements the Multiplexer interface. @@ -106,10 +107,10 @@ func (mux *multiplexer) RegisterAll(registrations Registrations) error { } // Deregister is specified on the Multiplexer interface. -func (mux *multiplexer) Deregister(domain, resource, id string) { +func (mux *multiplexer) Deregister(domain, resource string, ids ...string) { mux.mutex.Lock() defer mux.mutex.Unlock() - mux.mapping.deregister(domain, resource, id) + mux.mapping.deregister(domain, resource, ids...) } // ServeHTTP is specified on the http.Handler interface. From cb42c31deb8261ac078d581cddaada6ae4bbd908 Mon Sep 17 00:00:00 2001 From: themue Date: Mon, 7 Nov 2016 16:02:45 +0100 Subject: [PATCH 069/127] Improved handler info and deregistration --- CHANGELOG.md | 7 +++++++ README.md | 2 +- handlers/doc.go | 2 +- jwt/doc.go | 2 +- request/doc.go | 2 +- rest/doc.go | 2 +- rest/mapping.go | 21 +++++++++++++++++++++ rest/multiplexer.go | 19 +++++++++++++++---- rest/rest_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ restaudit/doc.go | 2 +- 10 files changed, 91 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2a093..48b4b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Tideland Go REST Server Library +## 2016-11-07 + +- Added *RegisteredHandlers()* to *Multiplexer* retrieve the list + of registered handlers for one domain and resource +- *Deregister()* is now more flexible in deristering multiple + or all handlers for one domain and resource at once + ## 2016-11-03 - Added *request* package for more convenient requests to REST APIs diff --git a/README.md b/README.md index 2a5e485..1403b7b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.6.0 +Version 2.7.0 ## Packages diff --git a/handlers/doc.go b/handlers/doc.go index 2126901..5a21222 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the handlers package. func Version() version.Version { - return version.New(2, 6, 0) + return version.New(2, 7, 0) } // EOF diff --git a/jwt/doc.go b/jwt/doc.go index 5c56a51..a9f927d 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the JSON Web Token package. func Version() version.Version { - return version.New(2, 6, 0) + return version.New(2, 7, 0) } // EOF diff --git a/request/doc.go b/request/doc.go index e844bf0..33d3429 100644 --- a/request/doc.go +++ b/request/doc.go @@ -23,7 +23,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 6, 0) + return version.New(2, 7, 0) } // EOF diff --git a/rest/doc.go b/rest/doc.go index f7dd2d3..17f455e 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 6, 0) + return version.New(2, 7, 0) } // EOF diff --git a/rest/mapping.go b/rest/mapping.go index 7d5ac1d..1805324 100644 --- a/rest/mapping.go +++ b/rest/mapping.go @@ -82,6 +82,17 @@ func (hl *handlerList) deregister(ids ...string) { } } +// ids returns the handler ids of this handler list. +func (hl *handlerList) ids() []string { + ids := []string{} + current := hl.head + for current != nil { + ids = append(ids, current.handler.ID()) + current = current.next + } + return ids +} + // handle lets all resource handlers process the request. func (hl *handlerList) handle(job Job) error { current := hl.head @@ -128,6 +139,16 @@ func (m *mapping) register(domain, resource string, handler ResourceHandler) err return hl.register(handler) } +// registeredHandlers returns the IDs of the registered resource handlers. +func (m *mapping) registeredHandlers(domain, resource string) []string { + location := m.location(domain, resource) + hl, ok := m.handlers[location] + if !ok { + return nil + } + return hl.ids() +} + // deregister removes a resource handler. func (m *mapping) deregister(domain, resource string, ids ...string) { location := m.location(domain, resource) diff --git a/rest/multiplexer.go b/rest/multiplexer.go index 02795d5..cdb37b7 100644 --- a/rest/multiplexer.go +++ b/rest/multiplexer.go @@ -51,6 +51,10 @@ type Multiplexer interface { // RegisterAll allows to register multiple handler in one run. RegisterAll(registrations Registrations) error + // RegisteredHandlers returns the ID stack of registered handlers + // for a domain and resource. + RegisteredHandlers(domain, resource string) []string + // Deregister removes one, more, or all resource handler for a // given domain and resource. Deregister(domain, resource string, ids ...string) @@ -84,7 +88,7 @@ func NewMultiplexer(ctx context.Context, cfg etc.Etc) Multiplexer { } } -// Register is specified on the Multiplexer interface. +// Register implements the Multiplexer interface. func (mux *multiplexer) Register(domain, resource string, handler ResourceHandler) error { mux.mutex.Lock() defer mux.mutex.Unlock() @@ -95,7 +99,7 @@ func (mux *multiplexer) Register(domain, resource string, handler ResourceHandle return mux.mapping.register(domain, resource, handler) } -// RegisterAll is specified on the Multiplexer interface. +// RegisterAll implements the Multiplexer interface. func (mux *multiplexer) RegisterAll(registrations Registrations) error { for _, registration := range registrations { err := mux.Register(registration.Domain, registration.Resource, registration.Handler) @@ -106,14 +110,21 @@ func (mux *multiplexer) RegisterAll(registrations Registrations) error { return nil } -// Deregister is specified on the Multiplexer interface. +// RegisteredHandlers implements the Multiplexer interface. +func (mux *multiplexer) RegisteredHandlers(domain, resource string) []string { + mux.mutex.Lock() + defer mux.mutex.Unlock() + return mux.mapping.registeredHandlers(domain, resource) +} + +// Deregister implements the Multiplexer interface. func (mux *multiplexer) Deregister(domain, resource string, ids ...string) { mux.mutex.Lock() defer mux.mutex.Unlock() mux.mapping.deregister(domain, resource, ids...) } -// ServeHTTP is specified on the http.Handler interface. +// ServeHTTP implements the http.Handler interface. func (mux *multiplexer) ServeHTTP(w http.ResponseWriter, r *http.Request) { mux.mutex.RLock() defer mux.mutex.RUnlock() diff --git a/rest/rest_test.go b/rest/rest_test.go index 293b6b3..9d7cad9 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -224,6 +224,48 @@ func TestHandlerStack(t *testing.T) { assert.Substring("
  • Resource: stack
  • ", string(resp.Body)) } +// TestDeregister tests the different possibilities to stop handlers. +func TestDeregister(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + // Setup the test server. + mux := newMultiplexer(assert) + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err := mux.RegisterAll(rest.Registrations{ + {"deregister", "single", NewTestHandler("s1", assert)}, + {"deregister", "pair", NewTestHandler("p1", assert)}, + {"deregister", "pair", NewTestHandler("p2", assert)}, + {"deregister", "group", NewTestHandler("g1", assert)}, + {"deregister", "group", NewTestHandler("g2", assert)}, + {"deregister", "group", NewTestHandler("g3", assert)}, + {"deregister", "group", NewTestHandler("g4", assert)}, + {"deregister", "group", NewTestHandler("g5", assert)}, + {"deregister", "group", NewTestHandler("g6", assert)}, + }) + assert.Nil(err) + // Perform tests. + assert.Equal(mux.RegisteredHandlers("deregister", "single"), []string{"s1"}) + assert.Equal(mux.RegisteredHandlers("deregister", "pair"), []string{"p1", "p2"}) + assert.Equal(mux.RegisteredHandlers("deregister", "group"), []string{"g1", "g2", "g3", "g4", "g5", "g6"}) + + mux.Deregister("deregister", "single", "s1") + assert.Nil(mux.RegisteredHandlers("deregister", "single")) + mux.Deregister("deregister", "single") + assert.Nil(mux.RegisteredHandlers("deregister", "single")) + + mux.Deregister("deregister", "pair") + assert.Nil(mux.RegisteredHandlers("deregister", "pair")) + + mux.Deregister("deregister", "group", "x99") + assert.Equal(mux.RegisteredHandlers("deregister", "group"), []string{"g1", "g2", "g3", "g4", "g5", "g6"}) + mux.Deregister("deregister", "group", "g5") + assert.Equal(mux.RegisteredHandlers("deregister", "group"), []string{"g1", "g2", "g3", "g4", "g6"}) + mux.Deregister("deregister", "group", "g4", "g2") + assert.Equal(mux.RegisteredHandlers("deregister", "group"), []string{"g1", "g3", "g6"}) + mux.Deregister("deregister", "group") + assert.Nil(mux.RegisteredHandlers("deregister", "group")) +} + // TestMethodNotSupported tests the handling of a not support HTTP method. func TestMethodNotSupported(t *testing.T) { assert := audit.NewTestingAssertion(t, true) diff --git a/restaudit/doc.go b/restaudit/doc.go index d92c2f4..3bfced5 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -24,7 +24,7 @@ import ( // Version returns the version of the REST Audit package. func Version() version.Version { - return version.New(2, 6, 0) + return version.New(2, 7, 0) } // EOF From cad95ac3d464663c98937d8c8b39205ea635274a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 12 Nov 2016 17:10:23 +0100 Subject: [PATCH 070/127] Started implementation of API versioning --- rest/doc.go | 2 +- rest/job.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/rest/doc.go b/rest/doc.go index 17f455e..56f8dc5 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -101,7 +101,7 @@ import ( // Version returns the version of the REST package. func Version() version.Version { - return version.New(2, 7, 0) + return version.New(2, 8, 0, "alpha", "2016-11-12") } // EOF diff --git a/rest/job.go b/rest/job.go index 6af4821..82a9e43 100644 --- a/rest/job.go +++ b/rest/job.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/tideland/golib/stringex" + "github.com/tideland/golib/version" ) //-------------------- @@ -62,6 +63,16 @@ type Job interface { // JWTAuthorizationHandler). EnhanceContext(func(ctx context.Context) context.Context) + // Version returns the requested API version for this job. If none + // is set the version 1.0.0 will be returned as default. It will + // be retrieved aut of the header Version. + Version() version.Version + + // SetVersion allows to set an API version for the response. If + // none is set the version 1.0.0 will be set as default. It will + // be set in the header Version. + SetVersion(v version.Version) + // AcceptsContentType checks if the requestor accepts a given content type. AcceptsContentType(contentType string) bool @@ -192,6 +203,16 @@ func (j *job) EnhanceContext(f func(ctx context.Context) context.Context) { j.ctx = f(ctx) } +// Version implements the Job interface. +func (j *job) Version() version.Version { + vstr := j.request.Header.Get("Version") + if vstr == "" { + return version.Version(1, 0, 0) + } + // TODO Mue 2016-11-12 Version package needs parse of strings. + return version.Version(1, 0, 0) +} + // AcceptsContentType implements the Job interface. func (j *job) AcceptsContentType(contentType string) bool { return strings.Contains(j.request.Header.Get("Accept"), contentType) From 536f9164e9d9cfb89d8cb78defc112a50138444f Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 12 Nov 2016 23:34:27 +0100 Subject: [PATCH 071/127] Version almost done, missing parsing and tests --- rest/formatter.go | 3 +++ rest/job.go | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/rest/formatter.go b/rest/formatter.go index 5266ded..3913ab4 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -114,6 +114,7 @@ func (gf *gobFormatter) Write(status int, data interface{}, headers ...KeyValue) gf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } gf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeGOB) + gf.job.ResponseWriter().Header().Set("Version", gf.job.version.String()) gf.job.ResponseWriter().WriteHeader(status) err := enc.Encode(data) if err != nil { @@ -160,6 +161,7 @@ func (jf *jsonFormatter) Write(status int, data interface{}, headers ...KeyValue jf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } jf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeJSON) + jf.job.ResponseWriter().Header().Set("Version", jf.job.version.String()) jf.job.ResponseWriter().WriteHeader(status) _, err = jf.job.ResponseWriter().Write(body) return err @@ -198,6 +200,7 @@ func (xf *xmlFormatter) Write(status int, data interface{}, headers ...KeyValue) xf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } xf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeXML) + xf.job.ResponseWriter().Header().Set("Version", xf.job.version.String()) xf.job.ResponseWriter().WriteHeader(status) _, err = xf.job.ResponseWriter().Write(body) return err diff --git a/rest/job.go b/rest/job.go index 82a9e43..0d2a0f0 100644 --- a/rest/job.go +++ b/rest/job.go @@ -110,6 +110,7 @@ type job struct { ctx context.Context request *http.Request responseWriter http.ResponseWriter + version version.Version domain string resource string resourceID string @@ -149,6 +150,13 @@ func newJob(env *environment, r *http.Request, rw http.ResponseWriter) Job { j.resource = parts[1] j.domain = parts[0] } + // Retrieve the requested version of the API. + vsnstr := j.request.Header.Get("Version") + if vsnstr == "" { + j.version = version.Version(1, 0, 0) + } else { + // TODO Mue 2016-11-12 Version package needs parse of strings. + } return j } @@ -205,12 +213,14 @@ func (j *job) EnhanceContext(f func(ctx context.Context) context.Context) { // Version implements the Job interface. func (j *job) Version() version.Version { - vstr := j.request.Header.Get("Version") - if vstr == "" { - return version.Version(1, 0, 0) + return j.version +} + +// SerVersion implements the Job interface. +func (j.job) SetVersion(vsn version.Version) { + if vsn != nil { + j.version = vsn } - // TODO Mue 2016-11-12 Version package needs parse of strings. - return version.Version(1, 0, 0) } // AcceptsContentType implements the Job interface. From 2ca7f119fe7b0528866b14264d11756311c79dcc Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 15 Nov 2016 16:42:40 +0100 Subject: [PATCH 072/127] Added verson handling Also removed package version in doc files, they are useless. --- handlers/doc.go | 17 ----------------- jwt/doc.go | 17 ----------------- request/request.go | 5 +++++ rest/doc.go | 17 ----------------- rest/formatter.go | 6 +++--- rest/job.go | 13 ++++++++++--- rest/rest_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ restaudit/doc.go | 17 ----------------- 8 files changed, 65 insertions(+), 74 deletions(-) diff --git a/handlers/doc.go b/handlers/doc.go index 5a21222..96ab2cc 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -9,21 +9,4 @@ // some initial resource handlers to integrate into own solutions. package handlers -//-------------------- -// IMPORTS -//-------------------- - -import ( - "github.com/tideland/golib/version" -) - -//-------------------- -// VERSION -//-------------------- - -// Version returns the version of the handlers package. -func Version() version.Version { - return version.New(2, 7, 0) -} - // EOF diff --git a/jwt/doc.go b/jwt/doc.go index a9f927d..881d1d2 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -9,21 +9,4 @@ // verification, and analyzing of JSON Web Tokens. package jwt -//-------------------- -// IMPORTS -//-------------------- - -import ( - "github.com/tideland/golib/version" -) - -//-------------------- -// VERSION -//-------------------- - -// Version returns the version of the JSON Web Token package. -func Version() version.Version { - return version.New(2, 7, 0) -} - // EOF diff --git a/request/request.go b/request/request.go index 78bee0d..4136c8f 100644 --- a/request/request.go +++ b/request/request.go @@ -27,6 +27,7 @@ import ( "time" "github.com/tideland/golib/errors" + "github.com/tideland/golib/version" "github.com/tideland/gorest/jwt" "github.com/tideland/gorest/rest" @@ -202,6 +203,7 @@ func (r *response) Read(data interface{}) error { // Parameters allows to pass parameters to a call. type Parameters struct { + Version version.Version Token jwt.JWT ContentType string Content interface{} @@ -392,6 +394,9 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters } request.Header.Set("Content-Type", params.ContentType) } + if params.Version != nil { + request.Header.Set("Version", params.Version.String()) + } if params.Token != nil { request = jwt.AddTokenToRequest(request, params.Token) } diff --git a/rest/doc.go b/rest/doc.go index 56f8dc5..efa6155 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -87,21 +87,4 @@ // removed during the runtime. package rest -//-------------------- -// IMPORTS -//-------------------- - -import ( - "github.com/tideland/golib/version" -) - -//-------------------- -// VERSION -//-------------------- - -// Version returns the version of the REST package. -func Version() version.Version { - return version.New(2, 8, 0, "alpha", "2016-11-12") -} - // EOF diff --git a/rest/formatter.go b/rest/formatter.go index 3913ab4..ddb7ce9 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -114,7 +114,7 @@ func (gf *gobFormatter) Write(status int, data interface{}, headers ...KeyValue) gf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } gf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeGOB) - gf.job.ResponseWriter().Header().Set("Version", gf.job.version.String()) + gf.job.ResponseWriter().Header().Set("Version", gf.job.Version().String()) gf.job.ResponseWriter().WriteHeader(status) err := enc.Encode(data) if err != nil { @@ -161,7 +161,7 @@ func (jf *jsonFormatter) Write(status int, data interface{}, headers ...KeyValue jf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } jf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeJSON) - jf.job.ResponseWriter().Header().Set("Version", jf.job.version.String()) + jf.job.ResponseWriter().Header().Set("Version", jf.job.Version().String()) jf.job.ResponseWriter().WriteHeader(status) _, err = jf.job.ResponseWriter().Write(body) return err @@ -200,7 +200,7 @@ func (xf *xmlFormatter) Write(status int, data interface{}, headers ...KeyValue) xf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } xf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeXML) - xf.job.ResponseWriter().Header().Set("Version", xf.job.version.String()) + xf.job.ResponseWriter().Header().Set("Version", xf.job.Version().String()) xf.job.ResponseWriter().WriteHeader(status) _, err = xf.job.ResponseWriter().Write(body) return err diff --git a/rest/job.go b/rest/job.go index 0d2a0f0..288c4fc 100644 --- a/rest/job.go +++ b/rest/job.go @@ -19,6 +19,7 @@ import ( "strconv" "strings" + "github.com/tideland/golib/logger" "github.com/tideland/golib/stringex" "github.com/tideland/golib/version" ) @@ -153,9 +154,15 @@ func newJob(env *environment, r *http.Request, rw http.ResponseWriter) Job { // Retrieve the requested version of the API. vsnstr := j.request.Header.Get("Version") if vsnstr == "" { - j.version = version.Version(1, 0, 0) + j.version = version.New(1, 0, 0) } else { - // TODO Mue 2016-11-12 Version package needs parse of strings. + vsn, err := version.Parse(vsnstr) + if err != nil { + logger.Errorf("invalid request version: %v", err) + j.version = version.New(1, 0, 0) + } else { + j.version = vsn + } } return j } @@ -217,7 +224,7 @@ func (j *job) Version() version.Version { } // SerVersion implements the Job interface. -func (j.job) SetVersion(vsn version.Version) { +func (j *job) SetVersion(vsn version.Version) { if vsn != nil { j.version = vsn } diff --git a/rest/rest_test.go b/rest/rest_test.go index 9d7cad9..505d85e 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -22,6 +22,7 @@ import ( "github.com/tideland/golib/audit" "github.com/tideland/golib/etc" "github.com/tideland/golib/logger" + "github.com/tideland/golib/version" "github.com/tideland/gorest/rest" "github.com/tideland/gorest/restaudit" @@ -224,6 +225,47 @@ func TestHandlerStack(t *testing.T) { assert.Substring("
  • Resource: stack
  • ", string(resp.Body)) } +// TestVersion tests request and response version. +func TestVersion(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + // Setup the test server. + mux := newMultiplexer(assert) + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err := mux.Register("test", "json", NewTestHandler("json", assert)) + assert.Nil(err) + // Perform test requests. + resp := ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/base/test/json/4711?foo=0815", + Header: restaudit.KeyValues{ + "Accept": "application/json", + }, + }) + vsn := resp.Header["Version"] + assert.Equal(vsn, "1.0.0") + resp = ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/base/test/json/4711?foo=0815", + Header: restaudit.KeyValues{ + "Accept": "application/json", + "Version": "2", + }, + }) + vsn = resp.Header["Version"] + assert.Equal(vsn, "2.0.0") + resp = ts.DoRequest(&restaudit.Request{ + Method: "GET", + Path: "/base/test/json/4711?foo=0815", + Header: restaudit.KeyValues{ + "Accept": "application/json", + "Version": "3.0", + }, + }) + vsn = resp.Header["Version"] + assert.Equal(vsn, "4.0.0-alpha") +} + // TestDeregister tests the different possibilities to stop handlers. func TestDeregister(t *testing.T) { assert := audit.NewTestingAssertion(t, true) @@ -381,7 +423,12 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { } ctxTest := job.Context().Value("test") query := job.Query().ValueAsString("foo", "bar") + precedence, _ := job.Version().Compare(version.New(3, 0, 0)) + // Create response. data := TestRequestData{job.Domain(), job.Resource(), job.ResourceID(), query, ctxTest.(string)} + if precedence == version.Equal { + job.SetVersion(version.New(4, 0, 0, "alpha")) + } switch { case job.AcceptsContentType(rest.ContentTypeXML): th.assert.Logf("GET XML") diff --git a/restaudit/doc.go b/restaudit/doc.go index 3bfced5..69140b1 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -10,21 +10,4 @@ // resource handlers. package restaudit -//-------------------- -// IMPORTS -//-------------------- - -import ( - "github.com/tideland/golib/version" -) - -//-------------------- -// VERSION -//-------------------- - -// Version returns the version of the REST Audit package. -func Version() version.Version { - return version.New(2, 7, 0) -} - // EOF From d4efe13f39c2fe11077b850573c602bb74458607 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 15 Nov 2016 16:46:02 +0100 Subject: [PATCH 073/127] Changed forgotten version number in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1403b7b..3c96119 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.7.0 +Version 2.8.0 ## Packages From 391251e82e28d2fa9ca3efa51545eda0191b96eb Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 23 Nov 2016 17:00:49 +0100 Subject: [PATCH 074/127] Added JWTFromContext() --- CHANGELOG.md | 4 ++++ README.md | 2 +- handlers/jwtauth.go | 10 ++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48b4b00..e3bab13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-11-23 + +- Added *JWTFromContext()* to *handlers* + ## 2016-11-07 - Added *RegisteredHandlers()* to *Multiplexer* retrieve the list diff --git a/README.md b/README.md index 3c96119..d65027f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.8.0 +Version 2.8.1 ## Packages diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 3c5e4a2..8648cef 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -185,12 +185,18 @@ func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { } } +// JWTFromContext retrieves a JWT out of a context, when a JWTAuthorizationHandler +// earlier in the queue of handlers successfully received and checked one. +func JWTFromContext(ctx context.Context) (jwt.JWT, bool) { + jobJWT, ok := ctx.Value(jwtKey).(jwt.JWT) + return jobJWT, ok +} + // JWTFromJob retrieves a JWT out of the context of a job, when a // JWTAuthorizationHandler earlier in the queue of handlers successfully // received and checked one. func JWTFromJob(job rest.Job) (jwt.JWT, bool) { - jobJWT, ok := job.Context().Value(jwtKey).(jwt.JWT) - return jobJWT, ok + return JWTFromContext(job.Context()) } // EOF From 70262777bf1a561136b9ef6de25ef45a055b17d2 Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 23 Nov 2016 17:01:29 +0100 Subject: [PATCH 075/127] Changed wrong semantic number --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d65027f..5188124 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ I hope you like it. ;) ## Version -Version 2.8.1 +Version 2.9.0 ## Packages From c8cb857e20bfd50d2e694bdfc09b129ef2532291 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 23 Nov 2016 23:12:36 +0100 Subject: [PATCH 076/127] Improved context handling for JWT --- CHANGELOG.md | 2 ++ README.md | 5 +++-- handlers/handlers_test.go | 6 +++--- handlers/jwtauth.go | 38 +++++++++----------------------------- jwt/jwt.go | 20 +++++++++++++++++++- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3bab13..727327f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 2016-11-23 - Added *JWTFromContext()* to *handlers* +- Later removed JWT context from *handler*; now *jwt* package + has *NewContext()* and *FromContext()* as usual ## 2016-11-07 diff --git a/README.md b/README.md index 5188124..dd0b0f1 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ systems. It provides a convenient mapping of URL to handlers and methods specific to the called HTTP method. Additionally there are helpers for marshalling and unmarshalling. -The library earlier has been known as `web` package of the Tideland Go Library. +The library earlier has been known as `web` package of the +[Tideland Go Library](https://github.com/tideland/golib). I hope you like it. ;) ## Version -Version 2.9.0 +Version 2.9.1 ## Packages diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index fc164db..8856721 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -144,10 +144,10 @@ func TestJWTAuthorizationHandler(t *testing.T) { }, status: 200, auditf: func(assert audit.Assertion, job rest.Job) (bool, error) { - auditJWT, ok := handlers.JWTFromJob(job) + token, ok := jwt.FromContext(job.Context()) assert.True(ok) - assert.NotNil(auditJWT) - subject, ok := auditJWT.Claims().Subject() + assert.NotNil(token) + subject, ok := token.Claims().Subject() assert.True(ok) assert.Equal(subject, "test") return true, nil diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 8648cef..317ca58 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -25,12 +25,6 @@ import ( // JWT AUTHORIZATION HANDLER //-------------------- -// key for the storage of values in a context. -type key int - -// jwtKey for the storrage of a JWT. -var jwtKey key = 0 - // JWTAuthorizationConfig allows to control how the JWT authorization // handler works. All values are optional. In this case tokens are only // decoded without using a cache, validated for the current time plus/minus @@ -134,37 +128,37 @@ func (h *jwtAuthorizationHandler) Options(job rest.Job) (bool, error) { // check is used by all methods to check the token. func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { - var jobJWT jwt.JWT + var token jwt.JWT var err error switch { case h.cache != nil && h.key != nil: - jobJWT, err = jwt.VerifyCachedFromJob(job, h.cache, h.key) + token, err = jwt.VerifyCachedFromJob(job, h.cache, h.key) case h.cache != nil && h.key == nil: - jobJWT, err = jwt.DecodeCachedFromJob(job, h.cache) + token, err = jwt.DecodeCachedFromJob(job, h.cache) case h.cache == nil && h.key != nil: - jobJWT, err = jwt.VerifyFromJob(job, h.key) + token, err = jwt.VerifyFromJob(job, h.key) default: - jobJWT, err = jwt.DecodeFromJob(job) + token, err = jwt.DecodeFromJob(job) } // Now do the checks. if err != nil { return false, h.deny(job, err.Error()) } - if jobJWT == nil { + if token == nil { return false, h.deny(job, "no JSON Web Token") } - if !jobJWT.IsValid(h.leeway) { + if !token.IsValid(h.leeway) { return false, h.deny(job, "JSON Web Token claims 'nbf' and/or 'exp' are not valid") } if h.gatekeeper != nil { - err := h.gatekeeper(job, jobJWT.Claims()) + err := h.gatekeeper(job, token.Claims()) if err != nil { return false, h.deny(job, "access rejected by gatekeeper: "+err.Error()) } } // All fine, store token in context. job.EnhanceContext(func(ctx context.Context) context.Context { - return context.WithValue(ctx, jwtKey, jobJWT) + return jwt.NewContext(ctx, token) }) return true, nil } @@ -185,18 +179,4 @@ func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { } } -// JWTFromContext retrieves a JWT out of a context, when a JWTAuthorizationHandler -// earlier in the queue of handlers successfully received and checked one. -func JWTFromContext(ctx context.Context) (jwt.JWT, bool) { - jobJWT, ok := ctx.Value(jwtKey).(jwt.JWT) - return jobJWT, ok -} - -// JWTFromJob retrieves a JWT out of the context of a job, when a -// JWTAuthorizationHandler earlier in the queue of handlers successfully -// received and checked one. -func JWTFromJob(job rest.Job) (jwt.JWT, bool) { - return JWTFromContext(job.Context()) -} - // EOF diff --git a/jwt/jwt.go b/jwt/jwt.go index 4549a9d..c696ce1 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -12,6 +12,7 @@ package jwt //-------------------- import ( + "context" "encoding/base64" "encoding/json" "fmt" @@ -22,9 +23,26 @@ import ( ) //-------------------- -// CONST +// CONTEXT //-------------------- +// key for the storage of values in a context. +type key int + +// jwtKey for the storrage of a JWT. +var jwtKey key = 0 + +// NewContext returns a new context that carries a token. +func NewContext(ctx context.Context, token JWT) context.Context { + return context.WithValue(ctx, jwtKey, token) +} + +// FromContext returns the token stored in ctx, if any. +func FromContext(ctx context.Context) (JWT, bool) { + token, ok := ctx.Value(jwtKey).(JWT) + return token, ok +} + //-------------------- // JSON Web Token //-------------------- From 3d95b92c03b75422698f5a5a32de20075aa288e4 Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 1 Dec 2016 12:59:40 +0100 Subject: [PATCH 077/127] Added status code for internal server error --- rest/formatter.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rest/formatter.go b/rest/formatter.go index ddb7ce9..f487cff 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -32,13 +32,14 @@ import ( const ( // Standard REST status codes. - StatusOK = http.StatusOK - StatusCreated = http.StatusCreated - StatusNoContent = http.StatusNoContent - StatusBadRequest = http.StatusBadRequest - StatusUnauthorized = http.StatusUnauthorized - StatusNotFound = http.StatusNotFound - StatusConflict = http.StatusConflict + StatusOK = http.StatusOK + StatusCreated = http.StatusCreated + StatusNoContent = http.StatusNoContent + StatusBadRequest = http.StatusBadRequest + StatusUnauthorized = http.StatusUnauthorized + StatusNotFound = http.StatusNotFound + StatusConflict = http.StatusConflict + StatusInternalServerError = http.StatusInternalServerError // Standard REST content types. ContentTypePlain = "text/plain" From 9f13d794c1e94ef4f940e8a75f5cdce4cb40b7fa Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 1 Dec 2016 21:14:27 +0100 Subject: [PATCH 078/127] Changed doc --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 727327f..09addb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-12-01 + +- Added missing status code + ## 2016-11-23 - Added *JWTFromContext()* to *handlers* diff --git a/README.md b/README.md index dd0b0f1..ffb4576 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.9.1 +Version 2.9.2 ## Packages From 17c6ee863d0e98471a4dd62016c339eead93d4cb Mon Sep 17 00:00:00 2001 From: themue Date: Fri, 2 Dec 2016 12:50:42 +0100 Subject: [PATCH 079/127] Negative feedback is now logged as warning --- rest/formatter.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest/formatter.go b/rest/formatter.go index f487cff..c3192de 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -23,6 +23,7 @@ import ( "time" "github.com/tideland/golib/errors" + "github.com/tideland/golib/logger" "github.com/tideland/golib/stringex" ) @@ -96,6 +97,7 @@ func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...inte // NegativeFeedback writes a negative feedback envelope to the formatter. func NegativeFeedback(f Formatter, status int, msg string, args ...interface{}) error { fmsg := fmt.Sprintf(msg, args...) + logger.Warningf(fmsg) return f.Write(status, envelope{"fail", fmsg, nil}) } From 45747231e22868f5a80469f451e30f51e40f4afe Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 2 Dec 2016 20:04:17 +0100 Subject: [PATCH 080/127] Added last changes to docs --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09addb9..8084159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Tideland Go REST Server Library +## 2016-12-02 + +- Added logging to negative responses + ## 2016-12-01 - Added missing status code diff --git a/README.md b/README.md index ffb4576..8828337 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.9.2 +Version 2.9.3 ## Packages From 53241e48717d27f2334617d79801ce5bb9526279 Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 6 Dec 2016 15:28:23 +0100 Subject: [PATCH 081/127] Positive and negative feedback now return also false This way they can be used as final returns in handler methods. --- handlers/jwtauth.go | 12 ++++++------ rest/formatter.go | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 317ca58..5fdcab1 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -142,18 +142,18 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { } // Now do the checks. if err != nil { - return false, h.deny(job, err.Error()) + return h.deny(job, err.Error()) } if token == nil { - return false, h.deny(job, "no JSON Web Token") + return h.deny(job, "no JSON Web Token") } if !token.IsValid(h.leeway) { - return false, h.deny(job, "JSON Web Token claims 'nbf' and/or 'exp' are not valid") + return h.deny(job, "JSON Web Token claims 'nbf' and/or 'exp' are not valid") } if h.gatekeeper != nil { err := h.gatekeeper(job, token.Claims()) if err != nil { - return false, h.deny(job, "access rejected by gatekeeper: "+err.Error()) + return h.deny(job, "access rejected by gatekeeper: "+err.Error()) } } // All fine, store token in context. @@ -164,7 +164,7 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { } // deny sends a negative feedback to the caller. -func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { +func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) (bool, error) { h.logger(job, msg) switch { case job.AcceptsContentType(rest.ContentTypeJSON): @@ -175,7 +175,7 @@ func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) error { job.ResponseWriter().WriteHeader(rest.StatusUnauthorized) job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypePlain) job.ResponseWriter().Write([]byte(msg)) - return nil + return false, nil } } diff --git a/rest/formatter.go b/rest/formatter.go index c3192de..11a292a 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -89,16 +89,16 @@ type Formatter interface { } // PositiveFeedback writes a positive feedback envelope to the formatter. -func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) error { +func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) (bool, error) { fmsg := fmt.Sprintf(msg, args...) - return f.Write(StatusOK, envelope{"success", fmsg, payload}) + return false, f.Write(StatusOK, envelope{"success", fmsg, payload}) } // NegativeFeedback writes a negative feedback envelope to the formatter. -func NegativeFeedback(f Formatter, status int, msg string, args ...interface{}) error { +func NegativeFeedback(f Formatter, status int, msg string, args ...interface{}) (bool, error) { fmsg := fmt.Sprintf(msg, args...) logger.Warningf(fmsg) - return f.Write(status, envelope{"fail", fmsg, nil}) + return false, f.Write(status, envelope{"fail", fmsg, nil}) } //-------------------- From 04e6cee45609d53272fec433c6a7788791be04ef Mon Sep 17 00:00:00 2001 From: themue Date: Tue, 6 Dec 2016 15:31:48 +0100 Subject: [PATCH 082/127] Adopted latest changes in docs --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8084159..11447c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Tideland Go REST Server Library +## 2016-12-06 + +- *PositiveFeedback()* and *NegativeFeedback()* now also return + false to be directly used as final return in handler methods + ## 2016-12-02 - Added logging to negative responses diff --git a/README.md b/README.md index 8828337..2e28d58 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.9.3 +Version 2.9.4 ## Packages From f7df85fd6fccdbe8d68f8c9c95bdee4f9bc28d54 Mon Sep 17 00:00:00 2001 From: themue Date: Fri, 9 Dec 2016 13:12:26 +0100 Subject: [PATCH 083/127] More clear filename handling in FileServeHandler --- CHANGELOG.md | 5 +++++ handlers/errors.go | 4 +++- handlers/fileserve.go | 11 ++++++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11447c1..adf9f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Tideland Go REST Server Library +## 2016-12-09 + +- *FileServeHandler* now logs the absolute filename and logs + error if the name is invalid + ## 2016-12-06 - *PositiveFeedback()* and *NegativeFeedback()* now also return diff --git a/handlers/errors.go b/handlers/errors.go index 043cd84..4e9fb51 100644 --- a/handlers/errors.go +++ b/handlers/errors.go @@ -21,10 +21,12 @@ import ( const ( ErrUploadingFile = iota + 1 + ErrDownloadingFile ) var errorMessages = errors.Messages{ - ErrUploadingFile: "uploaded file cannot be handled by %q", + ErrUploadingFile: "uploaded file cannot be handled by '%s'", + ErrDownloadingFile: "file '%s' cannot be downloaded", } // EOF diff --git a/handlers/fileserve.go b/handlers/fileserve.go index b22b815..39bce5e 100644 --- a/handlers/fileserve.go +++ b/handlers/fileserve.go @@ -16,13 +16,14 @@ import ( "path/filepath" "strings" + "github.com/tideland/golib/errors" "github.com/tideland/golib/logger" "github.com/tideland/gorest/rest" ) //-------------------- -// FILE SERVER HANDLER +// FILE SERVE HANDLER //-------------------- // fileServeHandler implements the file server. @@ -53,8 +54,12 @@ func (h *fileServeHandler) Init(env rest.Environment, domain, resource string) e // Get is specified on the GetResourceHandler interface. func (h *fileServeHandler) Get(job rest.Job) (bool, error) { - filename := h.dir + job.ResourceID() - logger.Infof("serving file %q", filename) + filename, err := filepath.Abs(filepath.Clean(h.dir + job.ResourceID())) + if err != nil { + logger.Errorf("file '%s' does not exist", filename) + return false, errors.Annotate(err, ErrDownloadingFile, errorMessages, filename) + } + logger.Infof("serving file '%s'", filename) http.ServeFile(job.ResponseWriter(), job.Request(), filename) return true, nil } From 9b82b89c7cc442d6d1b71c550238beb2137a5ad7 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 11 Dec 2016 21:48:50 +0100 Subject: [PATCH 084/127] Changed doc after file server handler change --- CHANGELOG.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf9f2c..5ea7ad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Tideland Go REST Server Library -## 2016-12-09 +## 2016-12-11 - *FileServeHandler* now logs the absolute filename and logs error if the name is invalid diff --git a/README.md b/README.md index 2e28d58..81aa3bf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.9.4 +Version 2.9.5 ## Packages From fa2649a93a4aae8efd8af77da3c5ea112f9a819e Mon Sep 17 00:00:00 2001 From: themue Date: Wed, 14 Dec 2016 16:23:33 +0100 Subject: [PATCH 085/127] Added status code to positive and negative feedback --- rest/formatter.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/rest/formatter.go b/rest/formatter.go index 11a292a..c1f3ab5 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -64,12 +64,13 @@ var ( //-------------------- // envelope is a helper to give a qualified feedback in RESTful requests. -// It contains wether the request has been successful, in case of an -// error an additional message and the payload. +// It contains wether the request has been successful, a message, and in +// case of success some payload if wanted. type envelope struct { - Status string `json:"status" xml:"status"` - Message string `json:"message,omitempty" xml:"message,omitempty"` - Payload interface{} `json:"payload,omitempty" xml:"payload,omitempty"` + StatusCode int `json:"statusCode" xml:"statusCode"` + Status string `json:"status" xml:"status"` + Message string `json:"message,omitempty" xml:"message,omitempty"` + Payload interface{} `json:"payload,omitempty" xml:"payload,omitempty"` } //-------------------- @@ -80,7 +81,7 @@ type Formatter interface { // Write encodes the passed data to implementers format and writes // it with the passed status code and possible header values to the // response writer. - Write(status int, data interface{}, headers ...KeyValue) error + Write(statusCode int, data interface{}, headers ...KeyValue) error // Read checks if the request content type matches the implementers // format, reads its body and decodes it to the value pointed to by @@ -91,14 +92,16 @@ type Formatter interface { // PositiveFeedback writes a positive feedback envelope to the formatter. func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) (bool, error) { fmsg := fmt.Sprintf(msg, args...) - return false, f.Write(StatusOK, envelope{"success", fmsg, payload}) + return false, f.Write(StatusOK, envelope{StatusOK, "success", fmsg, payload}) } // NegativeFeedback writes a negative feedback envelope to the formatter. -func NegativeFeedback(f Formatter, status int, msg string, args ...interface{}) (bool, error) { +// The message is also logged. +func NegativeFeedback(f Formatter, statusCode int, msg string, args ...interface{}) (bool, error) { fmsg := fmt.Sprintf(msg, args...) - logger.Warningf(fmsg) - return false, f.Write(status, envelope{"fail", fmsg, nil}) + lmsg := fmt.Sprintf("(status code %d) "+fmsg, statusCode) + logger.Warningf(lmsg) + return false, f.Write(statusCode, envelope{statusCode, "fail", fmsg, nil}) } //-------------------- @@ -111,14 +114,14 @@ type gobFormatter struct { } // Write is specified on the Formatter interface. -func (gf *gobFormatter) Write(status int, data interface{}, headers ...KeyValue) error { +func (gf *gobFormatter) Write(statusCode int, data interface{}, headers ...KeyValue) error { enc := gob.NewEncoder(gf.job.ResponseWriter()) for _, header := range headers { gf.job.ResponseWriter().Header().Add(header.Key, fmt.Sprintf("%v", header.Value)) } gf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeGOB) gf.job.ResponseWriter().Header().Set("Version", gf.job.Version().String()) - gf.job.ResponseWriter().WriteHeader(status) + gf.job.ResponseWriter().WriteHeader(statusCode) err := enc.Encode(data) if err != nil { http.Error(gf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) @@ -149,7 +152,7 @@ type jsonFormatter struct { } // Write is specified on the Formatter interface. -func (jf *jsonFormatter) Write(status int, data interface{}, headers ...KeyValue) error { +func (jf *jsonFormatter) Write(statusCode int, data interface{}, headers ...KeyValue) error { body, err := json.Marshal(data) if err != nil { http.Error(jf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) @@ -165,7 +168,7 @@ func (jf *jsonFormatter) Write(status int, data interface{}, headers ...KeyValue } jf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeJSON) jf.job.ResponseWriter().Header().Set("Version", jf.job.Version().String()) - jf.job.ResponseWriter().WriteHeader(status) + jf.job.ResponseWriter().WriteHeader(statusCode) _, err = jf.job.ResponseWriter().Write(body) return err } @@ -193,7 +196,7 @@ type xmlFormatter struct { } // Write is specified on the Formatter interface. -func (xf *xmlFormatter) Write(status int, data interface{}, headers ...KeyValue) error { +func (xf *xmlFormatter) Write(statusCode int, data interface{}, headers ...KeyValue) error { body, err := xml.Marshal(data) if err != nil { http.Error(xf.job.ResponseWriter(), err.Error(), http.StatusInternalServerError) @@ -204,7 +207,7 @@ func (xf *xmlFormatter) Write(status int, data interface{}, headers ...KeyValue) } xf.job.ResponseWriter().Header().Set("Content-Type", ContentTypeXML) xf.job.ResponseWriter().Header().Set("Version", xf.job.Version().String()) - xf.job.ResponseWriter().WriteHeader(status) + xf.job.ResponseWriter().WriteHeader(statusCode) _, err = xf.job.ResponseWriter().Write(body) return err } From 09c62f03dc51d36f174db772ee5702043935f83c Mon Sep 17 00:00:00 2001 From: themue Date: Thu, 15 Dec 2016 15:20:38 +0100 Subject: [PATCH 086/127] Added status codes for JWT authorization --- CHANGELOG.md | 6 ++++++ handlers/handlers_test.go | 2 +- handlers/jwtauth.go | 16 ++++++++-------- rest/formatter.go | 1 + 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf9f2c..485cea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Tideland Go REST Server Library +## 2016-12-15 + +- Added *StatusCode* to feedback envelope +- *JWTAuthorizationHandler* now provides different status codes + depending on valid tokens, expiration time, and authorization + ## 2016-12-09 - *FileServeHandler* now logs the absolute filename and logs diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 8856721..8f86674 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -231,7 +231,7 @@ func TestJWTAuthorizationHandler(t *testing.T) { assert.Nil(err) return out }, - status: 401, + status: 403, }, } // Run defined tests. diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index 5fdcab1..cb07a52 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -142,18 +142,18 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { } // Now do the checks. if err != nil { - return h.deny(job, err.Error()) + return h.deny(job, rest.StatusBadRequest, err.Error()) } if token == nil { - return h.deny(job, "no JSON Web Token") + return h.deny(job, rest.StatusUnauthorized, "no JSON Web Token") } if !token.IsValid(h.leeway) { - return h.deny(job, "JSON Web Token claims 'nbf' and/or 'exp' are not valid") + return h.deny(job, rest.StatusForbidden, "JSON Web Token claims 'nbf' and/or 'exp' are not valid") } if h.gatekeeper != nil { err := h.gatekeeper(job, token.Claims()) if err != nil { - return h.deny(job, "access rejected by gatekeeper: "+err.Error()) + return h.deny(job, rest.StatusUnauthorized, "access rejected by gatekeeper: "+err.Error()) } } // All fine, store token in context. @@ -164,15 +164,15 @@ func (h *jwtAuthorizationHandler) check(job rest.Job) (bool, error) { } // deny sends a negative feedback to the caller. -func (h *jwtAuthorizationHandler) deny(job rest.Job, msg string) (bool, error) { +func (h *jwtAuthorizationHandler) deny(job rest.Job, statusCode int, msg string) (bool, error) { h.logger(job, msg) switch { case job.AcceptsContentType(rest.ContentTypeJSON): - return rest.NegativeFeedback(job.JSON(false), rest.StatusUnauthorized, msg) + return rest.NegativeFeedback(job.JSON(false), statusCode, msg) case job.AcceptsContentType(rest.ContentTypeXML): - return rest.NegativeFeedback(job.XML(), rest.StatusUnauthorized, msg) + return rest.NegativeFeedback(job.XML(), statusCode, msg) default: - job.ResponseWriter().WriteHeader(rest.StatusUnauthorized) + job.ResponseWriter().WriteHeader(statusCode) job.ResponseWriter().Header().Set("Content-Type", rest.ContentTypePlain) job.ResponseWriter().Write([]byte(msg)) return false, nil diff --git a/rest/formatter.go b/rest/formatter.go index c1f3ab5..15fb935 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -38,6 +38,7 @@ const ( StatusNoContent = http.StatusNoContent StatusBadRequest = http.StatusBadRequest StatusUnauthorized = http.StatusUnauthorized + StatusForbidden = http.StatusForbidden StatusNotFound = http.StatusNotFound StatusConflict = http.StatusConflict StatusInternalServerError = http.StatusInternalServerError From fdd68259d0989b854893997be5a978cc96733917 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 5 Jan 2017 22:10:56 +0100 Subject: [PATCH 087/127] New version 2.9.6 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81aa3bf..84c21f6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.9.5 +Version 2.9.6 ## Packages From ad554bd024d455b6855e1f8f148f3316d254fc07 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 19 Jan 2017 14:54:49 +0100 Subject: [PATCH 088/127] Added ability to access form values --- CHANGELOG.md | 5 +++++ README.md | 2 +- rest/context.go | 2 +- rest/doc.go | 2 +- rest/environment.go | 2 +- rest/errors.go | 2 +- rest/formatter.go | 40 ++++++++++++++++++++-------------------- rest/handler.go | 2 +- rest/job.go | 16 ++++++++++++---- rest/mapping.go | 2 +- rest/multiplexer.go | 2 +- rest/rest_test.go | 2 +- rest/templates.go | 2 +- rest/tools.go | 2 +- 14 files changed, 48 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 485cea2..f97cef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Tideland Go REST Server Library +## 2017-01-19 + +- Renamed type *Query* to *Values* +- Added *Form()* to *Job* + ## 2016-12-15 - Added *StatusCode* to feedback envelope diff --git a/README.md b/README.md index 84c21f6..15cdac9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.9.6 +Version 2.10.0 ## Packages diff --git a/rest/context.go b/rest/context.go index 4857ef0..78de7a7 100644 --- a/rest/context.go +++ b/rest/context.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Context // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/doc.go b/rest/doc.go index efa6155..e240f93 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/environment.go b/rest/environment.go index e02d337..d350218 100644 --- a/rest/environment.go +++ b/rest/environment.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Environment // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/errors.go b/rest/errors.go index 9174fb0..3803501 100644 --- a/rest/errors.go +++ b/rest/errors.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Errors // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/formatter.go b/rest/formatter.go index 15fb935..63ea318 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Formatter // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -227,12 +227,12 @@ func (xf *xmlFormatter) Read(data interface{}) error { } //-------------------- -// QUERY +// VALUES //-------------------- -// Query allows typed access with default values to a jobs -// request values passed as query. -type Query interface { +// Values allows typed access with default values to a jobs +// request values passed as query or form. +type Values interface { // ValueAsString retrieves the string value of a given key. If it // doesn't exist the default value dv is returned. ValueAsString(key, dv string) string @@ -259,49 +259,49 @@ type Query interface { ValueAsDuration(key string, dv time.Duration) time.Duration } -// query implements Query. -type query struct { +// values implements Values. +type values struct { values url.Values } // ValueAsString implements the Query interface. -func (q *query) ValueAsString(key, dv string) string { - value := queryValuer(q.values.Get(key)) +func (v *values) ValueAsString(key, dv string) string { + value := queryValuer(v.values.Get(key)) return defaulter.AsString(value, dv) } // ValueAsBool implements the Query interface. -func (q *query) ValueAsBool(key string, dv bool) bool { - value := queryValuer(q.values.Get(key)) +func (v *values) ValueAsBool(key string, dv bool) bool { + value := queryValuer(v.values.Get(key)) return defaulter.AsBool(value, dv) } // ValueAsInt implements the Query interface. -func (q *query) ValueAsInt(key string, dv int) int { - value := queryValuer(q.values.Get(key)) +func (v *values) ValueAsInt(key string, dv int) int { + value := queryValuer(v.values.Get(key)) return defaulter.AsInt(value, dv) } // ValueAsFloat64 implements the Query interface. -func (q *query) ValueAsFloat64(key string, dv float64) float64 { - value := queryValuer(q.values.Get(key)) +func (v *values) ValueAsFloat64(key string, dv float64) float64 { + value := queryValuer(v.values.Get(key)) return defaulter.AsFloat64(value, dv) } // ValueAsTime implements the Query interface. -func (q *query) ValueAsTime(key, format string, dv time.Time) time.Time { - value := queryValuer(q.values.Get(key)) +func (v *values) ValueAsTime(key, format string, dv time.Time) time.Time { + value := queryValuer(v.values.Get(key)) return defaulter.AsTime(value, format, dv) } // ValueAsDuration implements the Query interface. -func (q *query) ValueAsDuration(key string, dv time.Duration) time.Duration { - value := queryValuer(q.values.Get(key)) +func (v *values) ValueAsDuration(key string, dv time.Duration) time.Duration { + value := queryValuer(v.values.Get(key)) return defaulter.AsDuration(value, dv) } // queryValues implements the stringex.Valuer interface for -// the usage inside of query. +// the usage inside of values. type queryValuer string // Value implements the Valuer interface. diff --git a/rest/handler.go b/rest/handler.go index 2486d3d..cbb5ae3 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Handlers // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/job.go b/rest/job.go index 288c4fc..29b53a0 100644 --- a/rest/job.go +++ b/rest/job.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Job // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -102,7 +102,10 @@ type Job interface { XML() Formatter // Query returns a convenient access to query values. - Query() Query + Query() Values + + // Form returns a convenient access to form values. + Form() Values } // job implements the Job interface. @@ -306,8 +309,13 @@ func (j *job) XML() Formatter { } // Query implements the Job interface. -func (j *job) Query() Query { - return &query{j.request.URL.Query()} +func (j *job) Query() Values { + return &values{j.request.URL.Query()} +} + +// Form implements the Job interface. +func (j *job) Form() Values { + return &values{j.request.PostForm} } // EOF diff --git a/rest/mapping.go b/rest/mapping.go index 1805324..4e1f43c 100644 --- a/rest/mapping.go +++ b/rest/mapping.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Mapping // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/multiplexer.go b/rest/multiplexer.go index cdb37b7..737aefc 100644 --- a/rest/multiplexer.go +++ b/rest/multiplexer.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Multiplexer // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/rest_test.go b/rest/rest_test.go index 505d85e..5d4c2ae 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Unit Tests // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/templates.go b/rest/templates.go index 4f39435..3013674 100644 --- a/rest/templates.go +++ b/rest/templates.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - templatesCache // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/tools.go b/rest/tools.go index e4c06a6..86345d7 100644 --- a/rest/tools.go +++ b/rest/tools.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST - Tools // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. From 881d0fe9ca5d7c2a787ad1eb9afb328fcc5a9ea0 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 7 Feb 2017 22:35:55 +0100 Subject: [PATCH 089/127] Started adding convenience functions --- README.md | 2 +- restaudit/restaudit.go | 64 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 15cdac9..1a934c2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.10.0 +Version 2.11.0 ## Packages diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 3d19e8f..6bf5abc 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -13,6 +13,8 @@ package restaudit import ( "bytes" + "encoding/json" + "encoding/xml" "io" "io/ioutil" "mime/multipart" @@ -24,7 +26,19 @@ import ( ) //-------------------- -// TEST TOOLS +// CONSTENTS +//-------------------- + +const ( + HeaderAccept = "Accept" + HeaderContentType = "Content-Type" + + ApplicationJSON = "application/json" + ApplicationXML = "application/xml" +) + +//-------------------- +// TEST TYPES //-------------------- // KeyValues handles keys and values for request headers and cookies. @@ -40,6 +54,28 @@ type Request struct { RequestProcessor func(req *http.Request) *http.Request } +// SetJSONContent sets the content of a request to JSON. +func (r *Request) SetJSONContent(assert audit.Assertion, data interface{}) { + body, err := json.Marshal(data) + assert.Nil(err) + r.Body = body + r.Header = KeyValues{ + HeaderContentType: ApplicationJSON, + HeaderAccept: ApplicationJSON, + } +} + +// SetXMLContent sets the content of a request to XML. +func (r *Request) SetXMLContent(assert audit.Assertion, data interface{}) { + body, err := xml.Marshal(data) + assert.Nil(err) + r.Body = body + r.Header = KeyValues{ + HeaderContentType: ApplicationXML, + HeaderAccept: ApplicationXML, + } +} + // Response wraps all infos of a test response. type Response struct { Status int @@ -48,10 +84,30 @@ type Response struct { Body []byte } +// JSONContent retrieves the JSON content and unmarshals it. +func (r *Response) JSONContent(assert audit.Assertion, data interface{}) { + contentType, ok := r.Header[HeaderContentType] + assert.True(ok) + assert.Equal(contentType, ApplicationJSON) + err := json.Unmarshal(r.Body, data) + assert.Nil(err) +} + +// XMLContent retrieves the XML content and unmarshals it. +func (r *Response) XMLContent(assert audit.Assertion, data interface{}) { + contentType, ok := r.Header[HeaderContentType] + assert.True(ok) + assert.Equal(contentType, ApplicationJSON) + err := xml.Unmarshal(r.Body, data) + assert.Nil(err) +} + //-------------------- // TEST SERVER //-------------------- +// TestServer defines the test server with methods for requests +// and uploads. type TestServer interface { // Close shuts down the server and blocks until all outstanding // requests have completed. @@ -78,12 +134,12 @@ func StartServer(handler http.Handler, assert audit.Assertion) TestServer { } } -// Close is specified on the TestServer interface. +// Close implements the TestServer interface. func (ts *testServer) Close() { ts.server.Close() } -// DoRequest is specified on the TestServer interface. +// DoRequest implements the TestServer interface. func (ts *testServer) DoRequest(req *Request) *Response { // First prepare it. transport := &http.Transport{} @@ -115,7 +171,7 @@ func (ts *testServer) DoRequest(req *Request) *Response { return ts.response(resp) } -// DoUpload is specified on the TestServer interface. +// DoUpload implements the TestServer interface. func (ts *testServer) DoUpload(path, fieldname, filename, data string) *Response { // Prepare request. transport := &http.Transport{} From 784233c3832894bf32d71dc627b482d8c136bd92 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 9 Feb 2017 21:50:37 +0100 Subject: [PATCH 090/127] Extended restaudit and adopted changes in rest --- CHANGELOG.md | 6 ++ README.md | 10 +- handlers/audit.go | 2 +- handlers/doc.go | 4 +- handlers/errors.go | 2 +- handlers/fileserve.go | 2 +- handlers/fileupload.go | 2 +- handlers/handlers_test.go | 2 +- handlers/jwtauth.go | 2 +- handlers/wrapper.go | 2 +- jwt/algorithm.go | 2 +- jwt/cache.go | 2 +- jwt/cache_test.go | 2 +- jwt/claims.go | 2 +- jwt/claims_test.go | 2 +- jwt/doc.go | 4 +- request/doc.go | 7 +- request/errors.go | 2 +- request/request.go | 2 +- request/request_test.go | 2 +- rest/doc.go | 4 +- rest/rest_test.go | 187 +++++++++++++++----------------------- restaudit/doc.go | 8 +- restaudit/restaudit.go | 161 ++++++++++++++++++++++++++------ 24 files changed, 244 insertions(+), 177 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f97cef8..80d81f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Tideland Go REST Server Library +## 2017-02-10 + +- Extended *Request* and *Response* of *restaudit* with some + convenience methods for easier testing +- Adopted *restaudit* changes in *rest* tests + ## 2017-01-19 - Renamed type *Query* to *Values* diff --git a/README.md b/README.md index 1a934c2..71bf50b 100644 --- a/README.md +++ b/README.md @@ -18,31 +18,31 @@ Version 2.11.0 ## Packages -#### REST +### REST RESTful web request handling. [![GoDoc](https://godoc.org/github.com/tideland/gorest/rest?status.svg)](https://godoc.org/github.com/tideland/gorest/rest) -#### Request +### Request Convenient client requests to RESTful web services. [![GoDoc](https://godoc.org/github.com/tideland/gorest/request?status.svg)](https://godoc.org/github.com/tideland/gorest/request) -#### Handlers +### Handlers Some general purpose handlers for the library. [![GoDoc](https://godoc.org/github.com/tideland/gorest/handlers?status.svg)](https://godoc.org/github.com/tideland/gorest/handlers) -#### JSON Web Token +### JSON Web Token JWT package for secure authentication and information exchange like claims. [![GoDoc](https://godoc.org/github.com/tideland/gorest/jwt?status.svg)](https://godoc.org/github.com/tideland/gorest/jwt) -#### REST Audit +### REST Audit Helpers for the unit tests of the Go REST Server Library. diff --git a/handlers/audit.go b/handlers/audit.go index fd49bf9..2ab2a04 100644 --- a/handlers/audit.go +++ b/handlers/audit.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - Audit Handler // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/handlers/doc.go b/handlers/doc.go index 96ab2cc..37ec7af 100644 --- a/handlers/doc.go +++ b/handlers/doc.go @@ -1,11 +1,11 @@ // Tideland Go REST Server Library - Handlers // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. -// The Tideland Go REST Server Library handlers package defines +// Package handlers of the Tideland Go REST Server Library implements // some initial resource handlers to integrate into own solutions. package handlers diff --git a/handlers/errors.go b/handlers/errors.go index 4e9fb51..f37b7cf 100644 --- a/handlers/errors.go +++ b/handlers/errors.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - Errors // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/handlers/fileserve.go b/handlers/fileserve.go index 39bce5e..92b6530 100644 --- a/handlers/fileserve.go +++ b/handlers/fileserve.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - File Serve // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/handlers/fileupload.go b/handlers/fileupload.go index 79f2696..195f5b7 100644 --- a/handlers/fileupload.go +++ b/handlers/fileupload.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - File Upload // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 8f86674..2034978 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - Unit Tests // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/handlers/jwtauth.go b/handlers/jwtauth.go index cb07a52..4d5b2cb 100644 --- a/handlers/jwtauth.go +++ b/handlers/jwtauth.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - JWT Authorization // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/handlers/wrapper.go b/handlers/wrapper.go index 6561e28..2336520 100644 --- a/handlers/wrapper.go +++ b/handlers/wrapper.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Handlers - Wrapper // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/algorithm.go b/jwt/algorithm.go index 100a0a4..76dfe84 100644 --- a/jwt/algorithm.go +++ b/jwt/algorithm.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Algorithm // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/cache.go b/jwt/cache.go index 2ec224e..3a58c1c 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Cache // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/cache_test.go b/jwt/cache_test.go index c3b5435..35fdea1 100644 --- a/jwt/cache_test.go +++ b/jwt/cache_test.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Unit Tests // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/claims.go b/jwt/claims.go index fb2de23..343c4f1 100644 --- a/jwt/claims.go +++ b/jwt/claims.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Claims // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/claims_test.go b/jwt/claims_test.go index d3a2a6e..d07e3b2 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Unit Tests // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/doc.go b/jwt/doc.go index 881d1d2..4ee79e8 100644 --- a/jwt/doc.go +++ b/jwt/doc.go @@ -1,11 +1,11 @@ // Tideland Go REST Server Library - JSON Web Token // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. -// The Tideland Go REST Server Library jwt provides the generation, +// Package jwt of the Tideland Go REST Server Library provides the generation, // verification, and analyzing of JSON Web Tokens. package jwt diff --git a/request/doc.go b/request/doc.go index 33d3429..b814174 100644 --- a/request/doc.go +++ b/request/doc.go @@ -1,12 +1,13 @@ // Tideland Go REST Server Library - Request // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. -// The request package provides a simple way to handle cross-server -// requests in the Tideland REST ecosystem. +// Package request of the Tideland Go REST Server Library provides +// a simple way to handle cross-server requests in the Tideland +// REST ecosystem. package request //-------------------- diff --git a/request/errors.go b/request/errors.go index e749079..afd1979 100644 --- a/request/errors.go +++ b/request/errors.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Request - Errors // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/request/request.go b/request/request.go index 4136c8f..bc421a9 100644 --- a/request/request.go +++ b/request/request.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Request // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/request/request_test.go b/request/request_test.go index cc4856c..bdad5a4 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - Request - Unit Tests // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/rest/doc.go b/rest/doc.go index e240f93..9f8af58 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -5,8 +5,8 @@ // All rights reserved. Use of this source code is governed // by the new BSD license. -// The Tideland Go REST Server Library provides the package rest for the -// implementation of servers with a RESTful API. The business has to +// Package rest of the Tideland Go REST Server Library provides types for +// the implementation of servers with a RESTful API. The business has to // be implemented in types fullfilling the ResourceHandler interface. // This basic interface only allows the initialization of the handler. // More interesting are the other interfaces like GetResourceHandler diff --git a/rest/rest_test.go b/rest/rest_test.go index 5d4c2ae..efa5e88 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -12,11 +12,7 @@ package rest_test //-------------------- import ( - "bytes" "context" - "encoding/gob" - "encoding/json" - "encoding/xml" "testing" "github.com/tideland/golib/audit" @@ -50,17 +46,13 @@ func TestGetJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/json/4711?foo=0815", - Header: restaudit.KeyValues{"Accept": "application/json"}, - }) - var data TestRequestData - err = json.Unmarshal(resp.Body, &data) - assert.Nil(err) - assert.Equal(data.ResourceID, "4711") - assert.Equal(data.Query, "0815") - assert.Equal(data.Context, "foo") + req := restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + resp := ts.DoRequest(req) + resp.AssertStatus(assert, 200) + resp.AssertContentMatch(assert, `.*"ResourceID":"4711".*`) + resp.AssertContentMatch(assert, `.*"Query":"0815".*`) + resp.AssertContentMatch(assert, `.*"Context":"foo".*`) } // TestPutJSON tests the PUT command with a JSON payload and result. @@ -73,18 +65,14 @@ func TestPutJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. + req := restaudit.NewRequest("PUT", "/base/test/json/4711") reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} - reqBuf, _ := json.Marshal(reqData) - resp := ts.DoRequest(&restaudit.Request{ - Method: "PUT", - Path: "/base/test/json/4711", - Header: restaudit.KeyValues{"Content-Type": "application/json", "Accept": "application/json"}, - Body: reqBuf, - }) - var recvData TestRequestData - err = json.Unmarshal(resp.Body, &recvData) - assert.Nil(err) - assert.Equal(recvData, reqData) + req.SetContent(assert, restaudit.ApplicationJSON, reqData) + resp := ts.DoRequest(req) + resp.AssertStatus(assert, 200) + respData := TestRequestData{} + resp.AssertContent(assert, &respData) + assert.Equal(respData, reqData) } // TestGetXML tests the GET command with an XML result. @@ -97,12 +85,11 @@ func TestGetXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/xml/4711", - Header: restaudit.KeyValues{"Accept": "application/xml"}, - }) - assert.Substring("4711", string(resp.Body)) + req := restaudit.NewRequest("GET", "/base/test/xml/4711") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationXML) + resp := ts.DoRequest(req) + resp.AssertStatus(assert, 200) + resp.AssertContentMatch(assert, `.*4711.*`) } // TestPutXML tests the PUT command with a XML payload and result. @@ -115,18 +102,14 @@ func TestPutXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. + req := restaudit.NewRequest("PUT", "/base/test/xml/4711") reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} - reqBuf, _ := xml.Marshal(reqData) - resp := ts.DoRequest(&restaudit.Request{ - Method: "PUT", - Path: "/base/test/xml/4711", - Header: restaudit.KeyValues{"Content-Type": "application/xml", "Accept": "application/xml"}, - Body: reqBuf, - }) - var recvData TestRequestData - err = xml.Unmarshal(resp.Body, &recvData) - assert.Nil(err) - assert.Equal(recvData, reqData) + req.SetContent(assert, restaudit.ApplicationXML, reqData) + resp := ts.DoRequest(req) + resp.AssertStatus(assert, 200) + respData := TestRequestData{} + resp.AssertContent(assert, &respData) + assert.Equal(respData, reqData) } // TestPutGOB tests the PUT command with a GOB payload and result. @@ -139,21 +122,14 @@ func TestPutGOB(t *testing.T) { err := mux.Register("test", "gob", NewTestHandler("putgob", assert)) assert.Nil(err) // Perform test requests. + req := restaudit.NewRequest("POST", "/base/test/gob") reqData := TestCounterData{"test", 4711} - reqBuf := new(bytes.Buffer) - err = gob.NewEncoder(reqBuf).Encode(reqData) - assert.Nil(err, "GOB encode.") - resp := ts.DoRequest(&restaudit.Request{ - Method: "POST", - Path: "/base/test/gob", - Header: restaudit.KeyValues{"Content-Type": "application/vnd.tideland.gob"}, - Body: reqBuf.Bytes(), - }) - var respData TestCounterData - err = gob.NewDecoder(bytes.NewBuffer(resp.Body)).Decode(&respData) - assert.Nil(err) - assert.Equal(respData.ID, "test") - assert.Equal(respData.Count, int64(4711)) + req.SetContent(assert, restaudit.ApplicationGOB, reqData) + resp := ts.DoRequest(req) + resp.AssertStatus(assert, 200) + respData := TestCounterData{} + resp.AssertContent(assert, &respData) + assert.Equal(respData, reqData) } // TestLongPath tests the setting of long path tail as resource ID. @@ -166,11 +142,9 @@ func TestLongPath(t *testing.T) { err := mux.Register("content", "blog", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/content/blog/2014/09/30/just-a-test", - }) - assert.Substring("
  • Resource ID: 2014/09/30/just-a-test
  • ", string(resp.Body)) + req := restaudit.NewRequest("GET", "/base/content/blog/2014/09/30/just-a-test") + resp := ts.DoRequest(req) + resp.AssertContentMatch(assert, `.*Resource ID: 2014/09/30/just-a-test.*`) } // TestFallbackDefault tests the fallback to default. @@ -183,11 +157,9 @@ func TestFallbackDefault(t *testing.T) { err := mux.Register("testing", "index", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/x/y", - }) - assert.Substring("
  • Resource: y
  • ", string(resp.Body)) + req := restaudit.NewRequest("GET", "/base/x/y") + resp := ts.DoRequest(req) + resp.AssertContentMatch(assert, `.*Resource: y.*`) } // TestHandlerStack tests a complete handler stack. @@ -204,25 +176,17 @@ func TestHandlerStack(t *testing.T) { }) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/stack", - }) - token := resp.Header["Token"] + req := restaudit.NewRequest("GET", "/base/test/stack") + resp := ts.DoRequest(req) + resp.AssertContentMatch(assert, ".*Resource: token.*") + token := resp.AssertHeader(assert, "Token") assert.Equal(token, "foo") - assert.Substring("
  • Resource: token
  • ", string(resp.Body)) - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/stack", - Header: restaudit.KeyValues{"token": "foo"}, - }) - assert.Substring("
  • Resource: stack
  • ", string(resp.Body)) - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/stack", - Header: restaudit.KeyValues{"token": "foo"}, - }) - assert.Substring("
  • Resource: stack
  • ", string(resp.Body)) + req = restaudit.NewRequest("GET", "/base/test/stack") + req.AddHeader("token", "foo") + resp = ts.DoRequest(req) + resp.AssertContentMatch(assert, ".*Resource: stack.*") + resp = ts.DoRequest(req) + resp.AssertContentMatch(assert, ".*Resource: stack.*") } // TestVersion tests request and response version. @@ -235,34 +199,27 @@ func TestVersion(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/json/4711?foo=0815", - Header: restaudit.KeyValues{ - "Accept": "application/json", - }, - }) - vsn := resp.Header["Version"] + req := restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + resp := ts.DoRequest(req) + resp.AssertStatus(assert, 200) + vsn := resp.AssertHeader(assert, "Version") assert.Equal(vsn, "1.0.0") - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/json/4711?foo=0815", - Header: restaudit.KeyValues{ - "Accept": "application/json", - "Version": "2", - }, - }) - vsn = resp.Header["Version"] + + req = restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + req.AddHeader("Version", "2") + resp = ts.DoRequest(req) + resp.AssertStatus(assert, 200) + vsn = resp.AssertHeader(assert, "Version") assert.Equal(vsn, "2.0.0") - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/base/test/json/4711?foo=0815", - Header: restaudit.KeyValues{ - "Accept": "application/json", - "Version": "3.0", - }, - }) - vsn = resp.Header["Version"] + + req = restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + req.AddHeader("Version", "3.0") + resp = ts.DoRequest(req) + resp.AssertStatus(assert, 200) + vsn = resp.AssertHeader(assert, "Version") assert.Equal(vsn, "4.0.0-alpha") } @@ -318,11 +275,9 @@ func TestMethodNotSupported(t *testing.T) { err := mux.Register("test", "method", NewTestHandler("method", assert)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "OPTION", - Path: "/base/test/method", - }) - assert.Substring("OPTION", string(resp.Body)) + req := restaudit.NewRequest("OPTION", "/base/test/method") + resp := ts.DoRequest(req) + resp.AssertContentMatch(assert, ".*OPTION.*") } //-------------------- diff --git a/restaudit/doc.go b/restaudit/doc.go index 69140b1..7f24a15 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -1,13 +1,15 @@ // Tideland Go REST Server Library - REST Audit // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. -// The Tideland Go REST Server Library restaudit package is a little +// Package restaudit of the Tideland Go REST Server Library is a little // helper package for the unit testing of the rest package and the -// resource handlers. +// resource handlers. Requests can easily be created, marshalling data +// based on the content-type is done automatically. Response also +// provides assert methods for the tests. package restaudit // EOF diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 6bf5abc..6adc8a3 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - REST Audit // -// Copyright (C) 2009-2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -13,13 +13,16 @@ package restaudit import ( "bytes" + "encoding/gob" "encoding/json" "encoding/xml" + "html/template" "io" "io/ioutil" "mime/multipart" "net/http" "net/http/httptest" + "regexp" "strings" "github.com/tideland/golib/audit" @@ -33,6 +36,7 @@ const ( HeaderAccept = "Accept" HeaderContentType = "Content-Type" + ApplicationGOB = "application/vnd.tideland.gob" ApplicationJSON = "application/json" ApplicationXML = "application/xml" ) @@ -44,6 +48,10 @@ const ( // KeyValues handles keys and values for request headers and cookies. type KeyValues map[string]string +//-------------------- +// REQUEST +//-------------------- + // Request wraps all infos for a test request. type Request struct { Method string @@ -54,28 +62,91 @@ type Request struct { RequestProcessor func(req *http.Request) *http.Request } -// SetJSONContent sets the content of a request to JSON. -func (r *Request) SetJSONContent(assert audit.Assertion, data interface{}) { - body, err := json.Marshal(data) - assert.Nil(err) - r.Body = body - r.Header = KeyValues{ - HeaderContentType: ApplicationJSON, - HeaderAccept: ApplicationJSON, +// NewRequest creates a new test request with the given method +// and path. +func NewRequest(method, path string) *Request { + return &Request{ + Method: method, + Path: path, + } +} + +// AddHeader adds or overwrites a request header. +func (r *Request) AddHeader(key, value string) *Request { + if r.Header == nil { + r.Header = KeyValues{} } + r.Header[key] = value + return r } -// SetXMLContent sets the content of a request to XML. -func (r *Request) SetXMLContent(assert audit.Assertion, data interface{}) { - body, err := xml.Marshal(data) - assert.Nil(err) - r.Body = body - r.Header = KeyValues{ - HeaderContentType: ApplicationXML, - HeaderAccept: ApplicationXML, +// AddCookie adds or overwrites a request header. +func (r *Request) AddCookie(key, value string) *Request { + if r.Cookies == nil { + r.Cookies = KeyValues{} } + r.Cookies[key] = value + return r } +// SetContent sets the request content based on the type and +// the marshalled data. +func (r *Request) SetContent( + assert audit.Assertion, + contentType string, + data interface{}, +) *Request { + switch contentType { + case ApplicationGOB: + body := &bytes.Buffer{} + enc := gob.NewEncoder(body) + err := enc.Encode(data) + assert.Nil(err, "cannot encode data to GOB") + r.Body = body.Bytes() + r.AddHeader(HeaderContentType, ApplicationGOB) + r.AddHeader(HeaderAccept, ApplicationGOB) + case ApplicationJSON: + body, err := json.Marshal(data) + assert.Nil(err, "cannot marshal data to JSON") + r.Body = body + r.AddHeader(HeaderContentType, ApplicationJSON) + r.AddHeader(HeaderAccept, ApplicationJSON) + case ApplicationXML: + body, err := xml.Marshal(data) + assert.Nil(err, "cannot marshal data to XML") + r.Body = body + r.AddHeader(HeaderContentType, ApplicationXML) + r.AddHeader(HeaderAccept, ApplicationXML) + } + return r +} + +// RenderTemplate renders the passed data into the template +// and assigns it to the request body. The content type +// will be set too. +func (r *Request) RenderTemplate( + assert audit.Assertion, + contentType string, + templateSource string, + data interface{}, +) *Request { + // Render template. + t, err := template.New(r.Path).Parse(templateSource) + assert.Nil(err, "cannot parse template") + body := &bytes.Buffer{} + err = t.Execute(body, data) + assert.Nil(err, "cannot render template") + r.Body = body.Bytes() + // Set content type. + r.AddHeader(HeaderContentType, contentType) + r.AddHeader(HeaderAccept, contentType) + return r +} + +//-------------------- +// RESPONSE +//-------------------- + // Response wraps all infos of a test response. type Response struct { Status int @@ -84,22 +155,54 @@ type Response struct { Body []byte } -// JSONContent retrieves the JSON content and unmarshals it. -func (r *Response) JSONContent(assert audit.Assertion, data interface{}) { - contentType, ok := r.Header[HeaderContentType] - assert.True(ok) - assert.Equal(contentType, ApplicationJSON) - err := json.Unmarshal(r.Body, data) - assert.Nil(err) +// AssertStatus checks if the status is the expected one. +func (r *Response) AssertStatus(assert audit.Assertion, status int) { + assert.Equal(r.Status, status, "response status differs") +} + +// AssertHeader checks if a header exists and retrieves it. +func (r *Response) AssertHeader(assert audit.Assertion, key string) string { + assert.NotEmpty(r.Header, "response contains no header") + value, ok := r.Header[key] + assert.True(ok, "header '"+key+"' not found") + return value } -// XMLContent retrieves the XML content and unmarshals it. -func (r *Response) XMLContent(assert audit.Assertion, data interface{}) { +// AssertCookie checks if a cookie exists and retrieves it. +func (r *Response) AssertCookie(assert audit.Assertion, key string) string { + assert.NotEmpty(r.Cookies, "response contains no cookies") + value, ok := r.Cookies[key] + assert.True(ok, "cookie '"+key+"' not found") + return value +} + +// AssertContent retrieves the content based on the content type +// and unmarshals it accordingly. +func (r *Response) AssertContent(assert audit.Assertion, data interface{}) { contentType, ok := r.Header[HeaderContentType] assert.True(ok) - assert.Equal(contentType, ApplicationJSON) - err := xml.Unmarshal(r.Body, data) - assert.Nil(err) + switch contentType { + case ApplicationGOB: + body := bytes.NewBuffer(r.Body) + dec := gob.NewDecoder(body) + err := dec.Decode(data) + assert.Nil(err, "cannot decode GOB body") + case ApplicationJSON: + err := json.Unmarshal(r.Body, data) + assert.Nil(err, "cannot unmarshal JSON body") + case ApplicationXML: + err := xml.Unmarshal(r.Body, data) + assert.Nil(err, "cannot unmarshal XML body") + default: + assert.Fail("unknown content type: " + contentType) + } +} + +// AssertContentMatch checks if the content matches a regular expression. +func (r *Response) AssertContentMatch(assert audit.Assertion, pattern string) { + ok, err := regexp.MatchString(pattern, string(r.Body)) + assert.Nil(err, "illegal content match pattern") + assert.True(ok, "body doesn't match pattern") } //-------------------- From a8457681c3ffc829ec5ef9641e682bc21b882309 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 9 Feb 2017 21:52:28 +0100 Subject: [PATCH 091/127] Changed date of LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6ff86e4..0801e5d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2016, Frank Mueller / Tideland / Oldenburg / Germany +Copyright (c) 2009-2017, Frank Mueller / Tideland / Oldenburg / Germany All rights reserved. Redistribution and use in source and binary forms, with or without modification, From 992d429d2a969868ca66002744c9faf61a4229f2 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 11 Feb 2017 22:56:07 +0100 Subject: [PATCH 092/127] Started with version 2.12.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71bf50b..4b3616c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.11.0 +Version 2.12.0 ## Packages From 76b132d6bb18704b617bfc1c0bf43a3ee07a6996 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 11 Feb 2017 23:51:35 +0100 Subject: [PATCH 093/127] Made testing with restaudit more simple --- rest/rest_test.go | 86 ++++++++++++++++----------------- restaudit/restaudit.go | 107 ++++++++++++++++++++++++++--------------- 2 files changed, 110 insertions(+), 83 deletions(-) diff --git a/rest/rest_test.go b/rest/rest_test.go index efa5e88..a950692 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -46,13 +46,13 @@ func TestGetJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req := restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) resp := ts.DoRequest(req) - resp.AssertStatus(assert, 200) - resp.AssertContentMatch(assert, `.*"ResourceID":"4711".*`) - resp.AssertContentMatch(assert, `.*"Query":"0815".*`) - resp.AssertContentMatch(assert, `.*"Context":"foo".*`) + resp.AssertStatusEquals(200) + resp.AssertBodyContains(`"ResourceID":"4711"`) + resp.AssertBodyContains(`"Query":"0815"`) + resp.AssertBodyContains(`"Context":"foo"`) } // TestPutJSON tests the PUT command with a JSON payload and result. @@ -65,13 +65,13 @@ func TestPutJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("PUT", "/base/test/json/4711") + req := restaudit.NewRequest(assert, "PUT", "/base/test/json/4711") reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} - req.SetContent(assert, restaudit.ApplicationJSON, reqData) + req.SetBody(restaudit.ApplicationJSON, reqData) resp := ts.DoRequest(req) - resp.AssertStatus(assert, 200) + resp.AssertStatusEquals(200) respData := TestRequestData{} - resp.AssertContent(assert, &respData) + resp.AssertBody(&respData) assert.Equal(respData, reqData) } @@ -85,11 +85,11 @@ func TestGetXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("GET", "/base/test/xml/4711") + req := restaudit.NewRequest(assert, "GET", "/base/test/xml/4711") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationXML) resp := ts.DoRequest(req) - resp.AssertStatus(assert, 200) - resp.AssertContentMatch(assert, `.*4711.*`) + resp.AssertStatusEquals(200) + resp.AssertBodyContains(`4711`) } // TestPutXML tests the PUT command with a XML payload and result. @@ -102,13 +102,13 @@ func TestPutXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("PUT", "/base/test/xml/4711") + req := restaudit.NewRequest(assert, "PUT", "/base/test/xml/4711") reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} - req.SetContent(assert, restaudit.ApplicationXML, reqData) + req.SetBody(restaudit.ApplicationXML, reqData) resp := ts.DoRequest(req) - resp.AssertStatus(assert, 200) + resp.AssertStatusEquals(200) respData := TestRequestData{} - resp.AssertContent(assert, &respData) + resp.AssertBody(&respData) assert.Equal(respData, reqData) } @@ -122,13 +122,13 @@ func TestPutGOB(t *testing.T) { err := mux.Register("test", "gob", NewTestHandler("putgob", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("POST", "/base/test/gob") + req := restaudit.NewRequest(assert, "POST", "/base/test/gob") reqData := TestCounterData{"test", 4711} - req.SetContent(assert, restaudit.ApplicationGOB, reqData) + req.SetBody(restaudit.ApplicationGOB, reqData) resp := ts.DoRequest(req) - resp.AssertStatus(assert, 200) + resp.AssertStatusEquals(200) respData := TestCounterData{} - resp.AssertContent(assert, &respData) + resp.AssertBody(&respData) assert.Equal(respData, reqData) } @@ -142,9 +142,9 @@ func TestLongPath(t *testing.T) { err := mux.Register("content", "blog", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("GET", "/base/content/blog/2014/09/30/just-a-test") + req := restaudit.NewRequest(assert, "GET", "/base/content/blog/2014/09/30/just-a-test") resp := ts.DoRequest(req) - resp.AssertContentMatch(assert, `.*Resource ID: 2014/09/30/just-a-test.*`) + resp.AssertBodyContains(`Resource ID: 2014/09/30/just-a-test`) } // TestFallbackDefault tests the fallback to default. @@ -157,9 +157,9 @@ func TestFallbackDefault(t *testing.T) { err := mux.Register("testing", "index", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("GET", "/base/x/y") + req := restaudit.NewRequest(assert, "GET", "/base/x/y") resp := ts.DoRequest(req) - resp.AssertContentMatch(assert, `.*Resource: y.*`) + resp.AssertBodyContains(`Resource: y`) } // TestHandlerStack tests a complete handler stack. @@ -176,17 +176,16 @@ func TestHandlerStack(t *testing.T) { }) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("GET", "/base/test/stack") + req := restaudit.NewRequest(assert, "GET", "/base/test/stack") resp := ts.DoRequest(req) - resp.AssertContentMatch(assert, ".*Resource: token.*") - token := resp.AssertHeader(assert, "Token") - assert.Equal(token, "foo") - req = restaudit.NewRequest("GET", "/base/test/stack") + resp.AssertBodyContains("Resource: token") + resp.AssertHeaderEquals("Token", "foo") + req = restaudit.NewRequest(assert, "GET", "/base/test/stack") req.AddHeader("token", "foo") resp = ts.DoRequest(req) - resp.AssertContentMatch(assert, ".*Resource: stack.*") + resp.AssertBodyContains("Resource: stack") resp = ts.DoRequest(req) - resp.AssertContentMatch(assert, ".*Resource: stack.*") + resp.AssertBodyContains("Resource: stack") } // TestVersion tests request and response version. @@ -199,28 +198,25 @@ func TestVersion(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req := restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) resp := ts.DoRequest(req) - resp.AssertStatus(assert, 200) - vsn := resp.AssertHeader(assert, "Version") - assert.Equal(vsn, "1.0.0") + resp.AssertStatusEquals(200) + resp.AssertHeaderEquals("Version", "1.0.0") - req = restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req = restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.AddHeader("Version", "2") resp = ts.DoRequest(req) - resp.AssertStatus(assert, 200) - vsn = resp.AssertHeader(assert, "Version") - assert.Equal(vsn, "2.0.0") + resp.AssertStatusEquals(200) + resp.AssertHeaderEquals("Version", "2.0.0") - req = restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") + req = restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.AddHeader("Version", "3.0") resp = ts.DoRequest(req) - resp.AssertStatus(assert, 200) - vsn = resp.AssertHeader(assert, "Version") - assert.Equal(vsn, "4.0.0-alpha") + resp.AssertStatusEquals(200) + resp.AssertHeaderEquals("Version", "4.0.0-alpha") } // TestDeregister tests the different possibilities to stop handlers. @@ -275,9 +271,9 @@ func TestMethodNotSupported(t *testing.T) { err := mux.Register("test", "method", NewTestHandler("method", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("OPTION", "/base/test/method") + req := restaudit.NewRequest(assert, "OPTION", "/base/test/method") resp := ts.DoRequest(req) - resp.AssertContentMatch(assert, ".*OPTION.*") + resp.AssertBodyContains("OPTION") } //-------------------- diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 6adc8a3..7278d92 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -54,6 +54,7 @@ type KeyValues map[string]string // Request wraps all infos for a test request. type Request struct { + Assert audit.Assertion Method string Path string Header KeyValues @@ -64,8 +65,9 @@ type Request struct { // NewRequest creates a new test request with the given method // and path. -func NewRequest(method, path string) *Request { +func NewRequest(assert audit.Assertion, method, path string) *Request { return &Request{ + Assert: assert, Method: method, Path: path, } @@ -89,31 +91,27 @@ func (r *Request) AddCookie(key, value string) *Request { return r } -// SetContent sets the request content based on the type and +// SetBody sets the request content based on the type and // the marshalled data. -func (r *Request) SetContent( - assert audit.Assertion, - contentType string, - data interface{}, -) *Request { +func (r *Request) SetBody(contentType string, data interface{}) *Request { switch contentType { case ApplicationGOB: body := &bytes.Buffer{} enc := gob.NewEncoder(body) err := enc.Encode(data) - assert.Nil(err, "cannot encode data to GOB") + r.Assert.Nil(err, "cannot encode data to GOB") r.Body = body.Bytes() r.AddHeader(HeaderContentType, ApplicationGOB) r.AddHeader(HeaderAccept, ApplicationGOB) case ApplicationJSON: body, err := json.Marshal(data) - assert.Nil(err, "cannot marshal data to JSON") + r.Assert.Nil(err, "cannot marshal data to JSON") r.Body = body r.AddHeader(HeaderContentType, ApplicationJSON) r.AddHeader(HeaderAccept, ApplicationJSON) case ApplicationXML: body, err := xml.Marshal(data) - assert.Nil(err, "cannot marshal data to XML") + r.Assert.Nil(err, "cannot marshal data to XML") r.Body = body r.AddHeader(HeaderContentType, ApplicationXML) r.AddHeader(HeaderAccept, ApplicationXML) @@ -124,18 +122,13 @@ func (r *Request) SetContent( // RenderTemplate renders the passed data into the template // and assigns it to the request body. The content type // will be set too. -func (r *Request) RenderTemplate( - assert audit.Assertion, - contentType string, - templateSource string, - data interface{}, -) *Request { +func (r *Request) RenderTemplate(contentType string, templateSource string, data interface{}) *Request { // Render template. t, err := template.New(r.Path).Parse(templateSource) - assert.Nil(err, "cannot parse template") + r.Assert.Nil(err, "cannot parse template") body := &bytes.Buffer{} err = t.Execute(body, data) - assert.Nil(err, "cannot render template") + r.Assert.Nil(err, "cannot render template") r.Body = body.Bytes() // Set content type. r.AddHeader(HeaderContentType, contentType) @@ -149,60 +142,94 @@ func (r *Request) RenderTemplate( // Response wraps all infos of a test response. type Response struct { + Assert audit.Assertion Status int Header KeyValues Cookies KeyValues Body []byte } -// AssertStatus checks if the status is the expected one. -func (r *Response) AssertStatus(assert audit.Assertion, status int) { - assert.Equal(r.Status, status, "response status differs") +// AssertStatusEquals checks if the status is the expected one. +func (r *Response) AssertStatusEquals(expected int) { + r.Assert.Equal(r.Status, expected, "response status differs") } // AssertHeader checks if a header exists and retrieves it. -func (r *Response) AssertHeader(assert audit.Assertion, key string) string { - assert.NotEmpty(r.Header, "response contains no header") +func (r *Response) AssertHeader(key string) string { + r.Assert.NotEmpty(r.Header, "response contains no header") value, ok := r.Header[key] - assert.True(ok, "header '"+key+"' not found") + r.Assert.True(ok, "header '"+key+"' not found") return value } +// AssertHeaderEquals checks if a header exists and compares +// it to an expected one. +func (r *Response) AssertHeaderEquals(key, expected string) { + value := r.AssertHeader(key) + r.Assert.Equal(value, expected, "header value is not equal to expected") +} + +// AssertHeaderContains checks if a header exists and looks for +// an expected part. +func (r *Response) AssertHeaderContains(key, expected string) { + value := r.AssertHeader(key) + r.Assert.Substring(expected, value, "header value does not contain expected") +} + // AssertCookie checks if a cookie exists and retrieves it. -func (r *Response) AssertCookie(assert audit.Assertion, key string) string { - assert.NotEmpty(r.Cookies, "response contains no cookies") +func (r *Response) AssertCookie(key string) string { + r.Assert.NotEmpty(r.Cookies, "response contains no cookies") value, ok := r.Cookies[key] - assert.True(ok, "cookie '"+key+"' not found") + r.Assert.True(ok, "cookie '"+key+"' not found") return value } -// AssertContent retrieves the content based on the content type +// AssertCookieEquals checks if a cookie exists and compares +// it to an expected one. +func (r *Response) AssertCookieEquals(key, expected string) { + value := r.AssertCookie(key) + r.Assert.Equal(value, expected, "cookie value is not equal to expected") +} + +// AssertCookieContains checks if a cookie exists and looks for +// an expected part. +func (r *Response) AssertCookieContains(key, expected string) { + value := r.AssertCookie(key) + r.Assert.Substring(expected, value, "cookie value does not contain expected") +} + +// AssertBody retrieves the body based on the content type // and unmarshals it accordingly. -func (r *Response) AssertContent(assert audit.Assertion, data interface{}) { +func (r *Response) AssertBody(data interface{}) { contentType, ok := r.Header[HeaderContentType] - assert.True(ok) + r.Assert.True(ok) switch contentType { case ApplicationGOB: body := bytes.NewBuffer(r.Body) dec := gob.NewDecoder(body) err := dec.Decode(data) - assert.Nil(err, "cannot decode GOB body") + r.Assert.Nil(err, "cannot decode GOB body") case ApplicationJSON: err := json.Unmarshal(r.Body, data) - assert.Nil(err, "cannot unmarshal JSON body") + r.Assert.Nil(err, "cannot unmarshal JSON body") case ApplicationXML: err := xml.Unmarshal(r.Body, data) - assert.Nil(err, "cannot unmarshal XML body") + r.Assert.Nil(err, "cannot unmarshal XML body") default: - assert.Fail("unknown content type: " + contentType) + r.Assert.Fail("unknown content type: " + contentType) } } -// AssertContentMatch checks if the content matches a regular expression. -func (r *Response) AssertContentMatch(assert audit.Assertion, pattern string) { +// AssertBodyMatches checks if the body matches a regular expression. +func (r *Response) AssertBodyMatches(pattern string) { ok, err := regexp.MatchString(pattern, string(r.Body)) - assert.Nil(err, "illegal content match pattern") - assert.True(ok, "body doesn't match pattern") + r.Assert.Nil(err, "illegal content match pattern") + r.Assert.True(ok, "body doesn't match pattern") +} + +// AssertBodyContains checks if the body contains a string. +func (r *Response) AssertBodyContains(expected string) { + r.Assert.Contents(expected, r.Body, "body doesn't contains expected") } //-------------------- @@ -249,6 +276,9 @@ func (ts *testServer) DoRequest(req *Request) *Response { c := &http.Client{Transport: transport} url := ts.server.URL + req.Path var bodyReader io.Reader + if req.Assert == nil { + req.Assert = ts.assert + } if req.Body != nil { bodyReader = ioutil.NopCloser(bytes.NewBuffer(req.Body)) } @@ -309,6 +339,7 @@ func (ts *testServer) response(hr *http.Response) *Response { ts.assert.Nil(err, "cannot read response") defer hr.Body.Close() return &Response{ + Assert: ts.assert, Status: hr.StatusCode, Header: respHeader, Cookies: respCookies, From 793116752c76ee86720fb5945e13d21688f1e303 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 12 Feb 2017 19:40:11 +0100 Subject: [PATCH 094/127] More convenience and documentation --- CHANGELOG.md | 10 ++++ handlers/handlers_test.go | 39 ++++++---------- jwt/errors.go | 2 +- jwt/header_test.go | 96 ++++++++++++++------------------------- rest/handler.go | 15 +++--- rest/rest_test.go | 38 ++++++++-------- restaudit/doc.go | 38 ++++++++++++++++ restaudit/restaudit.go | 87 ++++++++++++++++++++--------------- 8 files changed, 175 insertions(+), 150 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d81f3..47b8dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Tideland Go REST Server Library +## 2017-02-12 + +- Some renamings in *Request* and *Response*, sadly + to the previous minor release +- More convenience helpers for testing +- Adopted new testing to more packages +- Using http package constants instead of own + plain strings +- Added documentation to restaudit + ## 2017-02-10 - Extended *Request* and *Response* of *restaudit* with some diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 2034978..a063217 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -51,11 +51,9 @@ func TestWrapperHandler(t *testing.T) { err := mux.Register("test", "wrapper", handlers.NewWrapperHandler("wrapper", handler)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/wrapper", - }) - assert.Equal(string(resp.Body), data) + req := restaudit.NewRequest("GET", "/test/wrapper") + resp := ts.DoRequest(req) + resp.AssertBodyContains(data) } // TestFileServeHandler tests the serving of files. @@ -81,16 +79,12 @@ func TestFileServeHandler(t *testing.T) { err = mux.Register("test", "files", handlers.NewFileServeHandler("files", dir)) assert.Nil(err) // Perform test requests. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/files/foo.txt", - }) - assert.Equal(string(resp.Body), data) - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/files/does.not.exist", - }) - assert.Equal(string(resp.Body), "404 page not found\n") + req := restaudit.NewRequest("GET", "/test/files/foo.txt") + resp := ts.DoRequest(req) + resp.AssertBodyContains(data) + req = restaudit.NewRequest("GET", "/test/files/does.not.exist") + resp = ts.DoRequest(req) + resp.AssertBodyContains("404 page not found") } // TestFileUploadHandler tests the uploading of files. @@ -247,11 +241,12 @@ func TestJWTAuthorizationHandler(t *testing.T) { err := mux.Register("jwt", test.id, handlers.NewAuditHandler("audit", assert, test.auditf)) assert.Nil(err) } - var requestProcessor func(req *http.Request) *http.Request + // Create request. + req := restaudit.NewRequest("GET", "/jwt/"+test.id+"/1234567890") if test.tokener != nil { - requestProcessor = func(req *http.Request) *http.Request { + req.SetRequestProcessor(func(req *http.Request) *http.Request { return jwt.AddTokenToRequest(req, test.tokener()) - } + }) } // Make request(s). runs := 1 @@ -259,12 +254,8 @@ func TestJWTAuthorizationHandler(t *testing.T) { runs = test.runs } for i := 0; i < runs; i++ { - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/jwt/" + test.id + "/1234567890", - RequestProcessor: requestProcessor, - }) - assert.Equal(resp.Status, test.status) + resp := ts.DoRequest(req) + resp.AssertStatusEquals(test.status) } } } diff --git a/jwt/errors.go b/jwt/errors.go index fb33d47..c385c64 100644 --- a/jwt/errors.go +++ b/jwt/errors.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Errors // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. diff --git a/jwt/header_test.go b/jwt/header_test.go index 07be705..94ecdf1 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Unit Tests // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -13,7 +13,6 @@ package jwt_test import ( "context" - "encoding/json" "net/http" "testing" "time" @@ -46,17 +45,14 @@ func TestDecodeRequest(t *testing.T) { err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) assert.Nil(err) // Perform test request. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/jwt/1234567890", - Header: restaudit.KeyValues{"Accept": "application/json"}, - RequestProcessor: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) - }, + req := restaudit.NewRequest("GET", "/test/jwt/1234567890") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + req.SetRequestProcessor(func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) }) - var claimsOut jwt.Claims - err = json.Unmarshal(resp.Body, &claimsOut) - assert.Nil(err) + resp := ts.DoRequest(req) + claimsOut := jwt.Claims{} + resp.AssertUnmarshalledBody(&claimsOut) assert.Equal(claimsOut, claimsIn) } @@ -76,29 +72,19 @@ func TestDecodeCachedRequest(t *testing.T) { err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, true)) assert.Nil(err) // Perform first test request. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/jwt/1234567890", - Header: restaudit.KeyValues{"Accept": "application/json"}, - RequestProcessor: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) - }, + req := restaudit.NewRequest("GET", "/test/jwt/1234567890") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + req.SetRequestProcessor(func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) }) - var claimsOut jwt.Claims - err = json.Unmarshal(resp.Body, &claimsOut) - assert.Nil(err) + resp := ts.DoRequest(req) + claimsOut := jwt.Claims{} + resp.AssertUnmarshalledBody(&claimsOut) assert.Equal(claimsOut, claimsIn) // Perform second test request. - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/jwt/1234567890", - Header: restaudit.KeyValues{"Accept": "application/json"}, - RequestProcessor: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) - }, - }) - err = json.Unmarshal(resp.Body, &claimsOut) - assert.Nil(err) + resp = ts.DoRequest(req) + claimsOut = jwt.Claims{} + resp.AssertUnmarshalledBody(&claimsOut) assert.Equal(claimsOut, claimsIn) } @@ -118,17 +104,14 @@ func TestVerifyRequest(t *testing.T) { err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, false)) assert.Nil(err) // Perform test request. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/jwt/1234567890", - Header: restaudit.KeyValues{"Accept": "application/json"}, - RequestProcessor: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) - }, + req := restaudit.NewRequest("GET", "/test/jwt/1234567890") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + req.SetRequestProcessor(func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) }) - var claimsOut jwt.Claims - err = json.Unmarshal(resp.Body, &claimsOut) - assert.Nil(err) + resp := ts.DoRequest(req) + claimsOut := jwt.Claims{} + resp.AssertUnmarshalledBody(&claimsOut) assert.Equal(claimsOut, claimsIn) } @@ -148,29 +131,18 @@ func TestVerifyCachedRequest(t *testing.T) { err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, true)) assert.Nil(err) // Perform first test request. - resp := ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/jwt/1234567890", - Header: restaudit.KeyValues{"Accept": "application/json"}, - RequestProcessor: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) - }, + req := restaudit.NewRequest("GET", "/test/jwt/1234567890") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + req.SetRequestProcessor(func(req *http.Request) *http.Request { + return jwt.AddTokenToRequest(req, jwtIn) }) - var claimsOut jwt.Claims - err = json.Unmarshal(resp.Body, &claimsOut) - assert.Nil(err) + resp := ts.DoRequest(req) + claimsOut := jwt.Claims{} + resp.AssertUnmarshalledBody(&claimsOut) assert.Equal(claimsOut, claimsIn) // Perform second test request. - resp = ts.DoRequest(&restaudit.Request{ - Method: "GET", - Path: "/test/jwt/1234567890", - Header: restaudit.KeyValues{"Accept": "application/json"}, - RequestProcessor: func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) - }, - }) - err = json.Unmarshal(resp.Body, &claimsOut) - assert.Nil(err) + resp = ts.DoRequest(req) + resp.AssertUnmarshalledBody(&claimsOut) assert.Equal(claimsOut, claimsIn) } diff --git a/rest/handler.go b/rest/handler.go index cbb5ae3..bf8f780 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -13,6 +13,7 @@ package rest import ( "fmt" + "net/http" "github.com/tideland/golib/errors" ) @@ -83,43 +84,43 @@ func handleJob(handler ResourceHandler, job Job) (bool, error) { return fmt.Sprintf("%s@%s/%s", handler.ID(), job.Domain(), job.Resource()) } switch job.Request().Method { - case "GET": + case http.MethodGet: grh, ok := handler.(GetResourceHandler) if !ok { return false, errors.New(ErrNoGetHandler, errorMessages, id()) } return grh.Get(job) - case "HEAD": + case http.MethodHead: hrh, ok := handler.(HeadResourceHandler) if !ok { return false, errors.New(ErrNoHeadHandler, errorMessages, id()) } return hrh.Head(job) - case "PUT": + case http.MethodPut: prh, ok := handler.(PutResourceHandler) if !ok { return false, errors.New(ErrNoPutHandler, errorMessages, id()) } return prh.Put(job) - case "POST": + case http.MethodPost: prh, ok := handler.(PostResourceHandler) if !ok { return false, errors.New(ErrNoPostHandler, errorMessages, id()) } return prh.Post(job) - case "PATCH": + case http.MethodPatch: prh, ok := handler.(PatchResourceHandler) if !ok { return false, errors.New(ErrNoPatchHandler, errorMessages, id()) } return prh.Patch(job) - case "DELETE": + case http.MethodDelete: drh, ok := handler.(DeleteResourceHandler) if !ok { return false, errors.New(ErrNoDeleteHandler, errorMessages, id()) } return drh.Delete(job) - case "OPTIONS": + case http.MethodOptions: orh, ok := handler.(OptionsResourceHandler) if !ok { return false, errors.New(ErrNoOptionsHandler, errorMessages, id()) diff --git a/rest/rest_test.go b/rest/rest_test.go index a950692..2f92c5d 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -46,7 +46,7 @@ func TestGetJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") + req := restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) resp := ts.DoRequest(req) resp.AssertStatusEquals(200) @@ -65,13 +65,13 @@ func TestPutJSON(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "PUT", "/base/test/json/4711") + req := restaudit.NewRequest("PUT", "/base/test/json/4711") reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} - req.SetBody(restaudit.ApplicationJSON, reqData) + req.MarshalBody(assert, restaudit.ApplicationJSON, reqData) resp := ts.DoRequest(req) resp.AssertStatusEquals(200) respData := TestRequestData{} - resp.AssertBody(&respData) + resp.AssertUnmarshalledBody(&respData) assert.Equal(respData, reqData) } @@ -85,7 +85,7 @@ func TestGetXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "GET", "/base/test/xml/4711") + req := restaudit.NewRequest("GET", "/base/test/xml/4711") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationXML) resp := ts.DoRequest(req) resp.AssertStatusEquals(200) @@ -102,13 +102,13 @@ func TestPutXML(t *testing.T) { err := mux.Register("test", "xml", NewTestHandler("xml", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "PUT", "/base/test/xml/4711") + req := restaudit.NewRequest("PUT", "/base/test/xml/4711") reqData := TestRequestData{"foo", "bar", "4711", "0815", ""} - req.SetBody(restaudit.ApplicationXML, reqData) + req.MarshalBody(assert, restaudit.ApplicationXML, reqData) resp := ts.DoRequest(req) resp.AssertStatusEquals(200) respData := TestRequestData{} - resp.AssertBody(&respData) + resp.AssertUnmarshalledBody(&respData) assert.Equal(respData, reqData) } @@ -122,13 +122,13 @@ func TestPutGOB(t *testing.T) { err := mux.Register("test", "gob", NewTestHandler("putgob", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "POST", "/base/test/gob") + req := restaudit.NewRequest("POST", "/base/test/gob") reqData := TestCounterData{"test", 4711} - req.SetBody(restaudit.ApplicationGOB, reqData) + req.MarshalBody(assert, restaudit.ApplicationGOB, reqData) resp := ts.DoRequest(req) resp.AssertStatusEquals(200) respData := TestCounterData{} - resp.AssertBody(&respData) + resp.AssertUnmarshalledBody(&respData) assert.Equal(respData, reqData) } @@ -142,7 +142,7 @@ func TestLongPath(t *testing.T) { err := mux.Register("content", "blog", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "GET", "/base/content/blog/2014/09/30/just-a-test") + req := restaudit.NewRequest("GET", "/base/content/blog/2014/09/30/just-a-test") resp := ts.DoRequest(req) resp.AssertBodyContains(`Resource ID: 2014/09/30/just-a-test`) } @@ -157,7 +157,7 @@ func TestFallbackDefault(t *testing.T) { err := mux.Register("testing", "index", NewTestHandler("default", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "GET", "/base/x/y") + req := restaudit.NewRequest("GET", "/base/x/y") resp := ts.DoRequest(req) resp.AssertBodyContains(`Resource: y`) } @@ -176,11 +176,11 @@ func TestHandlerStack(t *testing.T) { }) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "GET", "/base/test/stack") + req := restaudit.NewRequest("GET", "/base/test/stack") resp := ts.DoRequest(req) resp.AssertBodyContains("Resource: token") resp.AssertHeaderEquals("Token", "foo") - req = restaudit.NewRequest(assert, "GET", "/base/test/stack") + req = restaudit.NewRequest("GET", "/base/test/stack") req.AddHeader("token", "foo") resp = ts.DoRequest(req) resp.AssertBodyContains("Resource: stack") @@ -198,20 +198,20 @@ func TestVersion(t *testing.T) { err := mux.Register("test", "json", NewTestHandler("json", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") + req := restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) resp := ts.DoRequest(req) resp.AssertStatusEquals(200) resp.AssertHeaderEquals("Version", "1.0.0") - req = restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") + req = restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.AddHeader("Version", "2") resp = ts.DoRequest(req) resp.AssertStatusEquals(200) resp.AssertHeaderEquals("Version", "2.0.0") - req = restaudit.NewRequest(assert, "GET", "/base/test/json/4711?foo=0815") + req = restaudit.NewRequest("GET", "/base/test/json/4711?foo=0815") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.AddHeader("Version", "3.0") resp = ts.DoRequest(req) @@ -271,7 +271,7 @@ func TestMethodNotSupported(t *testing.T) { err := mux.Register("test", "method", NewTestHandler("method", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest(assert, "OPTION", "/base/test/method") + req := restaudit.NewRequest("OPTION", "/base/test/method") resp := ts.DoRequest(req) resp.AssertBodyContains("OPTION") } diff --git a/restaudit/doc.go b/restaudit/doc.go index 7f24a15..fd22a84 100644 --- a/restaudit/doc.go +++ b/restaudit/doc.go @@ -10,6 +10,44 @@ // resource handlers. Requests can easily be created, marshalling data // based on the content-type is done automatically. Response also // provides assert methods for the tests. +// +// So first step is to create a test server and register the handler(s) +// to test. Could best be done with a little helper function, depending +// on own needs, e.g. when the context shall contain more information. +// +// assert := audit.NewTestingAssertion(t, true) +// cfgStr := "{etc {basepath /}{default-domain testing}{default-resource index}}" +// cfg, err := etc.ReadString(cfgStr) +// assert.Nil(err) +// mux := rest.NewMultiplexer(context.Background(), cfg) +// ts := restaudit.StartServer(mux, assert) +// defer ts.Close() +// err := mux.Register("my-domain", "my-resource", NewMyHandler()) +// assert.Nil(err) +// +// During the tests you create the requests with +// +// req := restaudit.NewRequest("GET", "/my-domain/my-resource/4711") +// req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) +// +// The request the is done with +// +// resp := ts.DoRequest(req) +// resp.AssertStatusEquals(200) +// rest.AssertHeaderContains(restaudit.HeaderContentType, restaudit.ApplicationJSON) +// resp.AssertBodyContains(`"ResourceID":"4711"`) +// +// Also data can be marshalled including setting the content type +// and the response can be unmarshalled based on that type. +// +// req.MarshalBody(assert, restaudit.ApplicationJSON, myInData) +// ... +// var myOutData MyType +// resp.AssertUnmarshalledBody(&myOutData) +// assert.Equal(myOutData.MyField, "foo") +// +// There are more helpers for a convenient test, but the fields of +// Request and Response can also be accessed directly. package restaudit // EOF diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 7278d92..bf1adf9 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -52,22 +52,23 @@ type KeyValues map[string]string // REQUEST //-------------------- +// RequestProcessor is for pre-processing HTTP requests. +type RequestProcessor func(req *http.Request) *http.Request + // Request wraps all infos for a test request. type Request struct { - Assert audit.Assertion Method string Path string Header KeyValues Cookies KeyValues Body []byte - RequestProcessor func(req *http.Request) *http.Request + RequestProcessor RequestProcessor } // NewRequest creates a new test request with the given method // and path. -func NewRequest(assert audit.Assertion, method, path string) *Request { +func NewRequest(method, path string) *Request { return &Request{ - Assert: assert, Method: method, Path: path, } @@ -91,27 +92,37 @@ func (r *Request) AddCookie(key, value string) *Request { return r } -// SetBody sets the request content based on the type and +// SetRequestProcessor sets the pre-processor. +func (r *Request) SetRequestProcessor(processor RequestProcessor) *Request { + r.RequestProcessor = processor + return r +} + +// MarshalBody sets the request body based on the type and // the marshalled data. -func (r *Request) SetBody(contentType string, data interface{}) *Request { +func (r *Request) MarshalBody( + assert audit.Assertion, + contentType string, + data interface{}, +) *Request { switch contentType { case ApplicationGOB: body := &bytes.Buffer{} enc := gob.NewEncoder(body) err := enc.Encode(data) - r.Assert.Nil(err, "cannot encode data to GOB") + assert.Nil(err, "cannot encode data to GOB") r.Body = body.Bytes() r.AddHeader(HeaderContentType, ApplicationGOB) r.AddHeader(HeaderAccept, ApplicationGOB) case ApplicationJSON: body, err := json.Marshal(data) - r.Assert.Nil(err, "cannot marshal data to JSON") + assert.Nil(err, "cannot marshal data to JSON") r.Body = body r.AddHeader(HeaderContentType, ApplicationJSON) r.AddHeader(HeaderAccept, ApplicationJSON) case ApplicationXML: body, err := xml.Marshal(data) - r.Assert.Nil(err, "cannot marshal data to XML") + assert.Nil(err, "cannot marshal data to XML") r.Body = body r.AddHeader(HeaderContentType, ApplicationXML) r.AddHeader(HeaderAccept, ApplicationXML) @@ -122,13 +133,18 @@ func (r *Request) SetBody(contentType string, data interface{}) *Request { // RenderTemplate renders the passed data into the template // and assigns it to the request body. The content type // will be set too. -func (r *Request) RenderTemplate(contentType string, templateSource string, data interface{}) *Request { +func (r *Request) RenderTemplate( + assert audit.Assertion, + contentType string, + templateSource string, + data interface{}, +) *Request { // Render template. t, err := template.New(r.Path).Parse(templateSource) - r.Assert.Nil(err, "cannot parse template") + assert.Nil(err, "cannot parse template") body := &bytes.Buffer{} err = t.Execute(body, data) - r.Assert.Nil(err, "cannot render template") + assert.Nil(err, "cannot render template") r.Body = body.Bytes() // Set content type. r.AddHeader(HeaderContentType, contentType) @@ -142,7 +158,7 @@ func (r *Request) RenderTemplate(contentType string, templateSource string, data // Response wraps all infos of a test response. type Response struct { - Assert audit.Assertion + assert audit.Assertion Status int Header KeyValues Cookies KeyValues @@ -151,14 +167,14 @@ type Response struct { // AssertStatusEquals checks if the status is the expected one. func (r *Response) AssertStatusEquals(expected int) { - r.Assert.Equal(r.Status, expected, "response status differs") + r.assert.Equal(r.Status, expected, "response status differs") } // AssertHeader checks if a header exists and retrieves it. func (r *Response) AssertHeader(key string) string { - r.Assert.NotEmpty(r.Header, "response contains no header") + r.assert.NotEmpty(r.Header, "response contains no header") value, ok := r.Header[key] - r.Assert.True(ok, "header '"+key+"' not found") + r.assert.True(ok, "header '"+key+"' not found") return value } @@ -166,21 +182,21 @@ func (r *Response) AssertHeader(key string) string { // it to an expected one. func (r *Response) AssertHeaderEquals(key, expected string) { value := r.AssertHeader(key) - r.Assert.Equal(value, expected, "header value is not equal to expected") + r.assert.Equal(value, expected, "header value is not equal to expected") } // AssertHeaderContains checks if a header exists and looks for // an expected part. func (r *Response) AssertHeaderContains(key, expected string) { value := r.AssertHeader(key) - r.Assert.Substring(expected, value, "header value does not contain expected") + r.assert.Substring(expected, value, "header value does not contain expected") } // AssertCookie checks if a cookie exists and retrieves it. func (r *Response) AssertCookie(key string) string { - r.Assert.NotEmpty(r.Cookies, "response contains no cookies") + r.assert.NotEmpty(r.Cookies, "response contains no cookies") value, ok := r.Cookies[key] - r.Assert.True(ok, "cookie '"+key+"' not found") + r.assert.True(ok, "cookie '"+key+"' not found") return value } @@ -188,48 +204,48 @@ func (r *Response) AssertCookie(key string) string { // it to an expected one. func (r *Response) AssertCookieEquals(key, expected string) { value := r.AssertCookie(key) - r.Assert.Equal(value, expected, "cookie value is not equal to expected") + r.assert.Equal(value, expected, "cookie value is not equal to expected") } // AssertCookieContains checks if a cookie exists and looks for // an expected part. func (r *Response) AssertCookieContains(key, expected string) { value := r.AssertCookie(key) - r.Assert.Substring(expected, value, "cookie value does not contain expected") + r.assert.Substring(expected, value, "cookie value does not contain expected") } -// AssertBody retrieves the body based on the content type +// AssertUnmarshalledBody retrieves the body based on the content type // and unmarshals it accordingly. -func (r *Response) AssertBody(data interface{}) { +func (r *Response) AssertUnmarshalledBody(data interface{}) { contentType, ok := r.Header[HeaderContentType] - r.Assert.True(ok) + r.assert.True(ok) switch contentType { case ApplicationGOB: body := bytes.NewBuffer(r.Body) dec := gob.NewDecoder(body) err := dec.Decode(data) - r.Assert.Nil(err, "cannot decode GOB body") + r.assert.Nil(err, "cannot decode GOB body") case ApplicationJSON: err := json.Unmarshal(r.Body, data) - r.Assert.Nil(err, "cannot unmarshal JSON body") + r.assert.Nil(err, "cannot unmarshal JSON body") case ApplicationXML: err := xml.Unmarshal(r.Body, data) - r.Assert.Nil(err, "cannot unmarshal XML body") + r.assert.Nil(err, "cannot unmarshal XML body") default: - r.Assert.Fail("unknown content type: " + contentType) + r.assert.Fail("unknown content type: " + contentType) } } // AssertBodyMatches checks if the body matches a regular expression. func (r *Response) AssertBodyMatches(pattern string) { ok, err := regexp.MatchString(pattern, string(r.Body)) - r.Assert.Nil(err, "illegal content match pattern") - r.Assert.True(ok, "body doesn't match pattern") + r.assert.Nil(err, "illegal content match pattern") + r.assert.True(ok, "body doesn't match pattern") } // AssertBodyContains checks if the body contains a string. func (r *Response) AssertBodyContains(expected string) { - r.Assert.Contents(expected, r.Body, "body doesn't contains expected") + r.assert.Contents(expected, r.Body, "body doesn't contains expected") } //-------------------- @@ -276,9 +292,6 @@ func (ts *testServer) DoRequest(req *Request) *Response { c := &http.Client{Transport: transport} url := ts.server.URL + req.Path var bodyReader io.Reader - if req.Assert == nil { - req.Assert = ts.assert - } if req.Body != nil { bodyReader = ioutil.NopCloser(bytes.NewBuffer(req.Body)) } @@ -294,7 +307,7 @@ func (ts *testServer) DoRequest(req *Request) *Response { } httpReq.AddCookie(cookie) } - // Check if request shall be processed before performed. + // Check if request shall be pre-processed before performed. if req.RequestProcessor != nil { httpReq = req.RequestProcessor(httpReq) } @@ -339,7 +352,7 @@ func (ts *testServer) response(hr *http.Response) *Response { ts.assert.Nil(err, "cannot read response") defer hr.Body.Close() return &Response{ - Assert: ts.assert, + assert: ts.assert, Status: hr.StatusCode, Header: respHeader, Cookies: respCookies, From b06c29c7ab5359e51990eb1aad41085a3c5f9559 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 20 Mar 2017 16:06:20 +0100 Subject: [PATCH 095/127] Former envelope is now a Feedback (#8) Can be retrieved from request.Response or in restaudit tests. --- CHANGELOG.md | 7 ++++++- README.md | 2 +- request/request.go | 14 ++++++++++++++ request/request_test.go | 39 +++++++++++++++++++++++++++++++++++++++ rest/formatter.go | 36 ++++++++++++++++++------------------ restaudit/restaudit.go | 9 +++++++++ 6 files changed, 87 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b8dd7..b10d1b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # Tideland Go REST Server Library +## 2017-03-XX + +- Rename internal *envelope* to public *Feedback* in *rest* +- Added *ReadFeedback()* to *Response* in *request* + ## 2017-02-12 - Some renamings in *Request* and *Response*, sadly - to the previous minor release + incompatible to the previous minor release - More convenience helpers for testing - Adopted new testing to more packages - Using http package constants instead of own diff --git a/README.md b/README.md index 4b3616c..990ad9a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ I hope you like it. ;) ## Version -Version 2.12.0 +Version 2.13.0 ## Packages diff --git a/request/request.go b/request/request.go index bc421a9..0f0aebe 100644 --- a/request/request.go +++ b/request/request.go @@ -128,6 +128,10 @@ type Response interface { // Read decodes the content into the passed data depending // on the content type. Read(data interface{}) error + + // ReadFeedback tries to unmarshal the content of the + // response into a rest package feedback. + ReadFeedback() (rest.Feedback, bool) } // response implements Response. @@ -197,6 +201,16 @@ func (r *response) Read(data interface{}) error { return errors.New(ErrInvalidContentType, errorMessages, r.contentType) } +// ReadFeedback implements the Response interface. +func (r *response) ReadFeedback() (rest.Feedback, bool) { + fb := rest.Feedback{} + err := r.Read(&fb) + if err != nil { + return rest.Feedback{}, false + } + return fb, true +} + //-------------------- // CALL PARAMETERS //-------------------- diff --git a/request/request_test.go b/request/request_test.go index bdad5a4..da782b3 100644 --- a/request/request_test.go +++ b/request/request_test.go @@ -90,6 +90,37 @@ var tests = []struct { assert.Nil(err) assert.Equal(values["name"][0], "foo") }, + }, { + name: "GET returns a positive feedback", + method: "GET", + resource: "item", + id: "positive-feedback", + params: &request.Parameters{ + Accept: rest.ContentTypeJSON, + }, + check: func(assert audit.Assertion, response request.Response) { + fb, ok := response.ReadFeedback() + assert.True(ok) + assert.Equal(fb.StatusCode, rest.StatusOK) + assert.Equal(fb.Status, "success") + assert.Equal(fb.Message, "positive feedback") + assert.Equal(fb.Payload, "ok") + }, + }, { + name: "GET returns a negative feedback", + method: "GET", + resource: "item", + id: "negative-feedback", + params: &request.Parameters{ + Accept: rest.ContentTypeJSON, + }, + check: func(assert audit.Assertion, response request.Response) { + fb, ok := response.ReadFeedback() + assert.True(ok) + assert.Equal(fb.StatusCode, rest.StatusBadRequest) + assert.Equal(fb.Status, "fail") + assert.Equal(fb.Message, "negative feedback") + }, }, { name: "HEAD returns the resource ID as header", method: "HEAD", @@ -265,6 +296,14 @@ func (th *TestHandler) Init(env rest.Environment, domain, resource string) error func (th *TestHandler) Get(job rest.Job) (bool, error) { th.assert.Logf("handler #%d: GET", th.index) + // Special behavior for feedback tests. + switch job.ResourceID() { + case "positive-feedback": + return rest.PositiveFeedback(job.JSON(true), "ok", "positive feedback") + case "negative-feedback": + return rest.NegativeFeedback(job.JSON(true), rest.StatusBadRequest, "negative feedback") + } + // Regular behavior. content := &Content{ Index: th.index, Version: 1, diff --git a/rest/formatter.go b/rest/formatter.go index 63ea318..826bb88 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -61,19 +61,34 @@ var ( ) //-------------------- -// ENVELOPE +// FEEDBACK //-------------------- -// envelope is a helper to give a qualified feedback in RESTful requests. +// Feedback is a helper to give a qualified feedback in RESTful requests. // It contains wether the request has been successful, a message, and in // case of success some payload if wanted. -type envelope struct { +type Feedback struct { StatusCode int `json:"statusCode" xml:"statusCode"` Status string `json:"status" xml:"status"` Message string `json:"message,omitempty" xml:"message,omitempty"` Payload interface{} `json:"payload,omitempty" xml:"payload,omitempty"` } +// PositiveFeedback writes a positive feedback envelope to the formatter. +func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) (bool, error) { + fmsg := fmt.Sprintf(msg, args...) + return false, f.Write(StatusOK, Feedback{StatusOK, "success", fmsg, payload}) +} + +// NegativeFeedback writes a negative feedback envelope to the formatter. +// The message is also logged. +func NegativeFeedback(f Formatter, statusCode int, msg string, args ...interface{}) (bool, error) { + fmsg := fmt.Sprintf(msg, args...) + lmsg := fmt.Sprintf("(status code %d) "+fmsg, statusCode) + logger.Warningf(lmsg) + return false, f.Write(statusCode, Feedback{statusCode, "fail", fmsg, nil}) +} + //-------------------- // FORMATTER //-------------------- @@ -90,21 +105,6 @@ type Formatter interface { Read(data interface{}) error } -// PositiveFeedback writes a positive feedback envelope to the formatter. -func PositiveFeedback(f Formatter, payload interface{}, msg string, args ...interface{}) (bool, error) { - fmsg := fmt.Sprintf(msg, args...) - return false, f.Write(StatusOK, envelope{StatusOK, "success", fmsg, payload}) -} - -// NegativeFeedback writes a negative feedback envelope to the formatter. -// The message is also logged. -func NegativeFeedback(f Formatter, statusCode int, msg string, args ...interface{}) (bool, error) { - fmsg := fmt.Sprintf(msg, args...) - lmsg := fmt.Sprintf("(status code %d) "+fmsg, statusCode) - logger.Warningf(lmsg) - return false, f.Write(statusCode, envelope{statusCode, "fail", fmsg, nil}) -} - //-------------------- // GOB FORMATTER //-------------------- diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index bf1adf9..119afac 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/tideland/golib/audit" + "github.com/tideland/gorest/rest" ) //-------------------- @@ -236,6 +237,14 @@ func (r *Response) AssertUnmarshalledBody(data interface{}) { } } +// AssertUnmarshalledFeedback retrieves a rest.Feedback as body out of +// response and returns it for further tests. +func (r *Response) AssertUnmarshalledFeedback() rest.Feedback { + fb := rest.Feedback{} + r.AssertUnmarshalledBody(&fb) + return fb +} + // AssertBodyMatches checks if the body matches a regular expression. func (r *Response) AssertBodyMatches(pattern string) { ok, err := regexp.MatchString(pattern, string(r.Body)) From 7f44dd35a64669d5678b4b2326f0e0bee480ce8a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 20 Mar 2017 16:19:48 +0100 Subject: [PATCH 096/127] Changed callstack offset handling in restaudit (#12) --- CHANGELOG.md | 4 +++- restaudit/restaudit.go | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b10d1b1..b997eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Rename internal *envelope* to public *Feedback* in *rest* - Added *ReadFeedback()* to *Response* in *request* +- Asserts in *restaudit* now internally increase the callstack + offset so that the correct test line number is shown ## 2017-02-12 @@ -13,7 +15,7 @@ - Adopted new testing to more packages - Using http package constants instead of own plain strings -- Added documentation to restaudit +- Added documentation to *restaudit* ## 2017-02-10 diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 119afac..18ef101 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -168,11 +168,15 @@ type Response struct { // AssertStatusEquals checks if the status is the expected one. func (r *Response) AssertStatusEquals(expected int) { + restore := r.assert.IncrCallstackOffset() + defer restore() r.assert.Equal(r.Status, expected, "response status differs") } // AssertHeader checks if a header exists and retrieves it. func (r *Response) AssertHeader(key string) string { + restore := r.assert.IncrCallstackOffset() + defer restore() r.assert.NotEmpty(r.Header, "response contains no header") value, ok := r.Header[key] r.assert.True(ok, "header '"+key+"' not found") @@ -182,6 +186,8 @@ func (r *Response) AssertHeader(key string) string { // AssertHeaderEquals checks if a header exists and compares // it to an expected one. func (r *Response) AssertHeaderEquals(key, expected string) { + restore := r.assert.IncrCallstackOffset() + defer restore() value := r.AssertHeader(key) r.assert.Equal(value, expected, "header value is not equal to expected") } @@ -189,12 +195,16 @@ func (r *Response) AssertHeaderEquals(key, expected string) { // AssertHeaderContains checks if a header exists and looks for // an expected part. func (r *Response) AssertHeaderContains(key, expected string) { + restore := r.assert.IncrCallstackOffset() + defer restore() value := r.AssertHeader(key) r.assert.Substring(expected, value, "header value does not contain expected") } // AssertCookie checks if a cookie exists and retrieves it. func (r *Response) AssertCookie(key string) string { + restore := r.assert.IncrCallstackOffset() + defer restore() r.assert.NotEmpty(r.Cookies, "response contains no cookies") value, ok := r.Cookies[key] r.assert.True(ok, "cookie '"+key+"' not found") @@ -204,6 +214,8 @@ func (r *Response) AssertCookie(key string) string { // AssertCookieEquals checks if a cookie exists and compares // it to an expected one. func (r *Response) AssertCookieEquals(key, expected string) { + restore := r.assert.IncrCallstackOffset() + defer restore() value := r.AssertCookie(key) r.assert.Equal(value, expected, "cookie value is not equal to expected") } @@ -211,6 +223,8 @@ func (r *Response) AssertCookieEquals(key, expected string) { // AssertCookieContains checks if a cookie exists and looks for // an expected part. func (r *Response) AssertCookieContains(key, expected string) { + restore := r.assert.IncrCallstackOffset() + defer restore() value := r.AssertCookie(key) r.assert.Substring(expected, value, "cookie value does not contain expected") } @@ -218,6 +232,8 @@ func (r *Response) AssertCookieContains(key, expected string) { // AssertUnmarshalledBody retrieves the body based on the content type // and unmarshals it accordingly. func (r *Response) AssertUnmarshalledBody(data interface{}) { + restore := r.assert.IncrCallstackOffset() + defer restore() contentType, ok := r.Header[HeaderContentType] r.assert.True(ok) switch contentType { @@ -240,6 +256,8 @@ func (r *Response) AssertUnmarshalledBody(data interface{}) { // AssertUnmarshalledFeedback retrieves a rest.Feedback as body out of // response and returns it for further tests. func (r *Response) AssertUnmarshalledFeedback() rest.Feedback { + restore := r.assert.IncrCallstackOffset() + defer restore() fb := rest.Feedback{} r.AssertUnmarshalledBody(&fb) return fb @@ -247,6 +265,8 @@ func (r *Response) AssertUnmarshalledFeedback() rest.Feedback { // AssertBodyMatches checks if the body matches a regular expression. func (r *Response) AssertBodyMatches(pattern string) { + restore := r.assert.IncrCallstackOffset() + defer restore() ok, err := regexp.MatchString(pattern, string(r.Body)) r.assert.Nil(err, "illegal content match pattern") r.assert.True(ok, "body doesn't match pattern") @@ -254,6 +274,8 @@ func (r *Response) AssertBodyMatches(pattern string) { // AssertBodyContains checks if the body contains a string. func (r *Response) AssertBodyContains(expected string) { + restore := r.assert.IncrCallstackOffset() + defer restore() r.assert.Contents(expected, r.Body, "body doesn't contains expected") } From a0f88b071fe97443057ec91f274e1786f473ac64 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 20 Mar 2017 17:05:48 +0100 Subject: [PATCH 097/127] Added grep to restaudit response (#9) --- CHANGELOG.md | 1 + handlers/handlers_test.go | 2 ++ restaudit/restaudit.go | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b997eb7..b4c4b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added *ReadFeedback()* to *Response* in *request* - Asserts in *restaudit* now internally increase the callstack offset so that the correct test line number is shown +- Added *Response.AssertBodyGrep()* to *restaudit* ## 2017-02-12 diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index a063217..7510ba9 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -54,6 +54,8 @@ func TestWrapperHandler(t *testing.T) { req := restaudit.NewRequest("GET", "/test/wrapper") resp := ts.DoRequest(req) resp.AssertBodyContains(data) + punctuation := resp.AssertBodyGrep("[,!]") + assert.Length(punctuation, 2) } // TestFileServeHandler tests the serving of files. diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index 18ef101..c04c1f2 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -272,6 +272,15 @@ func (r *Response) AssertBodyMatches(pattern string) { r.assert.True(ok, "body doesn't match pattern") } +// AssertBodyGrep greps content out of the body. +func (r *Response) AssertBodyGrep(pattern string) []string { + restore := r.assert.IncrCallstackOffset() + defer restore() + expr, err := regexp.Compile(pattern) + r.assert.Nil(err, "illegal content grep pattern") + return expr.FindAllString(string(r.Body), -1) +} + // AssertBodyContains checks if the body contains a string. func (r *Response) AssertBodyContains(expected string) { restore := r.assert.IncrCallstackOffset() From 10c9986fb34c65918248c77815be63436d840064 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 20 Mar 2017 17:08:32 +0100 Subject: [PATCH 098/127] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c4b53..38f3a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Tideland Go REST Server Library -## 2017-03-XX +## 2017-03-20 - Rename internal *envelope* to public *Feedback* in *rest* - Added *ReadFeedback()* to *Response* in *request* From a6adf23dc27b0018d9930b9ad428657c78fd23cd Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 9 Apr 2017 19:19:52 +0200 Subject: [PATCH 099/127] Added Go Report Card badge to README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 990ad9a..f0b1bda 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ The library earlier has been known as `web` package of the I hope you like it. ;) +[![Sourcegraph](https://sourcegraph.com/github.com/tideland/gorest/-/badge.svg)](https://sourcegraph.com/github.com/tideland/gorest?badge) +[![Go Report Card](https://goreportcard.com/badge/github.com/tideland/gorest)](https://goreportcard.com/report/github.com/tideland/gorest) + ## Version Version 2.13.0 From d35b1f19353c27aac4adb1c7eb63a333d5fb6103 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 10 Apr 2017 16:42:53 +0200 Subject: [PATCH 100/127] Return prefilled request feedbacks in case of unmarshalling errors --- request/request.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/request/request.go b/request/request.go index 0f0aebe..d999ab1 100644 --- a/request/request.go +++ b/request/request.go @@ -206,7 +206,12 @@ func (r *response) ReadFeedback() (rest.Feedback, bool) { fb := rest.Feedback{} err := r.Read(&fb) if err != nil { - return rest.Feedback{}, false + return rest.Feedback{ + StatusCode: -1, + Status: "fail", + Message: err.Error(), + Payload: r.content, + }, false } return fb, true } From c8a5b52fa30720a3ee5aa4ce779f3cf0020cbfc1 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 12 Apr 2017 13:34:47 +0200 Subject: [PATCH 101/127] Started QA changes --- handlers/errors.go | 1 + jwt/algorithm.go | 19 ++++++++++--------- jwt/cache.go | 2 +- jwt/claims.go | 2 +- jwt/claims_test.go | 8 ++++---- jwt/errors.go | 1 + jwt/jwt.go | 11 +++++++---- jwt/key.go | 3 +-- request/errors.go | 1 + request/request.go | 6 ++++-- rest/environment.go | 1 + rest/errors.go | 1 + rest/formatter.go | 8 ++++++-- rest/job.go | 2 +- restaudit/restaudit.go | 1 + 15 files changed, 41 insertions(+), 26 deletions(-) diff --git a/handlers/errors.go b/handlers/errors.go index f37b7cf..bf09114 100644 --- a/handlers/errors.go +++ b/handlers/errors.go @@ -19,6 +19,7 @@ import ( // CONSTANTS //-------------------- +// Error codes of the handlers package. const ( ErrUploadingFile = iota + 1 ErrDownloadingFile diff --git a/jwt/algorithm.go b/jwt/algorithm.go index 76dfe84..2925e0a 100644 --- a/jwt/algorithm.go +++ b/jwt/algorithm.go @@ -17,11 +17,13 @@ import ( "crypto/hmac" "crypto/rand" "crypto/rsa" - _ "crypto/sha256" - _ "crypto/sha512" "encoding/asn1" "math/big" + // Import hashing packages just to register them via init(). + _ "crypto/sha256" + _ "crypto/sha512" + "github.com/tideland/golib/errors" ) @@ -150,14 +152,13 @@ func (a Algorithm) sign(data []byte, k Key, h crypto.Hash) (Signature, error) { return nil, errors.Annotate(err, ErrCannotSign, errorMessages) } return Signature(sig), nil - } else { - // RSA. - sig, err := rsa.SignPKCS1v15(rand.Reader, key, h, hashSum()) - if err != nil { - return nil, errors.Annotate(err, ErrCannotSign, errorMessages) - } - return Signature(sig), nil } + // RSA. + sig, err := rsa.SignPKCS1v15(rand.Reader, key, h, hashSum()) + if err != nil { + return nil, errors.Annotate(err, ErrCannotSign, errorMessages) + } + return Signature(sig), nil case string: // None algorithm. if a != "none" { diff --git a/jwt/cache.go b/jwt/cache.go index 3a58c1c..afade47 100644 --- a/jwt/cache.go +++ b/jwt/cache.go @@ -62,7 +62,7 @@ type cache struct { // The duration of the interval controls how often the background // cleanup is running. Final configuration parameter is the maximum // number of entries inside the cache. If these grow too fast the -// ttl will be temporarilly reduced for cleanup. +// ttl will be temporarily reduced for cleanup. func NewCache(ttl, leeway, interval time.Duration, maxEntries int) Cache { c := &cache{ entries: map[string]*cacheEntry{}, diff --git a/jwt/claims.go b/jwt/claims.go index 343c4f1..c0af70a 100644 --- a/jwt/claims.go +++ b/jwt/claims.go @@ -422,7 +422,7 @@ func (c Claims) MarshalJSON() ([]byte, error) { return b, nil } -// MarshalJSON implements the json.Marshaller interface. +// UnmarshalJSON implements the json.Marshaller interface. func (c *Claims) UnmarshalJSON(b []byte) error { if b == nil { return nil diff --git a/jwt/claims_test.go b/jwt/claims_test.go index d07e3b2..e0ed5ab 100644 --- a/jwt/claims_test.go +++ b/jwt/claims_test.go @@ -273,7 +273,7 @@ func TestClaimsAudience(t *testing.T) { assert.True(ok) old := claims.DeleteAudience() assert.Equal(old, aud) - aud, ok = claims.Audience() + _, ok = claims.Audience() assert.False(ok) } @@ -313,7 +313,7 @@ func TestClaimsIdentifier(t *testing.T) { assert.True(ok) old := claims.DeleteIdentifier() assert.Equal(old, jti) - jti, ok = claims.Identifier() + _, ok = claims.Identifier() assert.False(ok) } @@ -353,7 +353,7 @@ func TestClaimsIssuer(t *testing.T) { assert.True(ok) old := claims.DeleteIssuer() assert.Equal(old, iss) - iss, ok = claims.Issuer() + _, ok = claims.Issuer() assert.False(ok) } @@ -393,7 +393,7 @@ func TestClaimsSubject(t *testing.T) { assert.True(ok) old := claims.DeleteSubject() assert.Equal(old, sub) - sub, ok = claims.Subject() + _, ok = claims.Subject() assert.False(ok) } diff --git a/jwt/errors.go b/jwt/errors.go index c385c64..1d1c51f 100644 --- a/jwt/errors.go +++ b/jwt/errors.go @@ -19,6 +19,7 @@ import ( // CONSTANTS //-------------------- +// Error codes of the JWT package. const ( ErrCannotEncode = iota + 1 ErrCannotDecode diff --git a/jwt/jwt.go b/jwt/jwt.go index c696ce1..4d8401f 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -29,8 +29,9 @@ import ( // key for the storage of values in a context. type key int -// jwtKey for the storrage of a JWT. -var jwtKey key = 0 +const ( + jwtKey key = iota +) // NewContext returns a new context that carries a token. func NewContext(ctx context.Context, token JWT) context.Context { @@ -47,6 +48,8 @@ func FromContext(ctx context.Context) (JWT, bool) { // JSON Web Token //-------------------- +// JWT describes the interface to access the parts of a +// JSON Web Token. type JWT interface { // Stringer provides the String() method. fmt.Stringer @@ -79,7 +82,7 @@ type jwt struct { token string } -// Encodes creates a JSON Web Token for the given claims +// Encode creates a JSON Web Token for the given claims // based on key and algorithm. func Encode(claims Claims, key Key, algorithm Algorithm) (JWT, error) { jwt := &jwt{ diff --git a/jwt/key.go b/jwt/key.go index c4dcbc8..90dab28 100644 --- a/jwt/key.go +++ b/jwt/key.go @@ -1,6 +1,6 @@ // Tideland Go REST Server Library - JSON Web Token - Keys // -// Copyright (C) 2016 Frank Mueller / Tideland / Oldenburg / Germany +// Copyright (C) 2016-2017 Frank Mueller / Tideland / Oldenburg / Germany // // All rights reserved. Use of this source code is governed // by the new BSD license. @@ -28,7 +28,6 @@ import ( // Key is the used key to sign a token. The real implementation // controls signing and verification. - type Key interface{} // ReadECPrivateKey reads a PEM formated ECDSA private key diff --git a/request/errors.go b/request/errors.go index afd1979..7b85e6a 100644 --- a/request/errors.go +++ b/request/errors.go @@ -19,6 +19,7 @@ import ( // CONSTANTS //-------------------- +// Error codes of the request package. const ( ErrNoServerDefined = iota + 1 ErrCannotPrepareRequest diff --git a/request/request.go b/request/request.go index 0f0aebe..2d23279 100644 --- a/request/request.go +++ b/request/request.go @@ -40,7 +40,9 @@ import ( // key is to address the servers inside a context. type key int -var serversKey key = 0 +const ( + serversKey key = iota +) // server contains the configuration of one server. type server struct { @@ -82,7 +84,7 @@ func (s *servers) Add(domain, url string, transport *http.Transport) { s.servers[domain] = append(srvs, &server{url, transport}) return } - s.servers[domain] = []*server{&server{url, transport}} + s.servers[domain] = []*server{{url, transport}} } // Caller implements the Servers interface. diff --git a/rest/environment.go b/rest/environment.go index d350218..37c514d 100644 --- a/rest/environment.go +++ b/rest/environment.go @@ -22,6 +22,7 @@ import ( // ENVIRONMENT //-------------------- +// Environment describes the environment of a RESTful application. type Environment interface { // Context returns the context of the environment. Context() context.Context diff --git a/rest/errors.go b/rest/errors.go index 3803501..6525b5f 100644 --- a/rest/errors.go +++ b/rest/errors.go @@ -19,6 +19,7 @@ import ( // CONSTANTS //-------------------- +// Error codes of the rest package. const ( ErrDuplicateHandler = iota + 1 ErrInitHandler diff --git a/rest/formatter.go b/rest/formatter.go index 826bb88..b0e2a1f 100644 --- a/rest/formatter.go +++ b/rest/formatter.go @@ -31,8 +31,8 @@ import ( // CONST //-------------------- +// Standard REST status codes. const ( - // Standard REST status codes. StatusOK = http.StatusOK StatusCreated = http.StatusCreated StatusNoContent = http.StatusNoContent @@ -42,8 +42,10 @@ const ( StatusNotFound = http.StatusNotFound StatusConflict = http.StatusConflict StatusInternalServerError = http.StatusInternalServerError +) - // Standard REST content types. +// Standard REST content types. +const ( ContentTypePlain = "text/plain" ContentTypeHTML = "text/html" ContentTypeXML = "application/xml" @@ -93,6 +95,8 @@ func NegativeFeedback(f Formatter, statusCode int, msg string, args ...interface // FORMATTER //-------------------- +// Formatter allows reading or writing in handler methods based on the +// implementing formats like JSON, XML, or GOB. type Formatter interface { // Write encodes the passed data to implementers format and writes // it with the passed status code and possible header values to the diff --git a/rest/job.go b/rest/job.go index 29b53a0..e26f37a 100644 --- a/rest/job.go +++ b/rest/job.go @@ -259,7 +259,7 @@ func (j *job) Languages() Languages { languages = append(languages, Language{lv[0], value}) } } - sort.Reverse(languages) + languages = sort.Reverse(languages).(Languages) return languages } diff --git a/restaudit/restaudit.go b/restaudit/restaudit.go index c04c1f2..1783a5a 100644 --- a/restaudit/restaudit.go +++ b/restaudit/restaudit.go @@ -33,6 +33,7 @@ import ( // CONSTENTS //-------------------- +// Request and response header fields and values for testing purposes. const ( HeaderAccept = "Accept" HeaderContentType = "Content-Type" From 42ce41487820b44796157dc67a20add87012c125 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 12 Apr 2017 22:43:46 +0200 Subject: [PATCH 102/127] Reduced request complexity --- request/request.go | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/request/request.go b/request/request.go index 2d23279..e5e88fc 100644 --- a/request/request.go +++ b/request/request.go @@ -364,10 +364,26 @@ func (c *caller) Options(resource, resourceID string, params *Parameters) (Respo // request performs all requests. func (c *caller) request(method, resource, resourceID string, params *Parameters) (Response, error) { - if params == nil { - params = &Parameters{} + // Preparation. + client, urlStr, err := c.prepareClient(resource, resourceID) + if err != nil { + return nil, err } - // Prepare client. + request, err := c.prepareRequest(method, urlStr, params) + if err != nil { + return nil, err + } + // Perform request. + response, err := client.Do(request) + if err != nil { + return nil, errors.Annotate(err, ErrHTTPRequestFailed, errorMessages) + } + // Analyze response. + return analyzeResponse(response) +} + +// prepareClient prepares the client and the URL for the call. +func (c *caller) prepareClient(resource, resourceID string) (*http.Client, string, error) { // TODO Mue 2016-10-28 Add more algorithms than just random selection. srv := c.srvs[rand.Intn(len(c.srvs))] client := &http.Client{} @@ -376,7 +392,7 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters } u, err := url.Parse(srv.URL) if err != nil { - return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) + return nil, "", errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) } upath := strings.Trim(u.Path, "/") path := []string{upath, c.domain, resource} @@ -384,11 +400,19 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters path = append(path, resourceID) } u.Path = strings.Join(path, "/") - // Prepare request, check the parameters first. + return client, u.String(), nil +} + +// prepareRequest prepares the request to perform. +func (c *caller) prepareRequest(method, urlStr string, params *Parameters) (*http.Request, error) { + if params == nil { + params = &Parameters{} + } var request *http.Request + var err error if method == "GET" || method == "HEAD" { // These allow only URL encoded. - request, err = http.NewRequest(method, u.String(), nil) + request, err = http.NewRequest(method, urlStr, nil) if err != nil { return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) } @@ -404,7 +428,7 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters if err != nil { return nil, err } - request, err = http.NewRequest(method, u.String(), body) + request, err = http.NewRequest(method, urlStr, body) if err != nil { return nil, errors.Annotate(err, ErrCannotPrepareRequest, errorMessages) } @@ -422,13 +446,7 @@ func (c *caller) request(method, resource, resourceID string, params *Parameters if params.Accept != "" { request.Header.Set("Accept", params.Accept) } - // Perform request. - response, err := client.Do(request) - if err != nil { - return nil, errors.Annotate(err, ErrHTTPRequestFailed, errorMessages) - } - // Analyze response. - return analyzeResponse(response) + return request, nil } // analyzeResponse creates a response struct out of the HTTP response. From 4c78074b58ffe38208083fca70ec135205f2cd10 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 13 Apr 2017 17:32:19 +0200 Subject: [PATCH 103/127] Finalized QA changes --- README.md | 2 +- jwt/algorithm.go | 213 +++++++++++++++++++++++++++-------------------- 2 files changed, 123 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index f0b1bda..858f6b5 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ I hope you like it. ;) ## Version -Version 2.13.0 +Version 2.13.1 ## Packages diff --git a/jwt/algorithm.go b/jwt/algorithm.go index 2925e0a..abb42fe 100644 --- a/jwt/algorithm.go +++ b/jwt/algorithm.go @@ -107,58 +107,16 @@ func (a Algorithm) isRSAPSS() bool { // sign signs the passed data based on the key and the passed hash. func (a Algorithm) sign(data []byte, k Key, h crypto.Hash) (Signature, error) { - hashSum := func() []byte { - hasher := h.New() - hasher.Write(data) - return hasher.Sum(nil) - } switch key := k.(type) { case *ecdsa.PrivateKey: // ECDSA algorithms. - if a[0] != 'E' { - return nil, errors.New(ErrInvalidCombination, errorMessages, a, "ECDSA") - } - r, s, err := ecdsa.Sign(rand.Reader, key, hashSum()) - if err != nil { - return nil, errors.Annotate(err, ErrCannotSign, errorMessages) - } - sig, err := asn1.Marshal(ecPoint{r, s}) - if err != nil { - return nil, errors.Annotate(err, ErrCannotSign, errorMessages) - } - return Signature(sig), nil + return a.signECDSA(data, key, h) case []byte: // HMAC algorithms. - if a[0] != 'H' { - return nil, errors.New(ErrInvalidCombination, errorMessages, a, "HMAC") - } - hasher := hmac.New(h.New, key) - hasher.Write(data) - sig := hasher.Sum(nil) - return Signature(sig), nil + return a.signHMAC(data, key, h) case *rsa.PrivateKey: // RSA and RSAPSS algorithms. - if a[0] != 'P' && a[0] != 'R' { - return nil, errors.New(ErrInvalidCombination, errorMessages, a, "RSA(PSS)") - } - if a.isRSAPSS() { - // RSAPSS. - options := &rsa.PSSOptions{ - SaltLength: rsa.PSSSaltLengthAuto, - Hash: h, - } - sig, err := rsa.SignPSS(rand.Reader, key, h, hashSum(), options) - if err != nil { - return nil, errors.Annotate(err, ErrCannotSign, errorMessages) - } - return Signature(sig), nil - } - // RSA. - sig, err := rsa.SignPKCS1v15(rand.Reader, key, h, hashSum()) - if err != nil { - return nil, errors.Annotate(err, ErrCannotSign, errorMessages) - } - return Signature(sig), nil + return a.signRSA(data, key, h) case string: // None algorithm. if a != "none" { @@ -171,62 +129,71 @@ func (a Algorithm) sign(data []byte, k Key, h crypto.Hash) (Signature, error) { } } +// signECDSA signs the data using the ECDSA algorithm. +func (a Algorithm) signECDSA(data []byte, key *ecdsa.PrivateKey, h crypto.Hash) (Signature, error) { + if a[0] != 'E' { + return nil, errors.New(ErrInvalidCombination, errorMessages, a, "ECDSA") + } + r, s, err := ecdsa.Sign(rand.Reader, key, hashSum(data, h)) + if err != nil { + return nil, errors.Annotate(err, ErrCannotSign, errorMessages) + } + sig, err := asn1.Marshal(ecPoint{r, s}) + if err != nil { + return nil, errors.Annotate(err, ErrCannotSign, errorMessages) + } + return Signature(sig), nil +} + +// signHMAC signs the data using the HMAC algorithm. +func (a Algorithm) signHMAC(data, key []byte, h crypto.Hash) (Signature, error) { + if a[0] != 'H' { + return nil, errors.New(ErrInvalidCombination, errorMessages, a, "HMAC") + } + hasher := hmac.New(h.New, key) + hasher.Write(data) + sig := hasher.Sum(nil) + return Signature(sig), nil +} + +// signRSA signs the data using the RSAPSS or RSA algorithm. +func (a Algorithm) signRSA(data []byte, key *rsa.PrivateKey, h crypto.Hash) (Signature, error) { + if a[0] != 'P' && a[0] != 'R' { + return nil, errors.New(ErrInvalidCombination, errorMessages, a, "RSA(PSS)") + } + if a.isRSAPSS() { + // RSAPSS. + options := &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: h, + } + sig, err := rsa.SignPSS(rand.Reader, key, h, hashSum(data, h), options) + if err != nil { + return nil, errors.Annotate(err, ErrCannotSign, errorMessages) + } + return Signature(sig), nil + } + // RSA. + sig, err := rsa.SignPKCS1v15(rand.Reader, key, h, hashSum(data, h)) + if err != nil { + return nil, errors.Annotate(err, ErrCannotSign, errorMessages) + } + return Signature(sig), nil +} + // verify checks if the signature is correct for the passed data // based on the key and the passed hash. func (a Algorithm) verify(data []byte, sig Signature, k Key, h crypto.Hash) error { - hashSum := func() []byte { - hasher := h.New() - hasher.Write(data) - return hasher.Sum(nil) - } switch key := k.(type) { case *ecdsa.PublicKey: // ECDSA algorithms. - if a[0] != 'E' { - return errors.New(ErrInvalidCombination, errorMessages, a, "ECDSA") - } - var ecp ecPoint - if _, err := asn1.Unmarshal(sig, &ecp); err != nil { - return errors.Annotate(err, ErrCannotVerify, errorMessages) - } - if !ecdsa.Verify(key, hashSum(), ecp.R, ecp.S) { - return errors.New(ErrInvalidSignature, errorMessages) - } - return nil + return a.verifyECDSA(data, sig, key, h) case []byte: // HMAC algorithms. - if a[0] != 'H' { - return errors.New(ErrInvalidCombination, errorMessages, a, "HMAC") - } - expectedSig, err := a.sign(data, k, h) - if err != nil { - return errors.Annotate(err, ErrCannotVerify, errorMessages) - } - if !hmac.Equal(sig, expectedSig) { - return errors.New(ErrInvalidSignature, errorMessages) - } - return nil + return a.verifyHMAC(data, sig, key, h) case *rsa.PublicKey: // RSA and RSAPSS algorithms. - if a[0] != 'P' && a[0] != 'R' { - return errors.New(ErrInvalidCombination, errorMessages, a, "RSA(PSS)") - } - if a.isRSAPSS() { - // RSAPSS. - options := &rsa.PSSOptions{ - SaltLength: rsa.PSSSaltLengthAuto, - Hash: h, - } - if err := rsa.VerifyPSS(key, h, hashSum(), sig, options); err != nil { - return errors.Annotate(err, ErrInvalidSignature, errorMessages) - } - } else { - // RSA. - if err := rsa.VerifyPKCS1v15(key, h, hashSum(), sig); err != nil { - return errors.Annotate(err, ErrInvalidSignature, errorMessages) - } - } - return nil + return a.verifyRSA(data, sig, key, h) case string: // None algorithm. if a != "none" { @@ -242,4 +209,68 @@ func (a Algorithm) verify(data []byte, sig Signature, k Key, h crypto.Hash) erro } } +// verifyECDSA verifies the data using the ECDSA algorithm. +func (a Algorithm) verifyECDSA(data []byte, sig Signature, key *ecdsa.PublicKey, h crypto.Hash) error { + if a[0] != 'E' { + return errors.New(ErrInvalidCombination, errorMessages, a, "ECDSA") + } + var ecp ecPoint + if _, err := asn1.Unmarshal(sig, &ecp); err != nil { + return errors.Annotate(err, ErrCannotVerify, errorMessages) + } + if !ecdsa.Verify(key, hashSum(data, h), ecp.R, ecp.S) { + return errors.New(ErrInvalidSignature, errorMessages) + } + return nil +} + +// verifyHMAC verifies the data using the HMAC algorithm. +func (a Algorithm) verifyHMAC(data []byte, sig Signature, key []byte, h crypto.Hash) error { + if a[0] != 'H' { + return errors.New(ErrInvalidCombination, errorMessages, a, "HMAC") + } + expectedSig, err := a.sign(data, key, h) + if err != nil { + return errors.Annotate(err, ErrCannotVerify, errorMessages) + } + if !hmac.Equal(sig, expectedSig) { + return errors.New(ErrInvalidSignature, errorMessages) + } + return nil +} + +// verifyRSA verifies the data using the RSAPSS or RSS algorithm. +func (a Algorithm) verifyRSA(data []byte, sig Signature, key *rsa.PublicKey, h crypto.Hash) error { + if a[0] != 'P' && a[0] != 'R' { + return errors.New(ErrInvalidCombination, errorMessages, a, "RSA(PSS)") + } + if a.isRSAPSS() { + // RSAPSS. + options := &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: h, + } + if err := rsa.VerifyPSS(key, h, hashSum(data, h), sig, options); err != nil { + return errors.Annotate(err, ErrInvalidSignature, errorMessages) + } + } else { + // RSA. + if err := rsa.VerifyPKCS1v15(key, h, hashSum(data, h), sig); err != nil { + return errors.Annotate(err, ErrInvalidSignature, errorMessages) + } + } + return nil +} + +//-------------------- +// HELPERS +//-------------------- + +// hashSum determines the hash sum of the passed data. +func hashSum(data []byte, h crypto.Hash) []byte { + hasher := h.New() + hasher.Write(data) + return hasher.Sum(nil) +} + // EOF From cbef5c5aa037dca84beed2e8d2398916ffe3486f Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 23 Apr 2017 22:10:01 +0200 Subject: [PATCH 104/127] Started adding Path() to Job --- rest/job.go | 71 +++++++++++++++++++++++++++-------------------- rest/rest_test.go | 12 +++++++- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/rest/job.go b/rest/job.go index e26f37a..88adce0 100644 --- a/rest/job.go +++ b/rest/job.go @@ -24,6 +24,17 @@ import ( "github.com/tideland/golib/version" ) +//-------------------- +// CONSTANTS +//-------------------- + +// Path indexes for the different parts. +const ( + PathDomain = 0 + PathResource = 1 + PathResourceID = 2 +) + //-------------------- // JOB //-------------------- @@ -52,6 +63,10 @@ type Job interface { // ResourceID return the requests resource ID. ResourceID() string + // Path returns the parts of the URL path based on the + // index or an empty string. + Path(index int) string + // Context returns a job context also containing the // job itself. Context() context.Context @@ -115,9 +130,7 @@ type job struct { request *http.Request responseWriter http.ResponseWriter version version.Version - domain string - resource string - resourceID string + path []string } // newJob parses the URL and returns the prepared job. @@ -127,32 +140,19 @@ func newJob(env *environment, r *http.Request, rw http.ResponseWriter) Job { environment: env, request: r, responseWriter: rw, + path: stringex.SplitMap(r.URL.Path, "/", func(p string) (string, bool) { + if p == "" { + return "", false + } + return p, true + })[env.basepartsLen:], } - // Split path for REST identifiers. - parts := stringex.SplitMap(r.URL.Path, "/", func(p string) (string, bool) { - if p == "" { - return "", false - } - return p, true - })[env.basepartsLen:] - switch len(parts) { - case 3: - j.resourceID = parts[2] - j.resource = parts[1] - j.domain = parts[0] - case 2: - j.resource = parts[1] - j.domain = parts[0] + // Check path for defaults. + switch len(j.path) { case 1: - j.resource = j.environment.defaultResource - j.domain = parts[0] + j.path = append(j.path, j.environment.defaultResource) case 0: - j.resource = j.environment.defaultResource - j.domain = j.environment.defaultDomain - default: - j.resourceID = strings.Join(parts[2:], "/") - j.resource = parts[1] - j.domain = parts[0] + j.path = append(j.path, j.environment.defaultDomain, j.environment.defaultResource) } // Retrieve the requested version of the API. vsnstr := j.request.Header.Get("Version") @@ -172,7 +172,7 @@ func newJob(env *environment, r *http.Request, rw http.ResponseWriter) Job { // String is defined on the Stringer interface. func (j *job) String() string { - path := j.createPath(j.domain, j.resource, j.resourceID) + path := j.createPath(j.Domain(), j.Resource(), j.ResourceID()) return fmt.Sprintf("%s %s", j.request.Method, path) } @@ -193,17 +193,28 @@ func (j *job) ResponseWriter() http.ResponseWriter { // Domain implements the Job interface. func (j *job) Domain() string { - return j.domain + return j.path[PathDomain] } // Resource implements the Job interface. func (j *job) Resource() string { - return j.resource + return j.path[PathResource] } // ResourceID implements the Job interface. func (j *job) ResourceID() string { - return j.resourceID + if len(j.path) > 2 { + return strings.Join(j.path[PathResourceID:], "/") + } + return "" +} + +// Path implements the Job interface. +func (j *job) Path(index int) string { + if len(j.path) <= index { + return "" + } + return j.path[index] } // Context implements the Job interface. diff --git a/rest/rest_test.go b/rest/rest_test.go index 2f92c5d..6279621 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -13,6 +13,7 @@ package rest_test import ( "context" + "fmt" "testing" "github.com/tideland/golib/audit" @@ -387,6 +388,16 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { case job.AcceptsContentType(rest.ContentTypeJSON): th.assert.Logf("GET JSON") job.JSON(true).Write(rest.StatusOK, data) + case job.AcceptsContentType(rest.ContentTypePlain): + p0 := job.Path(rest.PathDomain) + p1 := job.Path(rest.PathResource) + p2 := job.Path(2) + p3 := job.Path(3) + p4 := job.Path(4) + p5 := job.Path(5) + p6 := job.Path(6) + s := fmt.Sprintf("0: %q 1: %q 2: %q 3: %q 4: %q 5: %q 6: %q", p0, p1, p2, p3, p4, p5, p6) + job.ResponseWriter().Write([]byte(s)) default: th.assert.Logf("GET HTML") job.Renderer().Render("test:context:html", data) @@ -416,7 +427,6 @@ func (th *TestHandler) Put(job rest.Job) (bool, error) { job.XML().Write(rest.StatusOK, data) } } - return true, nil } From 658e4b4c12a06757fe40702d390052ce48d15089 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 24 Apr 2017 15:08:45 +0200 Subject: [PATCH 105/127] Added testing for Path() --- rest/rest_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest/rest_test.go b/rest/rest_test.go index 6279621..34de73d 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -146,6 +146,11 @@ func TestLongPath(t *testing.T) { req := restaudit.NewRequest("GET", "/base/content/blog/2014/09/30/just-a-test") resp := ts.DoRequest(req) resp.AssertBodyContains(`Resource ID: 2014/09/30/just-a-test`) + // Now with path elements. + req = restaudit.NewRequest("GET", "/base/content/blog/2014/09/30/just-another-test") + req.AddHeader(restaudit.HeaderAccept, rest.ContentTypePlain) + resp = ts.DoRequest(req) + resp.AssertBodyContains(`0: "content" 1: "blog" 2: "2014" 3: "09" 4: "30" 5: "just-another-test" 6: ""`) } // TestFallbackDefault tests the fallback to default. From 9f4cb75b5c25491725d734e8026a84fbd5ee8a6c Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 24 Apr 2017 17:47:54 +0200 Subject: [PATCH 106/127] Started adding REST method name dispatching --- rest/handler.go | 63 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index bf8f780..8c73369 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -41,6 +41,13 @@ type GetResourceHandler interface { Get(job Job) (bool, error) } +// ReadResourceHandler is the additional interface for +// handlers understanding the verb GET but mapping it +// to method Read() according to the REST conventions. +type ReadResourceHandler interface { + Read(job Job) (bool, error) +} + // HeadResourceHandler is the additional interface for // handlers understanding the verb HEAD. type HeadResourceHandler interface { @@ -53,18 +60,39 @@ type PutResourceHandler interface { Put(job Job) (bool, error) } +// UpdateResourceHandler is the additional interface for +// handlers understanding the verb PUT but mapping it +// to method Update() according to the REST conventions. +type UpdateResourceHandler interface { + Update(job Job) (bool, error) +} + // PostResourceHandler is the additional interface for // handlers understanding the verb POST. type PostResourceHandler interface { Post(job Job) (bool, error) } +// CreateResourceHandler is the additional interface for +// handlers understanding the verb POST but mapping it +// to method Create() according to the REST conventions. +type CreateResourceHandler interface { + Create(job Job) (bool, error) +} + // PatchResourceHandler is the additional interface for // handlers understanding the verb PATCH. type PatchResourceHandler interface { Patch(job Job) (bool, error) } +// ModifyResourceHandler is the additional interface for +// handlers understanding the verb PATCH but mapping it +// to method Modify() according to the REST conventions. +type ModifyResourceHandler interface { + Modify(job Job) (bool, error) +} + // DeleteResourceHandler is the additional interface for // handlers understanding the verb DELETE. type DeleteResourceHandler interface { @@ -77,8 +105,16 @@ type OptionsResourceHandler interface { Options(job Job) (bool, error) } +// InfoResourceHandler is the additional interface for +// handlers understanding the verb OPTION but mapping it +// to method Info() according to the REST conventions. +type InfoResourceHandler interface { + Info(job Job) (bool, error) +} + // handleJob dispatches the passed job to the right method of the -// passed handler. +// passed handler. It always tries the nativ method first, then +// the alias method according to the REST conventions. func handleJob(handler ResourceHandler, job Job) (bool, error) { id := func() string { return fmt.Sprintf("%s@%s/%s", handler.ID(), job.Domain(), job.Resource()) @@ -87,7 +123,10 @@ func handleJob(handler ResourceHandler, job Job) (bool, error) { case http.MethodGet: grh, ok := handler.(GetResourceHandler) if !ok { - return false, errors.New(ErrNoGetHandler, errorMessages, id()) + grh, ok = handler.(ReadResourceHandler) + if !ok { + return false, errors.New(ErrNoGetHandler, errorMessages, id()) + } } return grh.Get(job) case http.MethodHead: @@ -99,19 +138,28 @@ func handleJob(handler ResourceHandler, job Job) (bool, error) { case http.MethodPut: prh, ok := handler.(PutResourceHandler) if !ok { - return false, errors.New(ErrNoPutHandler, errorMessages, id()) + prh, ok = handler.(UpdateResourceHandler) + if !ok { + return false, errors.New(ErrNoPutHandler, errorMessages, id()) + } } return prh.Put(job) case http.MethodPost: prh, ok := handler.(PostResourceHandler) if !ok { - return false, errors.New(ErrNoPostHandler, errorMessages, id()) + prh, ok = handler.(CreateResourceHandler) + if !ok { + return false, errors.New(ErrNoPostHandler, errorMessages, id()) + } } return prh.Post(job) case http.MethodPatch: prh, ok := handler.(PatchResourceHandler) if !ok { - return false, errors.New(ErrNoPatchHandler, errorMessages, id()) + prh, ok = handler.(ModifyResourceHandler) + if !ok { + return false, errors.New(ErrNoPatchHandler, errorMessages, id()) + } } return prh.Patch(job) case http.MethodDelete: @@ -123,7 +171,10 @@ func handleJob(handler ResourceHandler, job Job) (bool, error) { case http.MethodOptions: orh, ok := handler.(OptionsResourceHandler) if !ok { - return false, errors.New(ErrNoOptionsHandler, errorMessages, id()) + orh, ok = handler.(InfoResourceHandler) + if !ok { + return false, errors.New(ErrNoOptionsHandler, errorMessages, id()) + } } return orh.Options(job) } From f6edbabb6786c34fa8feb778ac9ef1d0ac6dcad0 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 25 Apr 2017 13:08:10 +0200 Subject: [PATCH 107/127] Added initial test for REST handler mapping --- rest/handler.go | 77 +++++++++++++++++--------------- rest/rest_test.go | 110 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 140 insertions(+), 47 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index 8c73369..c62cc3e 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -122,61 +122,66 @@ func handleJob(handler ResourceHandler, job Job) (bool, error) { switch job.Request().Method { case http.MethodGet: grh, ok := handler.(GetResourceHandler) - if !ok { - grh, ok = handler.(ReadResourceHandler) - if !ok { - return false, errors.New(ErrNoGetHandler, errorMessages, id()) - } + if ok { + return grh.Get(job) } - return grh.Get(job) + rrh, ok := handler.(ReadResourceHandler) + if ok { + return rrh.Read(job) + } + return false, errors.New(ErrNoGetHandler, errorMessages, id()) case http.MethodHead: hrh, ok := handler.(HeadResourceHandler) - if !ok { - return false, errors.New(ErrNoHeadHandler, errorMessages, id()) + if ok { + return hrh.Head(job) } - return hrh.Head(job) + return false, errors.New(ErrNoHeadHandler, errorMessages, id()) case http.MethodPut: prh, ok := handler.(PutResourceHandler) - if !ok { - prh, ok = handler.(UpdateResourceHandler) - if !ok { - return false, errors.New(ErrNoPutHandler, errorMessages, id()) - } + if ok { + return prh.Put(job) + } + urh, ok := handler.(UpdateResourceHandler) + if ok { + return urh.Update(job) } - return prh.Put(job) + return false, errors.New(ErrNoPutHandler, errorMessages, id()) case http.MethodPost: prh, ok := handler.(PostResourceHandler) - if !ok { - prh, ok = handler.(CreateResourceHandler) - if !ok { - return false, errors.New(ErrNoPostHandler, errorMessages, id()) - } + if ok { + return prh.Post(job) + } + crh, ok := handler.(CreateResourceHandler) + if ok { + return crh.Create(job) } - return prh.Post(job) + return false, errors.New(ErrNoPostHandler, errorMessages, id()) case http.MethodPatch: prh, ok := handler.(PatchResourceHandler) - if !ok { - prh, ok = handler.(ModifyResourceHandler) - if !ok { - return false, errors.New(ErrNoPatchHandler, errorMessages, id()) - } + if ok { + return prh.Patch(job) } - return prh.Patch(job) + mrh, ok := handler.(ModifyResourceHandler) + if ok { + return mrh.Modify(job) + } + return false, errors.New(ErrNoPatchHandler, errorMessages, id()) case http.MethodDelete: drh, ok := handler.(DeleteResourceHandler) - if !ok { - return false, errors.New(ErrNoDeleteHandler, errorMessages, id()) + if ok { + return drh.Delete(job) } - return drh.Delete(job) + return false, errors.New(ErrNoDeleteHandler, errorMessages, id()) case http.MethodOptions: orh, ok := handler.(OptionsResourceHandler) - if !ok { - orh, ok = handler.(InfoResourceHandler) - if !ok { - return false, errors.New(ErrNoOptionsHandler, errorMessages, id()) - } + if ok { + return orh.Options(job) + } + irh, ok := handler.(InfoResourceHandler) + if ok { + return irh.Info(job) } - return orh.Options(job) + return false, errors.New(ErrNoOptionsHandler, errorMessages, id()) } return false, errors.New(ErrMethodNotSupported, errorMessages, job.Request().Method) } diff --git a/rest/rest_test.go b/rest/rest_test.go index 34de73d..53872d6 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -277,9 +277,40 @@ func TestMethodNotSupported(t *testing.T) { err := mux.Register("test", "method", NewTestHandler("method", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("OPTION", "/base/test/method") + req := restaudit.NewRequest("OPTIONS", "/base/test/method") resp := ts.DoRequest(req) - resp.AssertBodyContains("OPTION") + resp.AssertBodyContains("OPTIONS") +} + +// TestRESTHandler tests the mapping of requests to the REST methods +// of a handler. +func TestRESTHandler(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + // Setup the test server. + mux := newMultiplexer(assert) + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err := mux.Register("test", "rest", NewRESTHandler("rest", assert)) + assert.Nil(err) + // Perform test requests. + req := restaudit.NewRequest("POST", "/base/test/rest") + resp := ts.DoRequest(req) + resp.AssertBodyContains("CREATE test/rest") + req = restaudit.NewRequest("GET", "/base/test/rest/12345") + resp = ts.DoRequest(req) + resp.AssertBodyContains("READ test/rest/12345") + req = restaudit.NewRequest("PUT", "/base/test/rest/12345") + resp = ts.DoRequest(req) + resp.AssertBodyContains("UPDATE test/rest/12345") + req = restaudit.NewRequest("PATCH", "/base/test/rest/12345") + resp = ts.DoRequest(req) + resp.AssertBodyContains("MODIFY test/rest/12345") + req = restaudit.NewRequest("DELETE", "/base/test/rest/12345") + resp = ts.DoRequest(req) + resp.AssertBodyContains("DELETE test/rest/12345") + req = restaudit.NewRequest("OPTIONS", "/base/test/rest/12345") + resp = ts.DoRequest(req) + resp.AssertBodyContains("INFO test/rest/12345") } //-------------------- @@ -352,25 +383,25 @@ const testTemplateHTML = ` ` -type TestHandler struct { +type testHandler struct { id string assert audit.Assertion } func NewTestHandler(id string, assert audit.Assertion) rest.ResourceHandler { - return &TestHandler{id, assert} + return &testHandler{id, assert} } -func (th *TestHandler) ID() string { +func (th *testHandler) ID() string { return th.id } -func (th *TestHandler) Init(env rest.Environment, domain, resource string) error { +func (th *testHandler) Init(env rest.Environment, domain, resource string) error { env.TemplatesCache().Parse("test:context:html", testTemplateHTML, "text/html") return nil } -func (th *TestHandler) Get(job rest.Job) (bool, error) { +func (th *testHandler) Get(job rest.Job) (bool, error) { if th.id == "auth:token" { job.ResponseWriter().Header().Add("Token", "foo") } @@ -410,11 +441,11 @@ func (th *TestHandler) Get(job rest.Job) (bool, error) { return true, nil } -func (th *TestHandler) Head(job rest.Job) (bool, error) { +func (th *testHandler) Head(job rest.Job) (bool, error) { return false, nil } -func (th *TestHandler) Put(job rest.Job) (bool, error) { +func (th *testHandler) Put(job rest.Job) (bool, error) { var data TestRequestData switch { case job.HasContentType(rest.ContentTypeJSON): @@ -435,7 +466,7 @@ func (th *TestHandler) Put(job rest.Job) (bool, error) { return true, nil } -func (th *TestHandler) Post(job rest.Job) (bool, error) { +func (th *testHandler) Post(job rest.Job) (bool, error) { var data TestCounterData err := job.GOB().Read(&data) if err != nil { @@ -446,10 +477,67 @@ func (th *TestHandler) Post(job rest.Job) (bool, error) { return true, nil } -func (th *TestHandler) Delete(job rest.Job) (bool, error) { +func (th *testHandler) Delete(job rest.Job) (bool, error) { return false, nil } +//-------------------- +// REST HANDLER +//-------------------- + +type restHandler struct { + id string + assert audit.Assertion +} + +func NewRESTHandler(id string, assert audit.Assertion) rest.ResourceHandler { + return &restHandler{id, assert} +} + +func (rh *restHandler) ID() string { + return rh.id +} + +func (rh *restHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +func (rh *restHandler) Create(job rest.Job) (bool, error) { + s := fmt.Sprintf("CREATE %v/%v", job.Domain(), job.Resource()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + +func (rh *restHandler) Read(job rest.Job) (bool, error) { + s := fmt.Sprintf("READ %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + +func (rh *restHandler) Update(job rest.Job) (bool, error) { + s := fmt.Sprintf("UPDATE %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + +func (rh *restHandler) Modify(job rest.Job) (bool, error) { + s := fmt.Sprintf("MODIFY %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + +func (rh *restHandler) Delete(job rest.Job) (bool, error) { + s := fmt.Sprintf("DELETE %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + +func (rh *restHandler) Info(job rest.Job) (bool, error) { + s := fmt.Sprintf("INFO %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + //-------------------- // HELPERS //-------------------- From 4c41dd8263257a2adb7c417b705a77a241b8c917 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Tue, 25 Apr 2017 21:36:58 +0200 Subject: [PATCH 108/127] Updated README and CHANGELOG --- CHANGELOG.md | 9 ++++++++- README.md | 13 ++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f3a00..fe9b900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,17 @@ # Tideland Go REST Server Library +## 2017-04-25 + +- Added access to URL path parts via *Job.Path()* +- Added interfaces for handler methods directly mapping + HTTP verbs to the according REST methods like *Create()*, + *Read()*, *Update()*, *Modify()*, *Delete()*, and *Info()* + ## 2017-03-20 - Rename internal *envelope* to public *Feedback* in *rest* - Added *ReadFeedback()* to *Response* in *request* -- Asserts in *restaudit* now internally increase the callstack +- Asserts in *restaudit* now internally increase the callstack offset so that the correct test line number is shown - Added *Response.AssertBodyGrep()* to *restaudit* diff --git a/README.md b/README.md index 858f6b5..08c2633 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,13 @@ The library earlier has been known as `web` package of the I hope you like it. ;) +[![GoDoc](https://godoc.org/github.com/tideland/gorest?status.svg)](https://godoc.org/github.com/tideland/gorestt) [![Sourcegraph](https://sourcegraph.com/github.com/tideland/gorest/-/badge.svg)](https://sourcegraph.com/github.com/tideland/gorest?badge) [![Go Report Card](https://goreportcard.com/badge/github.com/tideland/gorest)](https://goreportcard.com/report/github.com/tideland/gorest) ## Version -Version 2.13.1 +Version 2.14.0 ## Packages @@ -25,32 +26,22 @@ Version 2.13.1 RESTful web request handling. -[![GoDoc](https://godoc.org/github.com/tideland/gorest/rest?status.svg)](https://godoc.org/github.com/tideland/gorest/rest) - ### Request Convenient client requests to RESTful web services. -[![GoDoc](https://godoc.org/github.com/tideland/gorest/request?status.svg)](https://godoc.org/github.com/tideland/gorest/request) - ### Handlers Some general purpose handlers for the library. -[![GoDoc](https://godoc.org/github.com/tideland/gorest/handlers?status.svg)](https://godoc.org/github.com/tideland/gorest/handlers) - ### JSON Web Token JWT package for secure authentication and information exchange like claims. -[![GoDoc](https://godoc.org/github.com/tideland/gorest/jwt?status.svg)](https://godoc.org/github.com/tideland/gorest/jwt) - ### REST Audit Helpers for the unit tests of the Go REST Server Library. -[![GoDoc](https://godoc.org/github.com/tideland/gorest/restaudit?status.svg)](https://godoc.org/github.com/tideland/gorest/restaudit) - ## Contributors - Frank Mueller (https://github.com/themue / https://github.com/tideland) From 7da05bc78540ec170b8bc42d68d46b2eadadee6a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 26 Apr 2017 22:06:27 +0200 Subject: [PATCH 109/127] Added more asserts for overloaded REST method --- CHANGELOG.md | 2 +- rest/rest_test.go | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9b900..1ace6a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Tideland Go REST Server Library -## 2017-04-25 +## 2017-04-26 - Added access to URL path parts via *Job.Path()* - Added interfaces for handler methods directly mapping diff --git a/rest/rest_test.go b/rest/rest_test.go index 53872d6..9ac427a 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -292,7 +292,9 @@ func TestRESTHandler(t *testing.T) { defer ts.Close() err := mux.Register("test", "rest", NewRESTHandler("rest", assert)) assert.Nil(err) - // Perform test requests. + err = mux.Register("test", "double", NewDoubleHandler("double", assert)) + assert.Nil(err) + // Perform test requests on rest handler. req := restaudit.NewRequest("POST", "/base/test/rest") resp := ts.DoRequest(req) resp.AssertBodyContains("CREATE test/rest") @@ -311,6 +313,10 @@ func TestRESTHandler(t *testing.T) { req = restaudit.NewRequest("OPTIONS", "/base/test/rest/12345") resp = ts.DoRequest(req) resp.AssertBodyContains("INFO test/rest/12345") + // Perform test requests on double handler. + req = restaudit.NewRequest("GET", "/base/test/double/12345") + resp = ts.DoRequest(req) + resp.AssertBodyContains("GET test/double/12345") } //-------------------- @@ -538,6 +544,40 @@ func (rh *restHandler) Info(job rest.Job) (bool, error) { return true, nil } +//-------------------- +// DOUBLE HANDLER +//-------------------- + +// doubleHandler implements Get and Read. So Get should be chosen. +type doubleHandler struct { + id string + assert audit.Assertion +} + +func NewDoubleHandler(id string, assert audit.Assertion) rest.ResourceHandler { + return &doubleHandler{id, assert} +} + +func (dh *doubleHandler) ID() string { + return dh.id +} + +func (dh *doubleHandler) Init(env rest.Environment, domain, resource string) error { + return nil +} + +func (dh *doubleHandler) Get(job rest.Job) (bool, error) { + s := fmt.Sprintf("GET %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + +func (dh *doubleHandler) Read(job rest.Job) (bool, error) { + s := fmt.Sprintf("READ %v/%v/%v", job.Domain(), job.Resource(), job.ResourceID()) + job.ResponseWriter().Write([]byte(s)) + return true, nil +} + //-------------------- // HELPERS //-------------------- From cc72a47d7062b237909bd26f214bd024cdaffd23 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 26 Apr 2017 22:15:08 +0200 Subject: [PATCH 110/127] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08c2633..35eb7bc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The library earlier has been known as `web` package of the I hope you like it. ;) -[![GoDoc](https://godoc.org/github.com/tideland/gorest?status.svg)](https://godoc.org/github.com/tideland/gorestt) +[![GoDoc](https://godoc.org/github.com/tideland/gorest?status.svg)](https://godoc.org/github.com/tideland/gorest) [![Sourcegraph](https://sourcegraph.com/github.com/tideland/gorest/-/badge.svg)](https://sourcegraph.com/github.com/tideland/gorest?badge) [![Go Report Card](https://goreportcard.com/badge/github.com/tideland/gorest)](https://goreportcard.com/report/github.com/tideland/gorest) From 2114a7307c865beb44e7296eb24979af62b77e8d Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Wed, 26 Apr 2017 22:15:55 +0200 Subject: [PATCH 111/127] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 08c2633..35eb7bc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The library earlier has been known as `web` package of the I hope you like it. ;) -[![GoDoc](https://godoc.org/github.com/tideland/gorest?status.svg)](https://godoc.org/github.com/tideland/gorestt) +[![GoDoc](https://godoc.org/github.com/tideland/gorest?status.svg)](https://godoc.org/github.com/tideland/gorest) [![Sourcegraph](https://sourcegraph.com/github.com/tideland/gorest/-/badge.svg)](https://sourcegraph.com/github.com/tideland/gorest?badge) [![Go Report Card](https://goreportcard.com/badge/github.com/tideland/gorest)](https://goreportcard.com/report/github.com/tideland/gorest) From 238e02a6cbbb339fda8db1801a547384a438becd Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 5 May 2017 11:14:24 +0200 Subject: [PATCH 112/127] Started v2.15.0 with explicit path --- rest/path.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 rest/path.go diff --git a/rest/path.go b/rest/path.go new file mode 100644 index 0000000..a07c0ef --- /dev/null +++ b/rest/path.go @@ -0,0 +1,62 @@ +// Tideland Go REST Server Library - REST - Path +// +// Copyright (C) 2009-2017 Frank Mueller / Tideland / Oldenburg / Germany +// +// All rights reserved. Use of this source code is governed +// by the new BSD license. + +package rest + +//-------------------- +// IMPORTS +//-------------------- + +import ( + "github.com/tideland/golib/stringex" +) + +//-------------------- +// CONSTANTS +//-------------------- + +// Path indexes for the different parts. +const ( + PathDomain = 0 + PathResource = 1 + PathResourceID = 2 +) + +//-------------------- +// PATH +//-------------------- + +// Path provides access to the parts of a +// request path interesting for handling a +// job. +type Path interface { + // Length returns the number of parts of the path. + Length() int + + // Part returns the parts of the URL path based on the + // index or an empty string. + Part(index int) string + + // Domain returns the requests domain. + Domain() string + + // Resource returns the requests resource. + Resource() string + + // ResourceID return the requests resource ID. + ResourceID() string +} + +// path implements Path. +type path struct { + path []string +} + +func newPath(url ) *path { +} + +// EOF \ No newline at end of file From dca2493ebb12d0449654594718864c512a98e855 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Fri, 5 May 2017 17:16:42 +0200 Subject: [PATCH 113/127] Continued path implementation --- rest/path.go | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/rest/path.go b/rest/path.go index a07c0ef..40038a4 100644 --- a/rest/path.go +++ b/rest/path.go @@ -12,6 +12,8 @@ package rest //-------------------- import ( + "net/http" + "github.com/tideland/golib/stringex" ) @@ -21,8 +23,8 @@ import ( // Path indexes for the different parts. const ( - PathDomain = 0 - PathResource = 1 + PathDomain = 0 + PathResource = 1 PathResourceID = 2 ) @@ -32,15 +34,15 @@ const ( // Path provides access to the parts of a // request path interesting for handling a -// job. +// job. type Path interface { // Length returns the number of parts of the path. Length() int - + // Part returns the parts of the URL path based on the // index or an empty string. Part(index int) string - + // Domain returns the requests domain. Domain() string @@ -53,10 +55,26 @@ type Path interface { // path implements Path. type path struct { - path []string + parts []string } -func newPath(url ) *path { +// newPath returns the analyzed path. +func newPath(env *environment, r *http.Request) *path { + parts := stringex.SplitMap(r.URL.Path, "/", func(p string) (string, bool) { + if part == "" { + return "", false + } + return part, true + })[env.basepartsLen:] + switch len(parts) { + case 1: + parts = append(parts, env.defaultResource) + case 0: + parts = append(parts, env.defaultDomain, nev.defaultResource) + } + return &path{ + parts: parts, + } } -// EOF \ No newline at end of file +// EOF From 5d8c1463f546d22cc1aa913362248fbe1b6a1e4e Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 6 May 2017 20:53:50 +0200 Subject: [PATCH 114/127] Continued path implementation --- rest/path.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/rest/path.go b/rest/path.go index 40038a4..6cedae9 100644 --- a/rest/path.go +++ b/rest/path.go @@ -13,6 +13,7 @@ package rest import ( "net/http" + "strings" "github.com/tideland/golib/stringex" ) @@ -77,4 +78,35 @@ func newPath(env *environment, r *http.Request) *path { } } +// Length implements Path. +func (p *path) Length() int { + return len(p.parts) +} + +// Part implements Path. +func (p *path) Part(index int) string { + if len(p.parts) <= index { + return "" + } + return p.parts[index] +} + +// Domain implements Path. +func (p *path) Domain() string { + return p.parts[PathDomain] +} + +// Resource implements Path. +func (p *path) Resource() string { + return p.parts[PathResource] +} + +// ResourceID implements Path. +func (p *path) ResourceID() string { + if len(p.parts) > 2 { + return strings.Join(p.parts[PathResourceID:], "/") + } + return "" +} + // EOF From de1a78d12beb6744c8afcf91662d9bd0e94d5540 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 6 May 2017 23:31:37 +0200 Subject: [PATCH 115/127] Finalized path implementation --- rest/job.go | 58 ++++++++++++----------------------------------- rest/path.go | 4 ++-- rest/rest_test.go | 14 ++++++------ 3 files changed, 24 insertions(+), 52 deletions(-) diff --git a/rest/job.go b/rest/job.go index 88adce0..7fead50 100644 --- a/rest/job.go +++ b/rest/job.go @@ -20,21 +20,9 @@ import ( "strings" "github.com/tideland/golib/logger" - "github.com/tideland/golib/stringex" "github.com/tideland/golib/version" ) -//-------------------- -// CONSTANTS -//-------------------- - -// Path indexes for the different parts. -const ( - PathDomain = 0 - PathResource = 1 - PathResourceID = 2 -) - //-------------------- // JOB //-------------------- @@ -54,18 +42,20 @@ type Job interface { // ResponseWriter returns the used Go HTTP response writer. ResponseWriter() http.ResponseWriter - // Domain returns the requests domain. + // Domain returns the requests domain. It's deprecated, + // use Path(). Domain() string - // Resource returns the requests resource. + // Resource returns the requests resource. It's deprecated, + // use Path(). Resource() string - // ResourceID return the requests resource ID. + // ResourceID return the requests resource ID. It's deprecated, + // use Path(). ResourceID() string - // Path returns the parts of the URL path based on the - // index or an empty string. - Path(index int) string + // Path returns access to the request path inside the URL. + Path() Path // Context returns a job context also containing the // job itself. @@ -130,7 +120,7 @@ type job struct { request *http.Request responseWriter http.ResponseWriter version version.Version - path []string + path Path } // newJob parses the URL and returns the prepared job. @@ -140,19 +130,7 @@ func newJob(env *environment, r *http.Request, rw http.ResponseWriter) Job { environment: env, request: r, responseWriter: rw, - path: stringex.SplitMap(r.URL.Path, "/", func(p string) (string, bool) { - if p == "" { - return "", false - } - return p, true - })[env.basepartsLen:], - } - // Check path for defaults. - switch len(j.path) { - case 1: - j.path = append(j.path, j.environment.defaultResource) - case 0: - j.path = append(j.path, j.environment.defaultDomain, j.environment.defaultResource) + path: newPath(env, r), } // Retrieve the requested version of the API. vsnstr := j.request.Header.Get("Version") @@ -193,28 +171,22 @@ func (j *job) ResponseWriter() http.ResponseWriter { // Domain implements the Job interface. func (j *job) Domain() string { - return j.path[PathDomain] + return j.path.Domain() } // Resource implements the Job interface. func (j *job) Resource() string { - return j.path[PathResource] + return j.path.Resource() } // ResourceID implements the Job interface. func (j *job) ResourceID() string { - if len(j.path) > 2 { - return strings.Join(j.path[PathResourceID:], "/") - } - return "" + return j.path.ResourceID() } // Path implements the Job interface. -func (j *job) Path(index int) string { - if len(j.path) <= index { - return "" - } - return j.path[index] +func (j *job) Path() Path { + return j.path } // Context implements the Job interface. diff --git a/rest/path.go b/rest/path.go index 6cedae9..9a85928 100644 --- a/rest/path.go +++ b/rest/path.go @@ -61,7 +61,7 @@ type path struct { // newPath returns the analyzed path. func newPath(env *environment, r *http.Request) *path { - parts := stringex.SplitMap(r.URL.Path, "/", func(p string) (string, bool) { + parts := stringex.SplitMap(r.URL.Path, "/", func(part string) (string, bool) { if part == "" { return "", false } @@ -71,7 +71,7 @@ func newPath(env *environment, r *http.Request) *path { case 1: parts = append(parts, env.defaultResource) case 0: - parts = append(parts, env.defaultDomain, nev.defaultResource) + parts = append(parts, env.defaultDomain, env.defaultResource) } return &path{ parts: parts, diff --git a/rest/rest_test.go b/rest/rest_test.go index 9ac427a..293040c 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -431,13 +431,13 @@ func (th *testHandler) Get(job rest.Job) (bool, error) { th.assert.Logf("GET JSON") job.JSON(true).Write(rest.StatusOK, data) case job.AcceptsContentType(rest.ContentTypePlain): - p0 := job.Path(rest.PathDomain) - p1 := job.Path(rest.PathResource) - p2 := job.Path(2) - p3 := job.Path(3) - p4 := job.Path(4) - p5 := job.Path(5) - p6 := job.Path(6) + p0 := job.Path().Domain() + p1 := job.Path().Resource() + p2 := job.Path().Part(2) + p3 := job.Path().Part(3) + p4 := job.Path().Part(4) + p5 := job.Path().Part(5) + p6 := job.Path().Part(6) s := fmt.Sprintf("0: %q 1: %q 2: %q 3: %q 4: %q 5: %q 6: %q", p0, p1, p2, p3, p4, p5, p6) job.ResponseWriter().Write([]byte(s)) default: From 46b94840d11b7105949d9f160d3585c426ba3ab6 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 8 May 2017 21:55:47 +0200 Subject: [PATCH 116/127] Added latest changes to CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ace6a9..0816854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Tideland Go REST Server Library +## 2017-05-08 + +- Changed *Job.Path()* to return a *Path* instance +- This instance provides access to the different parts + of the path + ## 2017-04-26 - Added access to URL path parts via *Job.Path()* From 7d46f773f64495fdbcca6f29f3224e4e3b7aed62 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 15 May 2017 11:48:01 +0200 Subject: [PATCH 117/127] Added methods to path, changed ResourceID() --- CHANGELOG.md | 6 +++++- rest/job.go | 8 ++++---- rest/path.go | 25 ++++++++++++++++++++++++- rest/rest_test.go | 8 +++++--- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0816854..7dd1031 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Tideland Go REST Server Library -## 2017-05-08 +## 2017-05-15 - Changed *Job.Path()* to return a *Path* instance - This instance provides access to the different parts of the path +- Now *JoinedResourceID()* has the role of the former + *ResourceID()* (*Job* uses this one in the deprecated + *ResourceID()*) while the method *ResourceID()* of path + returns only the third part of the path ## 2017-04-26 diff --git a/rest/job.go b/rest/job.go index 7fead50..73e2a10 100644 --- a/rest/job.go +++ b/rest/job.go @@ -43,15 +43,15 @@ type Job interface { ResponseWriter() http.ResponseWriter // Domain returns the requests domain. It's deprecated, - // use Path(). + // use Job.Path().Domain() instead. Domain() string // Resource returns the requests resource. It's deprecated, - // use Path(). + // use Job.Path().Resource() instead. Resource() string // ResourceID return the requests resource ID. It's deprecated, - // use Path(). + // use Job.Path().JoinedResourceID() instead. ResourceID() string // Path returns access to the request path inside the URL. @@ -181,7 +181,7 @@ func (j *job) Resource() string { // ResourceID implements the Job interface. func (j *job) ResourceID() string { - return j.path.ResourceID() + return j.path.JoinedResourceID() } // Path implements the Job interface. diff --git a/rest/path.go b/rest/path.go index 9a85928..5cadbd5 100644 --- a/rest/path.go +++ b/rest/path.go @@ -40,6 +40,12 @@ type Path interface { // Length returns the number of parts of the path. Length() int + // ContainsSubResourceIDs returns true, if the path doesn't + // end after the resource ID, e.g. to address items of an order. + // + // Example: /shop/orders/12345/item/1 + ContainsSubResourceIDs() bool + // Part returns the parts of the URL path based on the // index or an empty string. Part(index int) string @@ -50,8 +56,12 @@ type Path interface { // Resource returns the requests resource. Resource() string - // ResourceID return the requests resource ID. + // ResourceID returns the requests resource ID. ResourceID() string + + // JoinedResourceID returns the requests resource ID together + // with all following parts of the path. + JoinedResourceID() string } // path implements Path. @@ -83,6 +93,11 @@ func (p *path) Length() int { return len(p.parts) } +// ContainsSubResourceIDs implements Path. +func (p *path) ContainsSubResourceIDs() bool { + return len(p.parts) > 3 +} + // Part implements Path. func (p *path) Part(index int) string { if len(p.parts) <= index { @@ -103,6 +118,14 @@ func (p *path) Resource() string { // ResourceID implements Path. func (p *path) ResourceID() string { + if len(p.parts) > 2 { + return p.parts[PathResourceID] + } + return "" +} + +// JoinedResourceID implements Path. +func (p *path) JoinedResourceID() string { if len(p.parts) > 2 { return strings.Join(p.parts[PathResourceID:], "/") } diff --git a/rest/rest_test.go b/rest/rest_test.go index 293040c..3357ff1 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -150,7 +150,7 @@ func TestLongPath(t *testing.T) { req = restaudit.NewRequest("GET", "/base/content/blog/2014/09/30/just-another-test") req.AddHeader(restaudit.HeaderAccept, rest.ContentTypePlain) resp = ts.DoRequest(req) - resp.AssertBodyContains(`0: "content" 1: "blog" 2: "2014" 3: "09" 4: "30" 5: "just-another-test" 6: ""`) + resp.AssertBodyContains(`0: "content" 1: "blog" 2: "2014" 3: "09" 4: "30" 5: "just-another-test" 6: "" J: "2014/09/30/just-another-test"`) } // TestFallbackDefault tests the fallback to default. @@ -431,14 +431,16 @@ func (th *testHandler) Get(job rest.Job) (bool, error) { th.assert.Logf("GET JSON") job.JSON(true).Write(rest.StatusOK, data) case job.AcceptsContentType(rest.ContentTypePlain): + th.assert.True(job.Path().ContainsSubResourceIDs()) p0 := job.Path().Domain() p1 := job.Path().Resource() - p2 := job.Path().Part(2) + p2 := job.Path().ResourceID() p3 := job.Path().Part(3) p4 := job.Path().Part(4) p5 := job.Path().Part(5) p6 := job.Path().Part(6) - s := fmt.Sprintf("0: %q 1: %q 2: %q 3: %q 4: %q 5: %q 6: %q", p0, p1, p2, p3, p4, p5, p6) + j := job.Path().JoinedResourceID() + s := fmt.Sprintf("0: %q 1: %q 2: %q 3: %q 4: %q 5: %q 6: %q J: %q", p0, p1, p2, p3, p4, p5, p6, j) job.ResponseWriter().Write([]byte(s)) default: th.assert.Logf("GET HTML") From cecde874e43d3468c51873d86d939444f7dffda4 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Mon, 19 Jun 2017 12:28:33 +0200 Subject: [PATCH 118/127] Reduced handleJob complexity and finished fix --- README.md | 4 +- rest/handler.go | 153 ++++++++++++++++++++++++++++------------------ rest/rest_test.go | 6 +- 3 files changed, 101 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 35eb7bc..8f6a607 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,15 @@ The library earlier has been known as `web` package of the I hope you like it. ;) +[![GitHub release](https://img.shields.io/github/release/tideland/gorest.svg)](https://github.com/tideland/gorest) +[![GitHub license](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://raw.githubusercontent.com/tideland/gorest/master/LICENSE) [![GoDoc](https://godoc.org/github.com/tideland/gorest?status.svg)](https://godoc.org/github.com/tideland/gorest) [![Sourcegraph](https://sourcegraph.com/github.com/tideland/gorest/-/badge.svg)](https://sourcegraph.com/github.com/tideland/gorest?badge) [![Go Report Card](https://goreportcard.com/badge/github.com/tideland/gorest)](https://goreportcard.com/report/github.com/tideland/gorest) ## Version -Version 2.14.0 +Version 2.15.1 ## Packages diff --git a/rest/handler.go b/rest/handler.go index c62cc3e..e5fa416 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -116,74 +116,111 @@ type InfoResourceHandler interface { // passed handler. It always tries the nativ method first, then // the alias method according to the REST conventions. func handleJob(handler ResourceHandler, job Job) (bool, error) { - id := func() string { - return fmt.Sprintf("%s@%s/%s", handler.ID(), job.Domain(), job.Resource()) - } switch job.Request().Method { case http.MethodGet: - grh, ok := handler.(GetResourceHandler) - if ok { - return grh.Get(job) - } - rrh, ok := handler.(ReadResourceHandler) - if ok { - return rrh.Read(job) - } - return false, errors.New(ErrNoGetHandler, errorMessages, id()) + return handleGetJob(handler, job) case http.MethodHead: - hrh, ok := handler.(HeadResourceHandler) - if ok { - return hrh.Head(job) - } - return false, errors.New(ErrNoHeadHandler, errorMessages, id()) + return handleHeadJob(handler, job) case http.MethodPut: - prh, ok := handler.(PutResourceHandler) - if ok { - return prh.Put(job) - } - urh, ok := handler.(UpdateResourceHandler) - if ok { - return urh.Update(job) - } - return false, errors.New(ErrNoPutHandler, errorMessages, id()) + return handlePutJob(handler, job) case http.MethodPost: - prh, ok := handler.(PostResourceHandler) - if ok { - return prh.Post(job) - } - crh, ok := handler.(CreateResourceHandler) - if ok { - return crh.Create(job) - } - return false, errors.New(ErrNoPostHandler, errorMessages, id()) + return handlePostJob(handler, job) case http.MethodPatch: - prh, ok := handler.(PatchResourceHandler) - if ok { - return prh.Patch(job) - } - mrh, ok := handler.(ModifyResourceHandler) - if ok { - return mrh.Modify(job) - } - return false, errors.New(ErrNoPatchHandler, errorMessages, id()) + return handlePatchJob(handler, job) case http.MethodDelete: - drh, ok := handler.(DeleteResourceHandler) - if ok { - return drh.Delete(job) - } - return false, errors.New(ErrNoDeleteHandler, errorMessages, id()) + return handleDeleteJob(handler, job) case http.MethodOptions: - orh, ok := handler.(OptionsResourceHandler) - if ok { - return orh.Options(job) - } - irh, ok := handler.(InfoResourceHandler) - if ok { - return irh.Info(job) - } - return false, errors.New(ErrNoOptionsHandler, errorMessages, id()) + return handleOptionsJob(handler, job) } return false, errors.New(ErrMethodNotSupported, errorMessages, job.Request().Method) } +// handleGetJob handles a job containing a GET request. +func handleGetJob(handler ResourceHandler, job Job) (bool, error) { + grh, ok := handler.(GetResourceHandler) + if ok { + return grh.Get(job) + } + rrh, ok := handler.(ReadResourceHandler) + if ok { + return rrh.Read(job) + } + return false, errors.New(ErrNoGetHandler, errorMessages, jobDescription(handler, job)) +} + +// handleHeadJob handles a job containing a HEAD request. +func handleHeadJob(handler ResourceHandler, job Job) (bool, error) { + hrh, ok := handler.(HeadResourceHandler) + if ok { + return hrh.Head(job) + } + return false, errors.New(ErrNoHeadHandler, errorMessages, jobDescription(handler, job)) +} + +// handlePutJob handles a job containing a PUT request. +func handlePutJob(handler ResourceHandler, job Job) (bool, error) { + prh, ok := handler.(PutResourceHandler) + if ok { + return prh.Put(job) + } + urh, ok := handler.(UpdateResourceHandler) + if ok { + return urh.Update(job) + } + return false, errors.New(ErrNoPutHandler, errorMessages, jobDescription(handler, job)) +} + +// handlePostJob handles a job containing a POST request. +func handlePostJob(handler ResourceHandler, job Job) (bool, error) { + prh, ok := handler.(PostResourceHandler) + if ok { + return prh.Post(job) + } + crh, ok := handler.(CreateResourceHandler) + if ok { + return crh.Create(job) + } + return false, errors.New(ErrNoPostHandler, errorMessages, jobDescription(handler, job)) +} + +// handlePatchJob handles a job containing a PATCH request. +func handlePatchJob(handler ResourceHandler, job Job) (bool, error) { + prh, ok := handler.(PatchResourceHandler) + if ok { + return prh.Patch(job) + } + mrh, ok := handler.(ModifyResourceHandler) + if ok { + return mrh.Modify(job) + } + return false, errors.New(ErrNoPatchHandler, errorMessages, jobDescription(handler, job)) +} + +// handleDeleteJob handles a job containing a DELETE request. +func handleDeleteJob(handler ResourceHandler, job Job) (bool, error) { + drh, ok := handler.(DeleteResourceHandler) + if ok { + return drh.Delete(job) + } + return false, errors.New(ErrNoDeleteHandler, errorMessages, jobDescription(handler, job)) +} + +// handleOptionsJob handles a job containing an OPTIONS request. +func handleOptionsJob(handler ResourceHandler, job Job) (bool, error) { + orh, ok := handler.(OptionsResourceHandler) + if ok { + return orh.Options(job) + } + irh, ok := handler.(InfoResourceHandler) + if ok { + return irh.Info(job) + } + return false, errors.New(ErrNoOptionsHandler, errorMessages, jobDescription(handler, job)) +} + +// jobDescription returns a description for possible errors. +func jobDescription(handler ResourceHandler, job Job) string { + return fmt.Sprintf("%s@%s/%s", handler.ID(), job.Domain(), job.Resource()) +} + // EOF diff --git a/rest/rest_test.go b/rest/rest_test.go index 3357ff1..ed8e89e 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -274,12 +274,12 @@ func TestMethodNotSupported(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err := mux.Register("test", "method", NewTestHandler("method", assert)) + err := mux.Register("test", "no-options", NewTestHandler("no-options", assert)) assert.Nil(err) // Perform test requests. - req := restaudit.NewRequest("OPTIONS", "/base/test/method") + req := restaudit.NewRequest("OPTIONS", "/base/test/no-options") resp := ts.DoRequest(req) - resp.AssertBodyContains("OPTIONS") + resp.AssertBodyContains("no-options") } // TestRESTHandler tests the mapping of requests to the REST methods From 1cfd35e18fe4a67cebdf821202e3c99b6ebd76a4 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 10 Sep 2017 00:51:18 +0200 Subject: [PATCH 119/127] Mssing support for method will lead to 405 So far all errors are with error code 500. But more RESTful is error code 405 (method not allowed). --- rest/handler.go | 16 ++++++++-------- rest/multiplexer.go | 13 +++++++++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index e5fa416..b4773d5 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -145,7 +145,7 @@ func handleGetJob(handler ResourceHandler, job Job) (bool, error) { if ok { return rrh.Read(job) } - return false, errors.New(ErrNoGetHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // handleHeadJob handles a job containing a HEAD request. @@ -154,7 +154,7 @@ func handleHeadJob(handler ResourceHandler, job Job) (bool, error) { if ok { return hrh.Head(job) } - return false, errors.New(ErrNoHeadHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // handlePutJob handles a job containing a PUT request. @@ -167,7 +167,7 @@ func handlePutJob(handler ResourceHandler, job Job) (bool, error) { if ok { return urh.Update(job) } - return false, errors.New(ErrNoPutHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // handlePostJob handles a job containing a POST request. @@ -180,7 +180,7 @@ func handlePostJob(handler ResourceHandler, job Job) (bool, error) { if ok { return crh.Create(job) } - return false, errors.New(ErrNoPostHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // handlePatchJob handles a job containing a PATCH request. @@ -193,7 +193,7 @@ func handlePatchJob(handler ResourceHandler, job Job) (bool, error) { if ok { return mrh.Modify(job) } - return false, errors.New(ErrNoPatchHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // handleDeleteJob handles a job containing a DELETE request. @@ -202,7 +202,7 @@ func handleDeleteJob(handler ResourceHandler, job Job) (bool, error) { if ok { return drh.Delete(job) } - return false, errors.New(ErrNoDeleteHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // handleOptionsJob handles a job containing an OPTIONS request. @@ -215,12 +215,12 @@ func handleOptionsJob(handler ResourceHandler, job Job) (bool, error) { if ok { return irh.Info(job) } - return false, errors.New(ErrNoOptionsHandler, errorMessages, jobDescription(handler, job)) + return false, errors.New(ErrMethodNotSupported, errorMessages, jobDescription(handler, job)) } // jobDescription returns a description for possible errors. func jobDescription(handler ResourceHandler, job Job) string { - return fmt.Sprintf("%s@%s/%s", handler.ID(), job.Domain(), job.Resource()) + return fmt.Sprintf("%s %s@%s/%s", job.Request().Method, handler.ID(), job.Domain(), job.Resource()) } // EOF diff --git a/rest/multiplexer.go b/rest/multiplexer.go index 737aefc..4a507ce 100644 --- a/rest/multiplexer.go +++ b/rest/multiplexer.go @@ -17,6 +17,7 @@ import ( "net/http" "sync" + "github.com/tideland/golib/errors" "github.com/tideland/golib/etc" "github.com/tideland/golib/logger" "github.com/tideland/golib/monitoring" @@ -132,15 +133,19 @@ func (mux *multiplexer) ServeHTTP(w http.ResponseWriter, r *http.Request) { measuring := monitoring.BeginMeasuring(job.String()) defer measuring.EndMeasuring() if err := mux.mapping.handle(job); err != nil { - mux.internalServerError("error handling request", job, err) + mux.handleError("error handling request", job, err) } } -// internalServerError logs an internal error and returns it to the user. -func (mux *multiplexer) internalServerError(format string, job Job, err error) { +// handleError logs an error and returns it to the user. +func (mux *multiplexer) handleError(format string, job Job, err error) { + code := http.StatusInternalServerError msg := fmt.Sprintf(format+" %q: %v", job, err) logger.Errorf(msg) - http.Error(job.ResponseWriter(), msg, http.StatusInternalServerError) + if errors.IsError(err, ErrMethodNotSupported) { + code = http.StatusMethodNotAllowed + } + http.Error(job.ResponseWriter(), msg, code) } // EOF From 98b3e7253f7ef4672ca57854d0759e9d984fc8c4 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 10 Sep 2017 16:50:52 +0200 Subject: [PATCH 120/127] Testing new behavior and finished docs --- CHANGELOG.md | 9 +++++++++ README.md | 2 +- rest/rest_test.go | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd1031..efd1ede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Tideland Go REST Server Library +## 2017-09-10 + +- Fixed return code in case of not implemented HTTP methods + to *405* (method not allowed) + +## 2017-06-19 + +- Reduced job handling complexity + ## 2017-05-15 - Changed *Job.Path()* to return a *Path* instance diff --git a/README.md b/README.md index 8f6a607..fe591f6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ I hope you like it. ;) ## Version -Version 2.15.1 +Version 2.15.2 ## Packages diff --git a/rest/rest_test.go b/rest/rest_test.go index ed8e89e..66e9528 100644 --- a/rest/rest_test.go +++ b/rest/rest_test.go @@ -14,6 +14,7 @@ package rest_test import ( "context" "fmt" + "net/http" "testing" "github.com/tideland/golib/audit" @@ -279,6 +280,7 @@ func TestMethodNotSupported(t *testing.T) { // Perform test requests. req := restaudit.NewRequest("OPTIONS", "/base/test/no-options") resp := ts.DoRequest(req) + resp.AssertStatusEquals(http.StatusMethodNotAllowed) resp.AssertBodyContains("no-options") } From 6a7bdb06de3429de8f2cbb447dfbe3be83d8f060 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 28 Sep 2017 17:22:42 +0200 Subject: [PATCH 121/127] Started with better JET retrieval --- jwt/header.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/jwt/header.go b/jwt/header.go index ed671ca..d2ed4cb 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -19,9 +19,22 @@ import ( ) //-------------------- -// JOB AND REQUEST HANDLING +// REQUEST AND JOB HANDLING //-------------------- +// AddToRequest adds a token as header to a request for +// usage by a client. +func AddToRequest(req *http.Request, jwt JWT) *http.Request { + req.Header.Add("Authorization", "Bearer "+jwt.String()) + return req +} + +// DecodeFromRequest tries to retrieve a token from a request +// header. +func DecodeFromRequest(req *http.Request) (JWT, error) { + return nil, nil +} + // DecodeFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is only decoded. func DecodeFromJob(job rest.Job) (JWT, error) { @@ -48,27 +61,22 @@ func VerifyCachedFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { return retrieveFromJob(job, cache, key) } -// AddTokenToRequest adds a token as header to a request for -// usage by a client. -func AddTokenToRequest(req *http.Request, jwt JWT) *http.Request { - req.Header.Add("Authorization", "Bearer "+jwt.String()) - return req -} - //-------------------- // PRIVATE HELPERS //-------------------- -// retrieveFromJob is the generic retrieval function with possible -// caching and verifaction. -func retrieveFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { +// retrieveFromRequest is the generic retrieval function with possible +// caching and verification. +func retrieveFromRequest(req *http.Request, cache Cache, key Key) (JWT, error) { // Retrieve token from header. - authorization := job.Request().Header.Get("Authorization") + authorization := req.Header.Get("Authorization") if authorization == "" { + // TODO(mue): Add error. return nil, nil } fields := strings.Fields(authorization) if len(fields) != 2 || fields[0] != "Bearer" { + // TODO(mue): Add error. return nil, nil } // Check cache. From 792fe45787ffada002c520713abf4eb5a8551ea3 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Thu, 28 Sep 2017 22:05:58 +0200 Subject: [PATCH 122/127] Made compiling again --- jwt/header.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jwt/header.go b/jwt/header.go index d2ed4cb..b6f4d69 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -30,7 +30,7 @@ func AddToRequest(req *http.Request, jwt JWT) *http.Request { } // DecodeFromRequest tries to retrieve a token from a request -// header. +// header. func DecodeFromRequest(req *http.Request) (JWT, error) { return nil, nil } @@ -38,27 +38,27 @@ func DecodeFromRequest(req *http.Request) (JWT, error) { // DecodeFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is only decoded. func DecodeFromJob(job rest.Job) (JWT, error) { - return retrieveFromJob(job, nil, nil) + return retrieveFromRequest(job.Request(), nil, nil) } // DecodeCachedFromJob retrieves a possible JWT from the request // inside a REST job and checks if it already is cached. The JWT is // only decoded. In case of no error the token is added to the cache. func DecodeCachedFromJob(job rest.Job, cache Cache) (JWT, error) { - return retrieveFromJob(job, cache, nil) + return retrieveFromRequest(job.Request(), cache, nil) } // VerifyFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. func VerifyFromJob(job rest.Job, key Key) (JWT, error) { - return retrieveFromJob(job, nil, key) + return retrieveFromRequest(job.Request(), nil, key) } // VerifyCachedFromJob retrieves a possible JWT from the request // inside a REST job and checks if it already is cached. The JWT is // verified. In case of no error the token is added to the cache. func VerifyCachedFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { - return retrieveFromJob(job, cache, key) + return retrieveFromRequest(job.Request(), cache, key) } //-------------------- @@ -71,12 +71,12 @@ func retrieveFromRequest(req *http.Request, cache Cache, key Key) (JWT, error) { // Retrieve token from header. authorization := req.Header.Get("Authorization") if authorization == "" { - // TODO(mue): Add error. + // TODO(mue): Add error. return nil, nil } fields := strings.Fields(authorization) if len(fields) != 2 || fields[0] != "Bearer" { - // TODO(mue): Add error. + // TODO(mue): Add error. return nil, nil } // Check cache. From ea78867898f2c23e7eeb3464b1ea567049b7ae1a Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sat, 30 Sep 2017 23:45:29 +0200 Subject: [PATCH 123/127] Finished new retrieving, tests not yet done --- jwt/errors.go | 40 ++++++++++++++++++++++------------------ jwt/header.go | 21 ++++++++++----------- jwt/header_test.go | 25 +++++++++++++++++++++---- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/jwt/errors.go b/jwt/errors.go index 1d1c51f..862f4da 100644 --- a/jwt/errors.go +++ b/jwt/errors.go @@ -39,27 +39,31 @@ const ( ErrNoECDSAKey ErrCannotParseRSA ErrNoRSAKey + ErrNoAuthorizationHeader + ErrInvalidAuthorizationHeader ) var errorMessages = errors.Messages{ - ErrCannotEncode: "cannot encode the %s", - ErrCannotDecode: "cannot decode the %s", - ErrCannotSign: "cannot sign the token", - ErrCannotVerify: "cannot verify the %s", - ErrNoKey: "no key available, only after encoding or verifying", - ErrJSONMarshalling: "error marshalling to JSON", - ErrJSONUnmarshalling: "error unmarshalling from JSON", - ErrInvalidTokenPart: "part of the token contains invalid data", - ErrInvalidCombination: "invalid combination of algorithm %q and key type %q", - ErrInvalidAlgorithm: "signature algorithm %q is invalid", - ErrInvalidKeyType: "key type %T is invalid", - ErrInvalidSignature: "token signature is invalid", - ErrCannotReadPEM: "cannot read the PEM", - ErrCannotDecodePEM: "cannot decode the PEM", - ErrCannotParseECDSA: "cannot parse the ECDSA", - ErrNoECDSAKey: "passed key is no ECDSA key", - ErrCannotParseRSA: "cannot parse the RSA", - ErrNoRSAKey: "passed key is no RSA key", + ErrCannotEncode: "cannot encode the %s", + ErrCannotDecode: "cannot decode the %s", + ErrCannotSign: "cannot sign the token", + ErrCannotVerify: "cannot verify the %s", + ErrNoKey: "no key available, only after encoding or verifying", + ErrJSONMarshalling: "error marshalling to JSON", + ErrJSONUnmarshalling: "error unmarshalling from JSON", + ErrInvalidTokenPart: "part of the token contains invalid data", + ErrInvalidCombination: "invalid combination of algorithm %q and key type %q", + ErrInvalidAlgorithm: "signature algorithm %q is invalid", + ErrInvalidKeyType: "key type %T is invalid", + ErrInvalidSignature: "token signature is invalid", + ErrCannotReadPEM: "cannot read the PEM", + ErrCannotDecodePEM: "cannot decode the PEM", + ErrCannotParseECDSA: "cannot parse the ECDSA", + ErrNoECDSAKey: "passed key is no ECDSA key", + ErrCannotParseRSA: "cannot parse the RSA", + ErrNoRSAKey: "passed key is no RSA key", + ErrNoAuthorizationHeader: "request contains no authorization header", + ErrInvalidAuthorizationHeader: "invalid authorization header: '%s'", } // EOF diff --git a/jwt/header.go b/jwt/header.go index b6f4d69..5998041 100644 --- a/jwt/header.go +++ b/jwt/header.go @@ -15,6 +15,7 @@ import ( "net/http" "strings" + "github.com/tideland/golib/errors" "github.com/tideland/gorest/rest" ) @@ -32,52 +33,50 @@ func AddToRequest(req *http.Request, jwt JWT) *http.Request { // DecodeFromRequest tries to retrieve a token from a request // header. func DecodeFromRequest(req *http.Request) (JWT, error) { - return nil, nil + return decodeFromRequest(req, nil, nil) } // DecodeFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is only decoded. func DecodeFromJob(job rest.Job) (JWT, error) { - return retrieveFromRequest(job.Request(), nil, nil) + return decodeFromRequest(job.Request(), nil, nil) } // DecodeCachedFromJob retrieves a possible JWT from the request // inside a REST job and checks if it already is cached. The JWT is // only decoded. In case of no error the token is added to the cache. func DecodeCachedFromJob(job rest.Job, cache Cache) (JWT, error) { - return retrieveFromRequest(job.Request(), cache, nil) + return decodeFromRequest(job.Request(), cache, nil) } // VerifyFromJob retrieves a possible JWT from // the request inside a REST job. The JWT is verified. func VerifyFromJob(job rest.Job, key Key) (JWT, error) { - return retrieveFromRequest(job.Request(), nil, key) + return decodeFromRequest(job.Request(), nil, key) } // VerifyCachedFromJob retrieves a possible JWT from the request // inside a REST job and checks if it already is cached. The JWT is // verified. In case of no error the token is added to the cache. func VerifyCachedFromJob(job rest.Job, cache Cache, key Key) (JWT, error) { - return retrieveFromRequest(job.Request(), cache, key) + return decodeFromRequest(job.Request(), cache, key) } //-------------------- // PRIVATE HELPERS //-------------------- -// retrieveFromRequest is the generic retrieval function with possible +// decodeFromRequest is the generic decoder with possible // caching and verification. -func retrieveFromRequest(req *http.Request, cache Cache, key Key) (JWT, error) { +func decodeFromRequest(req *http.Request, cache Cache, key Key) (JWT, error) { // Retrieve token from header. authorization := req.Header.Get("Authorization") if authorization == "" { - // TODO(mue): Add error. - return nil, nil + return nil, errors.New(ErrNoAuthorizationHeader, errorMessages) } fields := strings.Fields(authorization) if len(fields) != 2 || fields[0] != "Bearer" { - // TODO(mue): Add error. - return nil, nil + return nil, errors.New(ErrInvalidAuthorizationHeader, errorMessages, authorization) } // Check cache. if cache != nil { diff --git a/jwt/header_test.go b/jwt/header_test.go index 94ecdf1..616b0ac 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -29,6 +29,23 @@ import ( // TESTS //-------------------- +// TestDecodeInvalidRequest tests the decoding of requests +// without a header or an invalid one. +func TestDecodeInvalidRequest(t *testing.T) { + assert := audit.NewTestingAssertion(t, true) + assert.Logf("testing decode invalid requests") + // Setup the test server. + mux := newMultiplexer(assert) + ts := restaudit.StartServer(mux, assert) + defer ts.Close() + err := mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) + assert.Nil(err) + // Perform request without authorization. + req := restaudit.NewRequest("GET", "/test/jwt/1234567890") + req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) + ts.DoRequest(req) +} + // TestDecodeRequest tests the decoding of a token // in a handler. func TestDecodeRequest(t *testing.T) { @@ -48,7 +65,7 @@ func TestDecodeRequest(t *testing.T) { req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -75,7 +92,7 @@ func TestDecodeCachedRequest(t *testing.T) { req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -107,7 +124,7 @@ func TestVerifyRequest(t *testing.T) { req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} @@ -134,7 +151,7 @@ func TestVerifyCachedRequest(t *testing.T) { req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) req.SetRequestProcessor(func(req *http.Request) *http.Request { - return jwt.AddTokenToRequest(req, jwtIn) + return jwt.AddToRequest(req, jwtIn) }) resp := ts.DoRequest(req) claimsOut := jwt.Claims{} From c28828d232663cd63943eb2e490eb6819edd5e8f Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 1 Oct 2017 13:49:30 +0200 Subject: [PATCH 124/127] Redesigned JWT test handler, invalid tests can follow --- jwt/header_test.go | 119 +++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/jwt/header_test.go b/jwt/header_test.go index 616b0ac..4f0b084 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -38,7 +38,8 @@ func TestDecodeInvalidRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err := mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) + asserter := newDecodeAsserter(assert, false) + err := mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform request without authorization. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") @@ -59,7 +60,8 @@ func TestDecodeRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, false)) + asserter := newDecodeAsserter(assert, false) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") @@ -86,7 +88,8 @@ func TestDecodeCachedRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, nil, true)) + asserter := newDecodeAsserter(assert, true) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform first test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") @@ -118,7 +121,8 @@ func TestVerifyRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, false)) + asserter := newVerifyAsserter(assert, key, false) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") @@ -145,7 +149,8 @@ func TestVerifyCachedRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - err = mux.Register("test", "jwt", NewTestHandler("jwt", assert, key, true)) + asserter := newVerifyAsserter(assert, key, true) + err = mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform first test request. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") @@ -167,20 +172,66 @@ func TestVerifyCachedRequest(t *testing.T) { // HANDLER //-------------------- -// testHandler is used in the test scenarios. -type testHandler struct { - id string - assert audit.Assertion - key jwt.Key - cache jwt.Cache +// testAsserter instances will handle the assertions in the testHandler. +type testAsserter func(job rest.Job) (bool, error) + +func newDecodeAsserter(assert audit.Assertion, cached bool) testAsserter { + var cache jwt.Cache + if cached { + cache = jwt.NewCache(time.Minute, time.Minute, time.Minute, 10) + } + return func(job rest.Job) (bool, error) { + var token jwt.JWT + var err error + if cached { + token, err = jwt.DecodeCachedFromJob(job, cache) + } else { + token, err = jwt.DecodeFromJob(job) + } + assert.Nil(err) + assert.True(token.IsValid(time.Minute)) + subject, ok := token.Claims().Subject() + assert.True(ok) + assert.Equal(subject, job.ResourceID()) + job.JSON(true).Write(rest.StatusOK, token.Claims()) + return true, nil + } } -func NewTestHandler(id string, assert audit.Assertion, key jwt.Key, useCache bool) rest.ResourceHandler { +func newVerifyAsserter(assert audit.Assertion, key jwt.Key, cached bool) testAsserter { var cache jwt.Cache - if useCache { + if cached { cache = jwt.NewCache(time.Minute, time.Minute, time.Minute, 10) } - return &testHandler{id, assert, key, cache} + return func(job rest.Job) (bool, error) { + var token jwt.JWT + var err error + if cached { + token, err = jwt.VerifyCachedFromJob(job, cache, key) + } else { + token, err = jwt.VerifyFromJob(job, key) + } + assert.Nil(err) + assert.True(token.IsValid(time.Minute)) + subject, ok := token.Claims().Subject() + assert.True(ok) + assert.Equal(subject, job.ResourceID()) + job.JSON(true).Write(rest.StatusOK, token.Claims()) + return true, nil + } +} + +// testHandler is used in the test scenarios. +type testHandler struct { + id string + asserter testAsserter +} + +func newTestHandler(id string, asserter testAsserter) rest.ResourceHandler { + return &testHandler{ + id: id, + asserter: asserter, + } } func (th *testHandler) ID() string { @@ -192,45 +243,7 @@ func (th *testHandler) Init(env rest.Environment, domain, resource string) error } func (th *testHandler) Get(job rest.Job) (bool, error) { - if th.key == nil { - return th.testDecode(job) - } else { - return th.testVerify(job) - } -} - -func (th *testHandler) testDecode(job rest.Job) (bool, error) { - decode := func() (jwt.JWT, error) { - if th.cache == nil { - return jwt.DecodeFromJob(job) - } - return jwt.DecodeCachedFromJob(job, th.cache) - } - jwtOut, err := decode() - th.assert.Nil(err) - th.assert.True(jwtOut.IsValid(time.Minute)) - subject, ok := jwtOut.Claims().Subject() - th.assert.True(ok) - th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(rest.StatusOK, jwtOut.Claims()) - return true, nil -} - -func (th *testHandler) testVerify(job rest.Job) (bool, error) { - verify := func() (jwt.JWT, error) { - if th.cache == nil { - return jwt.VerifyFromJob(job, th.key) - } - return jwt.VerifyCachedFromJob(job, th.cache, th.key) - } - jwtOut, err := verify() - th.assert.Nil(err) - th.assert.True(jwtOut.IsValid(time.Minute)) - subject, ok := jwtOut.Claims().Subject() - th.assert.True(ok) - th.assert.Equal(subject, job.ResourceID()) - job.JSON(true).Write(rest.StatusOK, jwtOut.Claims()) - return true, nil + return th.asserter(job) } //-------------------- From e2b1e49670f9eb5a77b46b605b6e2c940fd5daaf Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 1 Oct 2017 21:53:39 +0200 Subject: [PATCH 125/127] Finalized testing --- jwt/header_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/jwt/header_test.go b/jwt/header_test.go index 4f0b084..fd3986f 100644 --- a/jwt/header_test.go +++ b/jwt/header_test.go @@ -38,13 +38,16 @@ func TestDecodeInvalidRequest(t *testing.T) { mux := newMultiplexer(assert) ts := restaudit.StartServer(mux, assert) defer ts.Close() - asserter := newDecodeAsserter(assert, false) + asserter := newHeaderAsserter(assert, ".* request contains no authorization header") err := mux.Register("test", "jwt", newTestHandler("jwt", asserter)) assert.Nil(err) // Perform request without authorization. req := restaudit.NewRequest("GET", "/test/jwt/1234567890") req.AddHeader(restaudit.HeaderAccept, restaudit.ApplicationJSON) - ts.DoRequest(req) + resp := ts.DoRequest(req) + ok := "" + resp.AssertUnmarshalledBody(&ok) + assert.Equal(ok, "OK") } // TestDecodeRequest tests the decoding of a token @@ -175,6 +178,16 @@ func TestVerifyCachedRequest(t *testing.T) { // testAsserter instances will handle the assertions in the testHandler. type testAsserter func(job rest.Job) (bool, error) +func newHeaderAsserter(assert audit.Assertion, pattern string) testAsserter { + return func(job rest.Job) (bool, error) { + token, err := jwt.DecodeFromJob(job) + assert.Nil(token) + assert.ErrorMatch(err, pattern) + job.JSON(true).Write(rest.StatusOK, "OK") + return true, nil + } +} + func newDecodeAsserter(assert audit.Assertion, cached bool) testAsserter { var cache jwt.Cache if cached { From a62ff31f847cea4bb0f2f0c5e0f6ff332776316c Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 1 Oct 2017 22:24:24 +0200 Subject: [PATCH 126/127] Added .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode From f02ea8bbdcd9a542d7c3628b1ba52b95ca2a6ed4 Mon Sep 17 00:00:00 2001 From: Frank Mueller Date: Sun, 1 Oct 2017 22:47:23 +0200 Subject: [PATCH 127/127] Added debug.test to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 722d5e7..589fa54 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .vscode +debug.test