Skip to content

Commit

Permalink
Merge branch 'feature/travisCI' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-hanna committed May 2, 2017
2 parents b76bf67 + dd08312 commit 41e7f16
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 14 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store

coverage.out
coverage-all.out
20 changes: 20 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
language: go
go:
- 1.7.x
# branches:
# only:
# - feature/travisCI
env:
global:
secure: hEiVIu1nvTloePPm72L6867b8h11/dxydcbpqoujUE89n5MiFh0zo/Du05N0eYjlmjANq3uvVGgHnWNSX4l8yqZf1QcOE2yvs9e06vqMZbK6iryT2Mm8KuLFgpHulQinz/SSJo6Kw4o8UXhJT9r80YqpgOT6EZX/2YRetz/kYQqI+qe6GqYSDZVATYgG54E0puLR0hBNwWuGfjWCmiGXFuIJl8mEqnx9rd4B8VPARMWToVvTo/IC3O1zpNPMV9zDcC3CG+/quQJHFELpCB1c7SHEYClKeZrVnrqcoGgs+3z3P0wPfXo+fTragRhtLf5Ynbru+Af1p3vzfcOjmVys+k6+b1cqS8GJ73k/+Y2L7QrrHrOZRJbA9ATYobXSfPFbCCw+iRRHnwZDHnZlMt8mQkn+LA0SH+YRNiyLRhgqPOKr00UwzMoi4ckDd6PjhT5SuWaoiADDFuxZ5Ld+VbWoNoLHeVtt+Kn+nR9PhOJ0taWnOKo6VOVg2oainR5vERuuP/JOG+sH1lN8LTjxHERb9Zx0zZowDczvLi0mD7ehoOwBp3fpuJ5FOSZyI8+jB0dZTzaOTBQWve71SQzGt7z9eXAzA8RNTkBt/MTCf9k0qqVD6n237nkMZMaluOeQ8ez6O8v1H2nZ9R/BrWcLhFwjLg+8294MCNYjcKmzhg6S5cY=
services:
- redis-server
install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
- go get github.com/garyburd/redigo/redis
- go get github.com/pborman/uuid
script:
- make test-cover-html
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=coverage-all.out
-service=travis-ci -repotoken=$COVERALLS_TOKEN
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2017 Adam Hanna

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.PHONY: test-cover-html
PACKAGES = $(shell find ./ -type d -not -path '*/\.*' | grep -v vendor)

fmt:
bash -c 'go list ./... | grep -v vendor | xargs -n1 go fmt'

test:
bash -c 'go list ./... | grep -v vendor | xargs -n1 go test -timeout=30s -tags="unit integration e2e"'

