Second Commit

This commit is contained in:
2017-08-06 17:48:01 +02:00
parent 591462e3c1
commit d754bfb686
17 changed files with 1205 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
package auth
type Authenticator interface {
ID() string
Name() string
Exists(username string) bool
Find(userid string) *User
Login(username, password string) (*User, error)
Register(username, password string, data map[string]string) (*User, error)
}
+138
View File
@@ -0,0 +1,138 @@
package auth
import (
"errors"
"strings"
"testing"
"git.aiterp.net/gisle/wrouter/generate"
)
var ErrExists = errors.New("auth: user exists")
var ErrLogin = errors.New("auth: login failed")
type testAuther struct {
FullName string
users []*User
passwords map[string]string
}
func (ta *testAuther) ID() string {
return strings.ToLower(ta.FullName)
}
func (ta *testAuther) Name() string {
return ta.FullName
}
func (ta *testAuther) Exists(username string) bool {
for _, user := range ta.users {
if user.Name == username {
return true
}
}
return false
}
func (ta *testAuther) Find(userid string) *User {
for _, user := range ta.users {
if user.ID == userid {
return user
}
}
return nil
}
func (ta *testAuther) Login(username, password string) (*User, error) {
for _, user := range ta.users {
if user.Name == username && password == ta.passwords[user.ID] {
return user, nil
}
}
return nil, ErrLogin
}
func (ta *testAuther) Register(username, password string, data map[string]string) (*User, error) {
if ta.Exists(username) {
return nil, ErrExists
}
if ta.passwords == nil {
ta.passwords = make(map[string]string)
}
id := generate.ID()
ta.passwords[id] = password
user := NewUser(ta, id, username, "member", data)
ta.users = append(ta.users, user)
return user, nil
}
func TestList(t *testing.T) {
ta1 := testAuther{FullName: "Auth1"}
ta2 := testAuther{FullName: "Auth2"}
Register(&ta1)
Register(&ta2)
if ta1.ID() != "auth1" {
t.Errorf("ta1.ID() = %s", ta1.ID())
t.Fail()
}
if ta2.ID() != "auth2" {
t.Errorf("ta2.ID() = %s", ta2.ID())
t.Fail()
}
t.Run("Find", func(t *testing.T) {
fa1 := FindAuthenticator("auth1")
fa2 := FindAuthenticator("auth2")
if &ta1 != fa1 {
t.Errorf("%s != %s", ta1.ID(), fa1.ID())
t.Fail()
}
if &ta2 != fa2 {
t.Errorf("%s != %s", ta2.ID(), fa2.ID())
t.Fail()
}
})
t.Run("Register", func(t *testing.T) {
user, err := ta1.Register("Test", "CakesAndStuff", nil)
if err != nil || user.Name != "Test" {
t.Logf("err = %v; name = \"%s\"", err, user.Name)
t.Fail()
}
if !ta1.Exists("Test") {
t.Log("Registered user does not exist")
t.Fail()
}
user2, err := ta1.Register("Test", "CakesAndStuff", nil)
if err == nil || user2 != nil {
t.Logf("err = %s; name = \"%s\"", err, user2.Name)
t.Fail()
}
})
t.Run("Login", func(t *testing.T) {
user, err := ta1.Login("Test", "CakesAndStuff")
if err != nil || user.Name != "Test" {
t.Logf("err = %v; name = \"%s\"", err, user.Name)
t.Fail()
}
user2, err := ta1.Login("Test", "WrongPassword")
if err == nil || user2 != nil {
t.Logf("err = %v; name = \"%s\"", err, user.Name)
t.Fail()
}
})
}
+108
View File
@@ -0,0 +1,108 @@
package auth
import (
"net/http"
"strings"
"git.aiterp.net/gisle/wrouter/response"
)
type handler struct {
}
func (h *handler) Handle(path string, w http.ResponseWriter, req *http.Request, user *User) bool {
// Get the subpath out of the path
subpath := req.URL.Path[len(path):]
if subpath[0] == '/' {
subpath = subpath[1:]
}
method := FindAuthenticator(req.Form.Get("method"))
switch strings.ToLower(subpath) {
case "login":
{
if req.Method != "POST" {
response.Text(w, 405, req.Method+" not allowed")
return true
}
username := req.Form.Get("username")
password := req.Form.Get("password")
w.Header().Set("X-Auth-Method", method.Name())
user, err := method.Login(username, password)
if err != nil && user != nil {
sess := OpenSession(user)
http.SetCookie(w, &http.Cookie{Name: SessionCookieName, Value: sess.ID, Expires: sess.Time.Add(SessionMaxTime)})
response.JSON(w, 200, sess)
} else {
response.Text(w, 401, "Login failed")
}
}
case "register":
{
if req.Method != "POST" {
response.Text(w, 405, req.Method+" not allowed")
return true
}
data := make(map[string]string)
for key, value := range req.Form {
if key != "username" && key != "password" && key != "method" {
data[key] = value[0]
}
}
username := req.Form.Get("username")
password := req.Form.Get("password")
user, err := method.Register(username, password, data)
if err != nil && user != nil {
sess := OpenSession(user)
http.SetCookie(w, &http.Cookie{Name: SessionCookieName, Value: sess.ID, Expires: sess.Time.Add(SessionMaxTime)})
response.JSON(w, 200, sess)
} else {
response.Text(w, 401, "Register failed")
}
}
case "logout-all":
{
if req.Method != "POST" {
response.Text(w, 405, req.Method+" not allowed")
return true
}
if user != nil {
ClearSessions(user)
response.Empty(w)
} else {
response.Text(w, 401, "Not logged in")
}
}
case "status":
{
if req.Method != "GET" {
response.Text(w, 405, req.Method+" not allowed")
return true
}
if user != nil {
response.JSON(w, 200, user)
} else {
response.Text(w, 401, "Not logged in")
}
}
default:
{
response.Text(w, 404, "Operation not found: "+subpath)
}
}
return true
}
var Handler = &handler{}
+32
View File
@@ -0,0 +1,32 @@
package auth
import (
"net/http"
"net/url"
"strings"
"testing"
)
type handlerStruct struct{}
func (hs *handlerStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req.ParseForm() // Router does this in non-tests
if strings.HasPrefix(req.URL.Path, "/auth") {
Handler.Handle("/auth", w, req, nil)
return
}
}
func TestHandler(t *testing.T) {
auther := testAuther{FullName: "Test"}
Register(&auther)
form := url.Values{}
form.Set("username", "Test")
form.Set("password", "stuff'nthings")
t.Run("Register", func(t *testing.T) {
})
}
+43
View File
@@ -0,0 +1,43 @@
package auth
import "strings"
var methods []Authenticator
// Register a method
func Register(method Authenticator) {
methods = append(methods, method)
}
// FindAuthenticator finds the first Method that answers with
// the ID().
func FindAuthenticator(id string) Authenticator {
for _, method := range methods {
if method.ID() == id {
return method
}
}
return nil
}
// ListAuthenticators gets a copy of the method list
func ListAuthenticators() []Authenticator {
dst := make([]Authenticator, len(methods))
copy(dst, methods)
return dst
}
func FindUser(fullid string) *User {
split := strings.SplitN(fullid, ":", 2)
autherID := split[0]
userID := split[1]
auther := FindAuthenticator(autherID)
if auther == nil {
return nil
}
return auther.Find(userID)
}
+98
View File
@@ -0,0 +1,98 @@
package auth
import (
"log"
"sync"
"time"
"git.aiterp.net/gisle/wrouter/generate"
)
const SessionMaxTime = time.Hour * 72
var sessionMutex sync.RWMutex
var sessions = make(map[string]*Session, 512)
var lastCheck = time.Now()
// SessionCookieName for the session cookie
var SessionCookieName = "sessid"
// Session is a simple in-memory structure describing a suer session
type Session struct {
ID string `json:"id"`
UserID string `json:"user"`
Time time.Time `json:"time"`
}
// OpenSession creates a new session from the supplied user's ID
func OpenSession(user *User) *Session {
session := &Session{generate.SessionID(), user.FullID(), time.Now()}
sessionMutex.Lock()
sessions[session.ID] = session
sessionMutex.Unlock()
// No need to do these checks when there's no activity.
if time.Since(lastCheck) > time.Hour {
lastCheck = time.Now()
go cleanup()
}
return session
}
// FindSession returns a session if the id maps to a still valid session
func FindSession(id string) *Session {
sessionMutex.RLock()
defer sessionMutex.RUnlock()
session := sessions[id]
// Check expiry and update
if session != nil {
if time.Since(session.Time) > SessionMaxTime {
return nil
}
if time.Since(session.Time) > time.Hour {
session.Time = time.Now()
}
}
return session
}
// CloseSession deletes a session by the id
func CloseSession(id string) {
sessionMutex.Lock()
delete(sessions, id)
sessionMutex.Unlock()
}
// ClearSessions removes all sessions with the given user ID
func ClearSessions(user *User) {
sessionMutex.Lock()
for _, sess := range sessions {
if sess.UserID == user.FullID() {
delete(sessions, sess.ID)
}
}
sessionMutex.Unlock()
}
func cleanup() {
count := 0
sessionMutex.Lock()
for key, session := range sessions {
if time.Since(session.Time) > SessionMaxTime {
delete(sessions, key)
count++
}
}
sessionMutex.Unlock()
if count > 0 {
log.Println("Removed", count, "sessions.")
}
}
+44
View File
@@ -0,0 +1,44 @@
package auth
import "testing"
func TestSession(t *testing.T) {
auther := testAuther{FullName: "Test"}
user := NewUser(&auther, "Tester", "Tester", "member", nil)
sessions := []*Session{OpenSession(user), OpenSession(user), OpenSession(user)}
ids := []string{sessions[0].ID, sessions[1].ID, sessions[2].ID}
t.Run("Find", func(t *testing.T) {
for i, id := range ids {
found := FindSession(id)
if found != sessions[i] {
t.Errorf("Find(\"%s\") == %+v", id, found)
t.Fail()
}
}
})
t.Run("Close", func(t *testing.T) {
CloseSession(ids[2])
if FindSession(ids[0]) == nil || FindSession(ids[1]) == nil || FindSession(ids[2]) != nil {
t.Errorf("Find(\"%s\") == %+v", ids[0], FindSession(ids[0]))
t.Errorf("Find(\"%s\") == %+v", ids[1], FindSession(ids[1]))
t.Errorf("Find(\"%s\") == %+v", ids[2], FindSession(ids[2]))
t.Fail()
}
})
t.Run("Clear", func(t *testing.T) {
ClearSessions(user)
if FindSession(ids[0]) != nil || FindSession(ids[1]) != nil || FindSession(ids[2]) != nil {
t.Errorf("Find(\"%s\") == %+v", ids[0], FindSession(ids[0]))
t.Errorf("Find(\"%s\") == %+v", ids[1], FindSession(ids[1]))
t.Errorf("Find(\"%s\") == %+v", ids[2], FindSession(ids[2]))
t.Fail()
}
})
}
+19
View File
@@ -0,0 +1,19 @@
package auth
type User struct {
ID string
Name string
Level string
Data map[string]string
method Authenticator
}
// FullID is the userid prefixed with the method ID
func (user *User) FullID() string {
return user.method.ID() + ":" + user.ID
}
func NewUser(method Authenticator, id, name, level string, data map[string]string) *User {
return &User{id, name, level, data, method}
}