Second Commit
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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{}
|
||||
@@ -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) {
|
||||
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
Reference in New Issue
Block a user