# thanks!
# https://gist.github.com/skarllot/13ebe8220822bc19494c8b076aabe9fc
test-cover-html:
echo "mode: count" > coverage-all.out
$(foreach pkg,$(PACKAGES),\
go test -tags="unit integration e2e" -coverprofile=coverage.out -covermode=count $(pkg);\
tail -n +2 coverage.out >> coverage-all.out;)
180 changes: 167 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
# Go Sessions
A dead simple, highly customizable sessions service for go http servers
[![Build Status](https://travis-ci.org/adam-hanna/sessions.svg)](https://travis-ci.org/adam-hanna/sessions) [![Coverage Status](https://coveralls.io/repos/github/adam-hanna/sessions/badge.svg)](https://coveralls.io/github/adam-hanna/sessions) [![Go Report Card](https://goreportcard.com/badge/github.com/adam-hanna/sessions)](https://goreportcard.com/report/github.com/adam-hanna/sessions) [![GoDoc](https://godoc.org/github.com/adam-hanna/sessions?status.svg)](https://godoc.org/github.com/adam-hanna/sessions)

## Quickstart
# Sessions
A dead simple, highly performant, highly customizable sessions service for go http servers.

By default, the service stores sessions in redis, and transports sessions to clients in cookies. However, these are easily customizeable. For instance, the storage interface only implements three methods:

~~~go
// ServiceInterface defines the behavior of the session store
type ServiceInterface interface {
SaveUserSession(userSession *user.Session) *sessionerrs.Custom
DeleteUserSession(sessionID string) *sessionerrs.Custom
FetchValidUserSession(sessionID string) (*user.Session, *sessionerrs.Custom)
}
~~~

**README Contents:**

1. [Quickstart](https://github.com/adam-hanna/sessions#quickstart)
2. [Performance](https://github.com/adam-hanna/sessions#performance)
3. [Design](https://github.com/adam-hanna/sessions#design)
4. [API](https://github.com/adam-hanna/sessions#api)
5. [Test Coverage](https://github.com/adam-hanna/sessions#test-coverage)
6. [Example](https://github.com/adam-hanna/sessions#example)
7. [License](https://github.com/adam-hanna/sessions#license)

## Quickstart
~~~go
package main

Expand Down Expand Up @@ -38,7 +60,8 @@ func main() {
}

transportOptions := transport.Options{
Secure: false, // note: can't use secure cookies in development!
HTTPOnly: true,
Secure: false, // note: can't use secure cookies in development!
}
seshTransport := transport.New(transportOptions)

Expand All @@ -47,17 +70,120 @@ func main() {

http.HandleFunc("/issue", issueSession)

log.Println("Listening on localhost:8080")
log.Println("Listening on localhost:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
}
~~~

## Testing
Tests are broken down into three categories: unit, integration and e2e. Integration and e2e tests require a connection to a redis server. The connection address can be set in the `REDIS_URL` environment variable. The default is ":6379"
## Performance
Benchmarks require a redis-server running. Set the `REDIS_URL` environment variable, otherwise the benchmarks look for ":6379".

YMMV
~~~ bash
$ (cd benchmark && go test -bench=.)

setting up benchmark tests
BenchmarkBaseServer-2 20000 72479 ns/op
BenchmarkValidSession-2 10000 151650 ns/op
PASS
shutting down benchmark tests
ok github.com/adam-hanna/sessions/benchmark 3.727s
~~~

## Design
By default, the service stores sessions in redis, and transports hashed sessionIDs to clients in cookies. However, these are easily customizeable through the creation of custom structs that implement the interface.

The general flow of the session service is as follows:

1. Create [store](https://godoc.org/github.com/adam-hanna/sessions/store), [auth](https://godoc.org/github.com/adam-hanna/sessions/auth) and [transport](https://godoc.org/github.com/adam-hanna/sessions/transport) services by calling their respective `New(...)` functions (or create your own custom services that implement the service's interface methods). Then pass these services to the `sessions.New(...)` constructor.
2. After a user logs in, call the `sessions.IssueUserSession(...)` function. This function first creates a new `user.Session`. SessionIDs are [RFC 4122 version 4 uuids](https://github.com/pborman/uuid). Next, the service hashes the sessionID with the provided key. The hashing algorithm is SHA-512, and therefore [the key used should be between 64 and 128 bytes](https://tools.ietf.org/html/rfc2104#section-3). Then, the service stores the session in redis and finally writes the hashed sessionID to the response writer in a cookie. Sessions written to the redis db utilize `EXPIREAT` to automatically destory expired sessions.
3. To check if a valid session was included in a request, use the `sessions.GetUserSession(...)` function. This function grabs the hashed sessionID from the session cookie, verifies the HMAC signature and finally looks up the session in the redis db. If the session is expired, or fails HMAC signature verification, this function will return an error with code 401. If the session is valid, and you'd like to extend the session's expiry, you can then call `session.ExtendUserSession(...)`. Session expiry's are never automatically extended, only through calling this function will the session's expiry be extended.
4. When a user logs out, call the `sessions.ClearUserSession(...)` function. This function destroys the session in the db and also destroys the cookie on the ResponseWriter.

## API
### [user.Session](https://godoc.org/github.com/adam-hanna/sessions/user#Session)
~~~go
type Session struct {
ID string
UserID string
ExpiresAt time.Time
JSON string
}
~~~
Session is the struct that is used to store session data. The JSON field allows you to set any custom information you'd like. See the [example](https://github.com/adam-hanna/sessions#example)

### [IssueUserSession](https://godoc.org/github.com/adam-hanna/sessions#IssueUserSession)
~~~ go
func (s *Service) IssueUserSession(userID string, json string, w http.ResponseWriter) (*user.Session, *sessionerrs.Custom)
~~~
IssueUserSession grants a new user session, writes that session info to the store and writes the session on the http.ResponseWriter.

This method should be called when a user logs in, for example.

### [ClearUserSession](https://godoc.org/github.com/adam-hanna/sessions#ClearUserSession)
~~~go
func (s *Service) ClearUserSession(userSession *user.Session, w http.ResponseWriter) *sessionerrs.Custom
~~~
ClearUserSession is used to remove the user session from the store and clear the cookies on the ResponseWriter.

This method should be called when a user logs out, for example.

### [GetUserSession](https://godoc.org/github.com/adam-hanna/sessions#GetUserSession)
~~~go
func (s *Service) GetUserSession(r *http.Request) (*user.Session, *sessionerrs.Custom)
~~~
GetUserSession returns a user session from the hashed sessionID included in the request. This method only returns valid sessions. Therefore, sessions that have expired, or that fail signature verification will return a custom session error with code 401.

### [ExtendUserSession](https://godoc.org/github.com/adam-hanna/sessions#ExtendUserSession)
~~~go
func (s *Service) ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) *sessionerrs.Custom
~~~
ExtendUserSession extends the ExpiresAt of a session by the Options.ExpirationDuration

Note that this function must be called, manually! Extension of user session expiry's does not happen automatically!

### [sessionerrs.Custom](https://godoc.org/github.com/adam-hanna/sessions/sessionerrs#Custom)
~~~go
type Custom struct {
// Code corresponds to an http status code (e.g. 401 Unauthorized, or 500 Internal Server Error)
Code int
// Err is the actual error thrown
Err error
}
~~~
Custom is the error type returned by this session service package. This custom error is useful for calling funcs to determine which http status code to return to clients on err. For example:

~~~go
userSession, seshErr := sesh.IssueUserSession("fakeUserID", "", w)
if seshErr != nil {
log.Printf("Err issuing user session: %v\n", seshErr)
http.Error(w, seshErr.Err.Error(), seshErr.Code) // seshErr is a custom err with an http code
return
}
~~~

## Test Coverage
~~~bash
ok github.com/adam-hanna/sessions 9.012s coverage: 94.1% of statements
? github.com/adam-hanna/sessions/sessionerrs [no test files]
ok github.com/adam-hanna/sessions/auth 0.003s coverage: 100.0% of statements
ok github.com/adam-hanna/sessions/store 0.006s coverage: 85.4% of statements
ok github.com/adam-hanna/sessions/benchmark 0.004s coverage: 0.0% of statements [no tests to run]
ok github.com/adam-hanna/sessions/transport 0.004s coverage: 95.2% of statements
ok github.com/adam-hanna/sessions/user 0.003s coverage: 100.0% of statements
~~~

Tests are broken down into three categories: unit, integration and e2e. Integration and e2e tests require a connection to a redis server. The connection address can be set in the `REDIS_URL` environment variable. The default is ":6379".

To run all tests, simply:
~~~
$ go test -tags="unit integration e2e" ./...
// or
$ make test
// or
$ make test-cover-html && go tool cover -html=coverage-all.out
~~~

To run only tests from one of the categories:
Expand All @@ -71,7 +197,7 @@ $ go test -tags="unit integration" ./...
~~~

## Example
The following example is a demonstration of using the session service along with a CSRF code to check for authentication. The CSRF code is stored in the userSession JSON field.
The following example is a demonstration of using the session service along with a CSRF code to check for authentication. The CSRF code is stored in the user.Session JSON field.

~~~go
package main
Expand Down Expand Up @@ -112,7 +238,7 @@ var issueSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)
}
JSONBytes, err := json.Marshal(myJSON)
if err != nil {
log.Printf("Err generating json: %v\n", err)
log.Printf("Err marhsalling json: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Expand Down Expand Up @@ -150,7 +276,7 @@ var requiresSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reque

myJSON := SessionJSON{}
if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
log.Printf("Err issuing unmarshalling json: %v\n", err)
log.Printf("Err unmarshalling json: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Expand All @@ -167,7 +293,7 @@ var requiresSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reque
// note that session expiry's need to be manually extended
seshErr = sesh.ExtendUserSession(userSession, r, w)
if seshErr != nil {
log.Printf("Err fetching user session: %v\n", seshErr)
log.Printf("Err extending user session: %v\n", seshErr)
http.Error(w, seshErr.Err.Error(), seshErr.Code)
return
}
Expand Down Expand Up @@ -199,7 +325,7 @@ var clearSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)

myJSON := SessionJSON{}
if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
log.Printf("Err issuing unmarshalling json: %v\n", err)
log.Printf("Err unmarshalling json: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Expand Down Expand Up @@ -249,7 +375,8 @@ func main() {
}

transportOptions := transport.Options{
Secure: false, // note: can't use secure cookies in development!
HTTPOnly: true,
Secure: false, // note: can't use secure cookies in development!
}
seshTransport := transport.New(transportOptions)

Expand All @@ -264,11 +391,38 @@ func main() {
log.Fatal(http.ListenAndServe("127.0.0.1:3000", nil))
}

// thanks
// https://astaxie.gitbooks.io/build-web-application-with-golang/en/06.2.html#unique-session-ids
func generateKey() (string, error) {
b := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
~~~

## License
~~~
The MIT License (MIT)
Copyright (c) 2017 Adam Hanna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
~~~
2 changes: 1 addition & 1 deletion benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func BenchmarkValidSession(b *testing.B) {
b.Fatal("Get:", err)
}
if res.StatusCode != 200 {
b.Errorf("Wanted 200 status code, received: %d\n", res.StatusCode)
b.Fatalf("Wanted 200 status code, received: %d\n", res.StatusCode)
}
}
}

0 comments on commit 41e7f16

Please sign in to comment.